Compare commits

..

No commits in common. "#22-local-subagents-and-skills" and "main" have entirely different histories.

9 changed files with 8 additions and 243 deletions

View file

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

View file

@ -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=my_agent
AGENT_ID=user-12345

2
.gitignore vendored
View file

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

View file

@ -9,7 +9,6 @@ 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

@ -1,53 +0,0 @@
# 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)

View file

@ -1,73 +0,0 @@
# 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
```

View file

@ -23,7 +23,6 @@ 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,28 +41,16 @@ 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=routes,
routes={
workspace_dir: FilesystemBackend(workspace_dir, virtual_mode=True),
}
)
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(
@ -71,18 +59,12 @@ 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=["/**"],
@ -101,3 +83,4 @@ def create_agent() -> Agent:
logger.exception(f"Error creating agent: {e}")
raise
return agent

View file

@ -1,89 +0,0 @@
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