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/.env.example b/.env.example index be0ed2f..6ce661a 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,4 @@ PROVIDER_URL=http://localhost:8000/v1 PROVIDER_API_KEY=your-api-key PROVIDER_MODEL=gpt-4 COMPOSIO_API_KEY=your-api-key -AGENT_ID=user-12345 \ No newline at end of file +AGENT_ID=my_agent \ No newline at end of file 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/README.md b/README.md index e69de29..3e63a73 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,53 @@ +# Lambda Agent Backend + +#### Это headless агент, предназначенный для запуска на удаленном сервере. Используется через [agent_api](https://git.lambda.coredump.ru/platform/agent_api) + +## Запуск +### 1. Agent_api +Для локальной разработки и запуска нужен [модуль с API](https://git.lambda.coredump.ru/platform/agent_api). +Склонируйте его в любую директорию: +```bash +git clone https://git.lambda.coredump.ru/platform/agent_api agent_api +``` + +Далее нужно добавить путь до этой директории в переменную окружения: +```bash +export AGENT_API_PATH=C:/Users/User/agent_api +``` + +### 2. Environment +Заполните файл `.env` по примеру `.env.example`. +- `COMPOSIO_API_KEY` заполняется, если нужно подключить внешние инструменты из сервиса [Composio.dev](https://composio.dev/) + +### 3. Make (опционально) +Чтобы было удобнее работать с проектом, желательно установить утилиту `make`. + +### 4. Запуск +Через **Make**: +```bash +make up-dev +``` + +Без **Make**: +```bash +docker compose --profile dev up +``` + +### 5. Подключение и использование +Подключиться к агенту можно через скрипт [manual.py](https://git.lambda.coredump.ru/platform/agent_api/src/branch/master/tests/manual.py) из репозитория `agent_api`. +В качестве `base_url` необходимо указать `ws://localhost:8000/`. + +Данные агента хранятся в директории `data`: +- `data/internal` - служебная информация: история чатов и т. д. +- `data/workspace` - рабочее пространство агента. Тут он сохраняет все файлы, +сюда же они попадают с поверхностей. + +#### Отправка файлов в агента: +- Необходимо вручную добавить файл в директорию `data/workspace` +- Перед отправкой сообщения через скрипт в `agent_api` запрашиваются вложения: +`Attachments (comma-separated, empty for none): `. Необходимо через запятую перечислить пути до файлов внутри директории `workspace`. +Например, файл `data/workspace/my_dir/file.txt` -> `my_dir/file.txt` + +## Субагенты +В директории `subagents` можно создавать собственных субагентов. +Подробнее в [SUBAGENTS.md](SUBAGENTS.md) \ No newline at end of file diff --git a/SUBAGENTS.md b/SUBAGENTS.md new file mode 100644 index 0000000..b0781dc --- /dev/null +++ b/SUBAGENTS.md @@ -0,0 +1,73 @@ +# SubAgents + +## Структура + +``` +subagents/ # В корне репозитория +├── media-agent/ +│ ├── SUBAGENT.md # Метаданные + системный промпт +│ ├── image-gen/ # Skill (формат Deep Agents) +│ │ └── SKILL.md +│ └── meme-maker/ +│ └── SKILL.md +└── researcher/ + ├── SUBAGENT.md + └── web-search/ + └── SKILL.md +``` + +Папка `subagents/` монтируется в `/subagents/` через `docker-compose`. + +## SUBAGENT.md + +```markdown +--- +name: media-agent +description: Генерирует медиа-контент. Использовать для создания картинок, мемов, видео. +--- + +Ты субагент для генерации медиа-контента. +Описание задач, инструментов, формата ответа... +``` + +**Поля:** +- `name` - уникальный ID (используется в `task()`) +- `description` - когда основной агент должен делегировать задачу этому субагенту + +Всё после закрывающего `---` улетает в системный промпт. + +## SKILL.md (навыки) + +```markdown +--- +name: image-gen +description: Генерация изображений по текстовому описанию +--- + +# Image Generation +## When to Use +... +## How to Use +... +``` + +Каждый навык - отдельная папка с файлом `SKILL.md`. Находятся на одном уровне с `SUBAGENT.md`. +В папке навыка могут находиться дополнительные скрипты. + +## Как добавить субагента + +1. Создать папку в `subagents/` с именем субагента +2. Добавить `SUBAGENT.md` с YAML frontmatter и промптом +3. (Опционально) добавить папки со скилами +4. Перезапустить контейнер + +## Инструменты + +Субагенты наследуют все тулзы основного агента (Composio + custom tools). + +## Docker Compose + +```yaml +volumes: + - ./subagents:/subagents +``` 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