From 40a56c9f11d6495e4d76d5c65cc0b0194fd0d5dc Mon Sep 17 00:00:00 2001 From: MrKan Date: Sun, 3 May 2026 16:53:13 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B0=D0=B2=D1=82=D0=BE=D0=B7=D0=B0=D0=B3?= =?UTF-8?q?=D1=80=D1=83=D0=B7=D0=BA=D0=B0=20=D1=81=D1=83=D0=B1=D0=B0=D0=B3?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 1 + .gitignore | 2 +- Dockerfile | 1 + docker-compose.yml | 1 + src/agent/base.py | 29 +++++++++++--- src/agent/subagents.py | 89 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 src/agent/subagents.py diff --git a/.dockerignore b/.dockerignore index f83dc7f..a8124c6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,3 +8,4 @@ __pycache__/ .env /data/ +/subagents/ diff --git a/.gitignore b/.gitignore index 6134988..95db2a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ /data/ +/subagents/ .idea/ -workspace/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/Dockerfile b/Dockerfile index e731081..36cc859 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ RUN apt update && apt install make sudo -y ENV AGENT_USER="agent" ENV WORKSPACE_DIR="/workspace/" ENV INTERNAL_DATA_DIR="/internal_data/" +ENV SUBAGENTS_DIR="/subagents/" RUN useradd --shell /bin/bash $AGENT_USER \ && mkdir -p $WORKSPACE_DIR /home/$AGENT_USER \ && chown -R agent:agent $WORKSPACE_DIR /home/$AGENT_USER diff --git a/docker-compose.yml b/docker-compose.yml index fbf4585..3bcaf63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,6 +23,7 @@ services: - ${AGENT_API_PATH}:/agent_api/ - ./data/workspace:/workspace/ - ./data/internal:/internal_data/ + - ./subagents:/subagents ports: - "8000:8000" env_file: diff --git a/src/agent/base.py b/src/agent/base.py index 425c6ea..d7c6aa8 100644 --- a/src/agent/base.py +++ b/src/agent/base.py @@ -1,13 +1,13 @@ import os +from typing import Any from deepagents import create_deep_agent, FilesystemPermission from deepagents.backends import CompositeBackend, FilesystemBackend, StateBackend from langchain_openai import ChatOpenAI -from langgraph.checkpoint.memory import MemorySaver from langgraph.graph.state import CompiledStateGraph from composio import Composio from composio_langchain import LangchainProvider -from langgraph.checkpoint.sqlite import SqliteSaver +from src.agent.subagents import load_subagents from src.agent.tools import send_file, execute_shell from src.agent.checkpointer import get_active_checkpointer from src.core.logger import get_logger @@ -41,16 +41,28 @@ def create_agent() -> Agent: workspace_dir = os.environ["WORKSPACE_DIR"] + routes: dict[str, Any] = { + workspace_dir: FilesystemBackend(workspace_dir, virtual_mode=True), + } + + subagents_dir = os.environ.get("SUBAGENTS_DIR") + if subagents_dir and os.path.isdir(subagents_dir): + routes["/subagents/"] = FilesystemBackend(subagents_dir, virtual_mode=True) + logger.debug(f"Mounted subagents directory at /subagents/: {subagents_dir}") + backend = CompositeBackend( default=StateBackend(), - routes={ - workspace_dir: FilesystemBackend(workspace_dir, virtual_mode=True), - } + routes=routes, ) logger.debug(f"Configured CompositeBackend with workspace: {workspace_dir}") checkpointer = get_active_checkpointer() logger.debug(f"Retrieved checkpointer: {type(checkpointer).__name__}") + + subagents = load_subagents() + subagents_log = f" with {len(subagents)} subagent(s)" if subagents else "" + logger.info(f"Creating agent{subagents_log}") + # noinspection PyTypeChecker # create_deep_agent возвращает CompiledStateGraph, но ниже мы его дополняем так, чтобы он соответствовал сигнатуре Agent agent: Agent = create_deep_agent( @@ -59,12 +71,18 @@ def create_agent() -> Agent: tools=tools + [send_file, execute_shell], backend=backend, checkpointer=checkpointer, + subagents=subagents if subagents else None, permissions=[ FilesystemPermission( operations=["read", "write"], paths=["/workspace/**"], mode="allow", ), + FilesystemPermission( + operations=["read"], + paths=["/subagents/**"], + mode="allow", + ), FilesystemPermission( operations=["read", "write"], paths=["/**"], @@ -83,4 +101,3 @@ def create_agent() -> Agent: logger.exception(f"Error creating agent: {e}") raise return agent - \ No newline at end of file diff --git a/src/agent/subagents.py b/src/agent/subagents.py new file mode 100644 index 0000000..de4b147 --- /dev/null +++ b/src/agent/subagents.py @@ -0,0 +1,89 @@ +import os +import yaml +from typing import Any + +from src.core.logger import get_logger + +logger = get_logger(__name__) + +SUBAGENT_MD = "SUBAGENT.md" + + +def _parse_subagent_md(file_path: str) -> dict[str, Any] | None: + """Парсит SUBAGENT.md файл, извлекая YAML метаданные и системный промпт.""" + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + except (OSError, IOError) as e: + logger.error(f"Failed to read {file_path}: {e}") + return None + + if not content.startswith("---"): + logger.warning( + f"Invalid SUBAGENT.md format in {file_path}: missing YAML frontmatter" + ) + return None + + # Находим закрывающий --- + end_marker = content.find("---", 3) + if end_marker == -1: + logger.warning( + f"Invalid SUBAGENT.md format in {file_path}: missing closing ---" + ) + return None + + yaml_content = content[3:end_marker].strip() + system_prompt = content[end_marker + 3 :].strip() + + try: + metadata = yaml.safe_load(yaml_content) or {} + except yaml.YAMLError as e: + logger.error(f"Failed to parse YAML in {file_path}: {e}") + return None + + name = metadata.get("name") + description = metadata.get("description") + + if not name or not description: + logger.warning(f"Missing required fields (name, description) in {file_path}") + return None + + return { + "name": name, + "description": description, + "system_prompt": system_prompt, + "skills": [f"/subagents/{name}/"], + } + + +def load_subagents() -> list[dict[str, Any]]: + """Загружает субагентов из директории SUBAGENTS_DIR.""" + subagents_dir = os.environ.get("SUBAGENTS_DIR") + if not subagents_dir: + logger.debug("SUBAGENTS_DIR not set, skipping subagent loading") + return [] + + if not os.path.isdir(subagents_dir): + logger.warning(f"SUBAGENTS_DIR '{subagents_dir}' is not a valid directory") + return [] + + subagents = [] + for entry in os.listdir(subagents_dir): + subagent_path = os.path.join(subagents_dir, entry) + if not os.path.isdir(subagent_path): + continue + + subagent_md_path = os.path.join(subagent_path, SUBAGENT_MD) + if not os.path.isfile(subagent_md_path): + logger.debug(f"No {SUBAGENT_MD} found in {subagent_path}, skipping") + continue + + subagent_spec = _parse_subagent_md(subagent_md_path) + if subagent_spec: + subagents.append(subagent_spec) + logger.info( + f"Loaded subagent '{subagent_spec['name']}': {subagent_spec['description'][:50]}..." + ) + + logger.info(f"Total subagents loaded: {len(subagents)}") + return subagents