diff --git a/README.md b/README.md index 3d3f9a0..5ad8c30 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ API сервисы для Team Board. Python + FastAPI, микросервисн | gateway | 8000 | API Gateway, аутентификация | | projects | 8001 | Проекты, Git интеграция | | tasks | 8002 | Задачи, канбан, подзадачи | -| agents | 8003 | AI агенты | +| agents | 8003 | AI агенты, OpenClaw интеграция | | chat | 8004 | Чаты проектов | ## Структура @@ -22,10 +22,196 @@ backend/ │ ├── tasks/ │ ├── agents/ │ └── chat/ +├── shared/ +│ └── openclaw_client.py ├── docker-compose.yml └── README.md ``` +## OpenClaw Integration + +### Архитектура + +``` +Team Board (agents service) + │ + │ HTTP POST /hooks/agent + ▼ +OpenClaw Gateway (localhost:18789) + │ + │ sessions_spawn + ▼ +Субагенты (изолированные сессии) + │ + │ результат + ▼ +Callback → Team Board +``` + +### Конфигурация + +```env +# OpenClaw +OPENCLAW_URL=http://localhost:18789 +OPENCLAW_TOKEN=team-board-secret-token +``` + +### OpenClaw Client + +```python +# shared/openclaw_client.py + +import httpx +from typing import Optional + +class OpenClawClient: + def __init__(self, base_url: str, token: str): + self.base_url = base_url + self.headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json" + } + + async def wake(self, text: str, mode: str = "now") -> dict: + """Разбудить основную сессию""" + async with httpx.AsyncClient() as client: + response = await client.post( + f"{self.base_url}/hooks/wake", + headers=self.headers, + json={"text": text, "mode": mode} + ) + return response.json() + + async def run_agent( + self, + message: str, + session_key: str, + agent_id: str = "main", + deliver: bool = False, + timeout: int = 300 + ) -> dict: + """Запустить задачу в изолированной сессии""" + async with httpx.AsyncClient() as client: + response = await client.post( + f"{self.base_url}/hooks/agent", + headers=self.headers, + json={ + "message": message, + "name": "TeamBoard", + "agentId": agent_id, + "sessionKey": session_key, + "deliver": deliver, + "timeoutSeconds": timeout + } + ) + return response.json() + + async def get_session_history(self, session_key: str) -> dict: + """Получить историю сессии""" + async with httpx.AsyncClient() as client: + response = await client.get( + f"{self.base_url}/api/sessions/{session_key}/history", + headers=self.headers + ) + return response.json() +``` + +## Agents Service + +### Модели + +```python +# services/agents/models.py + +from sqlalchemy import Column, String, Integer, Text, Boolean +from sqlalchemy.dialects.postgresql import UUID, TIMESTAMPTZ +from shared.database import Base +import uuid + +class Agent(Base): + __tablename__ = "agents" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + name = Column(String(100), nullable=False) + slug = Column(String(50), unique=True, nullable=False) + + # Настройки + model = Column(String(100), default="claude-opus-4-5") + system_prompt = Column(Text) + max_concurrent = Column(Integer, default=1) + + # Статус + status = Column(String(20), default="idle") + current_tasks = Column(Integer, default=0) + is_enabled = Column(Boolean, default=True) + + # Timestamps + created_at = Column(TIMESTAMPTZ, server_default="now()") + updated_at = Column(TIMESTAMPTZ, server_default="now()", onupdate="now()") +``` + +### API Endpoints + +```python +# services/agents/routes.py + +from fastapi import APIRouter, Depends, HTTPException +from .models import Agent +from .schemas import AgentCreate, AgentUpdate, TaskAssignment +from shared.openclaw_client import OpenClawClient + +router = APIRouter(prefix="/agents", tags=["agents"]) + +@router.get("/") +async def list_agents(): + """Список всех агентов""" + pass + +@router.post("/") +async def create_agent(data: AgentCreate): + """Создать нового агента""" + pass + +@router.put("/{agent_id}") +async def update_agent(agent_id: str, data: AgentUpdate): + """Обновить агента""" + pass + +@router.post("/{agent_id}/assign") +async def assign_task(agent_id: str, data: TaskAssignment): + """Назначить задачу агенту""" + agent = await Agent.get(agent_id) + + if not agent.is_enabled: + raise HTTPException(400, "Agent is disabled") + + if agent.current_tasks >= agent.max_concurrent: + raise HTTPException(400, "Agent is busy") + + # Запускаем через OpenClaw + client = OpenClawClient(settings.OPENCLAW_URL, settings.OPENCLAW_TOKEN) + + result = await client.run_agent( + message=data.prompt, + session_key=f"task:{data.task_id}", + agent_id=agent.slug + ) + + # Обновляем статус + agent.current_tasks += 1 + agent.status = "working" + await agent.save() + + return result + +@router.post("/callback") +async def agent_callback(data: dict): + """Callback от OpenClaw после выполнения задачи""" + session_key = data.get("sessionKey") + # Обновить задачу, уменьшить current_tasks агента + pass +``` + ## Запуск ```bash @@ -45,12 +231,22 @@ docker-compose up -d - PostgreSQL - Redis - SQLAlchemy +- httpx (async HTTP client) ## Переменные окружения ```env +# Database DATABASE_URL=postgresql://team_board:password@localhost:5432/team_board + +# Redis REDIS_URL=redis://localhost:6379 + +# Auth AUTHENTIK_CLIENT_ID=... AUTHENTIK_CLIENT_SECRET=... + +# OpenClaw +OPENCLAW_URL=http://localhost:18789 +OPENCLAW_TOKEN=team-board-secret-token ```