автозагрузка субагентов

This commit is contained in:
Егор Кандрушин 2026-05-03 16:53:13 +03:00
parent 684848eda9
commit 40a56c9f11
6 changed files with 116 additions and 7 deletions

View file

@ -8,3 +8,4 @@
__pycache__/
.env
/data/
/subagents/

2
.gitignore vendored
View file

@ -1,7 +1,7 @@
/data/
/subagents/
.idea/
workspace/
# Byte-compiled / optimized / DLL files
__pycache__/

View file

@ -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

View file

@ -23,6 +23,7 @@ services:
- ${AGENT_API_PATH}:/agent_api/
- ./data/workspace:/workspace/
- ./data/internal:/internal_data/
- ./subagents:/subagents
ports:
- "8000:8000"
env_file:

View file

@ -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

89
src/agent/subagents.py Normal file
View file

@ -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