- tests/test_compactor.py: 24 tests (estimate_tokens, extract_facts, build_summary, summarize_tool_output, compact_sections) - tests/test_key_data_extraction.py: 11 tests (extracción de tables, records, sections, modules, pages desde tool executions) - tests/test_fingerprint.py: 8 tests (deduplicación MD5, sort_keys, nested args) - tests/test_cost_calculation.py: 8 tests (pricing formula, custom pricing, rounding) - README.md: sección Tests con instrucciones de ejecución Todos offline, sin Docker/Redis/LLM. Ejecutar: python3 -m pytest tests/ -v Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
464 lines
18 KiB
Markdown
464 lines
18 KiB
Markdown
# Agentic Microservice
|
||
|
||
Microservicio de orquestación agéntica con context engineering avanzado, multi-agent orchestration, integración MCP, SSE streaming y knowledge base. Diseñado como backend para Acai Code.
|
||
|
||
## Arquitectura
|
||
|
||
```
|
||
Client → API (FastAPI) → Orchestrator → Subagents (planner/coder/collector/reviewer)
|
||
↓ ↓
|
||
Context Engine Model Adapter (Claude / OpenAI)
|
||
↓ ↓
|
||
Memory Store + KB MCP Client (stdio)
|
||
↓
|
||
Redis (sessions, events, knowledge)
|
||
```
|
||
|
||
**Principios:**
|
||
- **Context over chat history** — el modelo recibe estado estructurado, nunca raw tool outputs
|
||
- **Semantic search** — embeddings (OpenAI text-embedding-3-small) para seleccionar los docs más relevantes por similitud semántica
|
||
- **Task history compaction** — al completar una tarea, se comprime a ~50 tokens; en sesiones largas el agente recuerda todo sin desbordar el contexto
|
||
- **Artifact summarization** — outputs de tools resumidos antes de entrar al contexto
|
||
- **Error recovery** — steps fallidos se saltan, timeout global, sesión nunca se queda en executing
|
||
- **Concurrency lock** — Redis SETNX impide ejecución simultánea en la misma sesión
|
||
|
||
## Quick Start
|
||
|
||
### Con Docker Compose
|
||
|
||
```bash
|
||
# 1. Configurar
|
||
cp .env.example .env
|
||
# Editar .env — al menos una API key (ANTHROPIC o OPENAI)
|
||
|
||
# 2. Levantar todo (Redis + microservicio)
|
||
docker compose up --build
|
||
|
||
# 3. Solo Redis (para desarrollo local)
|
||
docker compose up redis -d
|
||
```
|
||
|
||
### Desarrollo local
|
||
|
||
```bash
|
||
# 1. Redis
|
||
docker compose up redis -d
|
||
|
||
# 2. Dependencias
|
||
pip install -r requirements.txt
|
||
|
||
# 3. Configurar .env
|
||
AGENTIC_OPENAI_API_KEY=sk-...
|
||
AGENTIC_DEFAULT_MODEL_PROVIDER=openai
|
||
AGENTIC_DEFAULT_MODEL_ID=gpt-4o
|
||
AGENTIC_REDIS_PORT=6380
|
||
|
||
# 4. Arrancar
|
||
python3 -m uvicorn src.main:app --reload --port 8001
|
||
|
||
# 5. Dashboard en http://localhost:8001/dashboard/
|
||
```
|
||
|
||
### Tests
|
||
|
||
```bash
|
||
# Ejecutar todos los tests unitarios (no necesita Docker, Redis ni LLM)
|
||
pip install pytest
|
||
python3 -m pytest tests/ -v
|
||
|
||
# Ejecutar un archivo específico
|
||
python3 -m pytest tests/test_compactor.py -v
|
||
|
||
# Ejecutar un test específico
|
||
python3 -m pytest tests/test_cost_calculation.py::TestCostCalculation::test_1m_input_tokens -v
|
||
```
|
||
|
||
Los tests validan: compactación de contexto, extracción de key_data para historial, fingerprinting de tool calls, y cálculo de costes. Son 100% offline — no consumen tokens ni necesitan servicios externos.
|
||
|
||
### Cargar Knowledge Base
|
||
|
||
```bash
|
||
# Cargar docs + generar embeddings para semantic search
|
||
curl -X POST http://localhost:8001/api/v1/knowledge/load \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"docs_path": "docs"}'
|
||
# → {"status": "loaded", "count": 13, "embeddings": true, ...}
|
||
```
|
||
|
||
Los embeddings se generan en batch via OpenAI `text-embedding-3-small` (~$0.001 por los 13 docs) y se almacenan en Redis. Solo se recalculan al hacer `/knowledge/load` de nuevo.
|
||
|
||
## Dashboard
|
||
|
||
Interfaz web para testing integrada en el microservicio. Se accede en `/dashboard/`.
|
||
|
||
**Features:**
|
||
- Gestión de sesiones (crear, eliminar, seleccionar)
|
||
- Chat con envío sync (Send) o streaming (Stream)
|
||
- Event Log en tiempo real con filtros por categoría y export JSON
|
||
- Inspector de estado (task, plan, facts, constraints, timeline)
|
||
- Context Debug — muestra exactamente qué recibe cada agente (secciones, tokens, previews)
|
||
- Dark/light mode
|
||
- Responsive (3 columnas → 1 columna en móvil)
|
||
|
||
**Atajos de teclado:**
|
||
- `Enter` → Send
|
||
- `Ctrl+Enter` → Stream
|
||
- `Shift+Enter` → Nueva línea
|
||
|
||
## API Reference
|
||
|
||
Base URL: `/api/v1`
|
||
|
||
### Sesiones
|
||
|
||
```bash
|
||
# Crear sesión
|
||
curl -X POST http://localhost:8001/api/v1/sessions \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"project_profile": {"name": "mi-proyecto", "tech_stack": ["python", "twig"]},
|
||
"immutable_rules": ["Responde siempre en español", "Usa Tailwind CSS"],
|
||
"metadata": {}
|
||
}'
|
||
# → {"session_id": "abc123...", "status": "idle"}
|
||
|
||
# Obtener estado
|
||
curl http://localhost:8001/api/v1/sessions/{session_id}
|
||
|
||
# Eliminar
|
||
curl -X DELETE http://localhost:8001/api/v1/sessions/{session_id}
|
||
```
|
||
|
||
### Mensajes
|
||
|
||
```bash
|
||
# Sync — espera la respuesta completa
|
||
curl -X POST http://localhost:8001/api/v1/sessions/{session_id}/messages \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"message": "Crea un módulo FAQ con acordeón"}'
|
||
|
||
# Streaming — retorna inmediatamente, resultados vía SSE
|
||
curl -X POST http://localhost:8001/api/v1/sessions/{session_id}/messages \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"message": "Crea un módulo FAQ con acordeón", "stream": true}'
|
||
```
|
||
|
||
Respuesta sync:
|
||
```json
|
||
{
|
||
"session_id": "...",
|
||
"task_id": "...",
|
||
"content": "### Step 1\n...\n### Review\n...",
|
||
"steps_completed": 5,
|
||
"steps_failed": [],
|
||
"artifacts_count": 0,
|
||
"review": "...",
|
||
"status": "completed"
|
||
}
|
||
```
|
||
|
||
### SSE Streaming
|
||
|
||
```bash
|
||
curl -N http://localhost:8001/api/v1/sessions/{session_id}/stream
|
||
```
|
||
|
||
Tipos de evento:
|
||
| Evento | Cuándo |
|
||
|--------|--------|
|
||
| `session.created` | Sesión inicializada |
|
||
| `execution.started` | Pipeline arranca |
|
||
| `subagent.assigned` | Step ruteado a un agente |
|
||
| `agent.delta` | Chunk de texto del agente |
|
||
| `tool.started` | Herramienta MCP ejecutándose |
|
||
| `tool.completed` | Herramienta terminó |
|
||
| `execution.completed` | Task completado |
|
||
| `error` | Error en step, planning, o timeout |
|
||
|
||
### Knowledge Base
|
||
|
||
```bash
|
||
# Cargar docs desde directorio
|
||
curl -X POST http://localhost:8001/api/v1/knowledge/load \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"docs_path": "docs"}'
|
||
|
||
# Cargar desde ruta absoluta
|
||
curl -X POST http://localhost:8001/api/v1/knowledge/load \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"docs_path": "/ruta/a/mis/docs"}'
|
||
|
||
# Listar docs cargados
|
||
curl http://localhost:8001/api/v1/knowledge
|
||
|
||
# Eliminar un doc
|
||
curl -X DELETE http://localhost:8001/api/v1/knowledge/{doc_id}
|
||
```
|
||
|
||
Los docs se cargan como `*.md` del directorio. Se sobreescriben si ya existen. No requiere reinicio — la siguiente conversación usa los docs actualizados. Al cargar se generan embeddings automáticamente para semantic search.
|
||
|
||
### Debug
|
||
|
||
```bash
|
||
# Historial de eventos (persistidos en Redis)
|
||
curl http://localhost:8001/api/v1/sessions/{session_id}/events
|
||
|
||
# Context debug — qué recibió cada agente
|
||
curl http://localhost:8001/api/v1/sessions/{session_id}/context-debug
|
||
|
||
# Health check
|
||
curl http://localhost:8001/health
|
||
```
|
||
|
||
## Context Engine
|
||
|
||
El componente más crítico del sistema. Ensambla el prompt que recibe el modelo con secciones priorizadas:
|
||
|
||
| Sección | Prioridad | Contenido |
|
||
|---------|-----------|-----------|
|
||
| `immutable_rules` | 100 | System prompt del agente + reglas de sesión. Nunca se recorta |
|
||
| `project_profile` | 80 | Perfil del proyecto (nombre, tech stack) |
|
||
| `knowledge_base` | 60 | Docs relevantes por semantic search + índice de títulos de todos |
|
||
| `task_history` | 55 | Resúmenes compactos de tareas anteriores (~50 tokens c/u) |
|
||
| `task_state` | 70 | Objetivo, plan, step actual, facts, constraints |
|
||
| `artifact_memory` | 50 | Resúmenes de outputs de herramientas (solo tarea actual) |
|
||
| `working_context` | 30 | Items de trabajo recientes entre steps |
|
||
|
||
**Semantic search:** genera embedding del objetivo/step del usuario y busca por cosine similarity contra los embeddings de todos los docs. Los más relevantes entran completos (budget 15k tokens), el resto aparece como título + summary para que el agente sepa que existen y pueda pedir rehidratación.
|
||
|
||
**Task history compaction:** al completar cada tarea, sus artifacts, facts y resultados se comprimen en un bloque de ~50 tokens que se guarda en `task_history`. Los artifacts viejos se borran de Redis. En una sesión de 20 tareas, el historial ocupa ~1000 tokens vs los miles que ocuparían los artifacts completos. El agente nunca pierde contexto de lo que hizo antes.
|
||
|
||
**Token counting:** usa `tiktoken` (encoding `cl100k_base`) para conteo real. Fallback a estimación si tiktoken no está instalado.
|
||
|
||
**Compaction:** si el total excede el context window (120k tokens default), las secciones de menor prioridad se comprimen o eliminan. `immutable_rules` nunca se toca.
|
||
|
||
**Presupuesto típico tras 20 tareas:**
|
||
```
|
||
immutable_rules: ~250 tokens (fijo)
|
||
project_profile: ~15 tokens (fijo)
|
||
knowledge_base: ~15,000 tokens (semantic search)
|
||
task_history: ~1,000 tokens (20 tareas × ~50 tokens)
|
||
task_state: ~400 tokens (tarea actual)
|
||
artifact_memory: ~500 tokens (solo tarea actual)
|
||
working_context: ~500 tokens (solo step actual)
|
||
─────────────────────────────────────
|
||
TOTAL: ~17,700 tokens / 120,000 disponibles
|
||
```
|
||
|
||
## Orchestrator Pipeline
|
||
|
||
```
|
||
mensaje → planner → [step₁ → step₂ → ... → stepₙ] → reviewer → respuesta
|
||
```
|
||
|
||
1. **Planner** descompone el mensaje en pasos con agent role asignado
|
||
2. **Router** rutea cada step al agente apropiado (keyword matching: implement→coder, buscar→collector, etc.)
|
||
3. **Subagent** ejecuta el step con su propio contexto controlado
|
||
4. Si un step **falla**, se marca como failed y el pipeline continúa con el siguiente
|
||
5. **Reviewer** valida el trabajo si hubo >1 step
|
||
6. **Timeout global** de 5 min (configurable) — si se excede, la sesión pasa a error
|
||
|
||
**Concurrency:** Redis lock (SETNX) con TTL de 5 min. Si llega un segundo mensaje mientras uno se ejecuta → `{"status": "busy"}`. El lock se libera automáticamente si el proceso muere.
|
||
|
||
## MCP (Model Context Protocol)
|
||
|
||
Arquitectura **per-session**: el microservicio corre como un Docker único, y cada sesión arranca sus propios subprocesos MCP con las env vars del proyecto del usuario.
|
||
|
||
### Modelo de operación
|
||
|
||
```
|
||
Docker (microservicio — uno solo corriendo)
|
||
└── mcp.json ← template: QUÉ servers arrancar (global)
|
||
└── mcp-server/ ← código MCP (baked-in)
|
||
|
||
POST /sessions { mcp_env: { ACAI_WEB_URL: "https://tienda.com", ... } }
|
||
→ Arranca subprocesos MCP con esas env vars
|
||
→ Session aislada con su propio MCPManager
|
||
→ 55 tools descubiertas (acai-code: 33, playwright: 21, fetch: 1)
|
||
|
||
DELETE /sessions/{id}
|
||
→ Mata los subprocesos MCP de esa sesión
|
||
```
|
||
|
||
### Configuración
|
||
|
||
**1. Template global** (`mcp.json` en la raíz — define QUÉ servers existen):
|
||
|
||
```json
|
||
{
|
||
"mcpServers": {
|
||
"acai-code": {
|
||
"command": "node",
|
||
"args": ["mcp-server/stdio.js"],
|
||
"env": {},
|
||
"timeout": 30,
|
||
"startup_timeout": 10
|
||
},
|
||
"playwright": {
|
||
"command": "npx",
|
||
"args": ["@playwright/mcp", "--headless"],
|
||
"timeout": 30,
|
||
"startup_timeout": 15
|
||
},
|
||
"fetch": {
|
||
"command": "uvx",
|
||
"args": ["mcp-server-fetch"],
|
||
"timeout": 30,
|
||
"startup_timeout": 15
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
```bash
|
||
# En .env
|
||
AGENTIC_MCP_CONFIG_PATH=mcp.json
|
||
```
|
||
|
||
**2. Per-session env vars** (cada usuario/proyecto pasa las suyas al crear sesión):
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8001/api/v1/sessions \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"project_profile": {"name": "tienda-online"},
|
||
"mcp_env": {
|
||
"ACAI_WEB_URL": "https://superadmin_tienda.forge.acaisuite.com/",
|
||
"ACAI_WEBSITE": "tienda.com",
|
||
"ACAI_PROJECT_DIR": "/projects/tienda"
|
||
}
|
||
}'
|
||
```
|
||
|
||
Las env vars de `mcp_env` se fusionan con las del template y se pasan al subproceso MCP. El MCP server lee el token desde el `.acai` del proyecto automáticamente.
|
||
|
||
**3. Legacy** (un solo server global, sin `mcp.json`):
|
||
|
||
```bash
|
||
AGENTIC_MCP_SERVER_COMMAND=node
|
||
AGENTIC_MCP_SERVER_ARGS=["mcp-server/stdio.js"]
|
||
```
|
||
|
||
### Tool namespacing
|
||
|
||
En modo multi-server, los tools se namespean con `__` (compatible con OpenAI que no acepta puntos): `acai_code__compile_module`, `playwright__browser_navigate`. En modo single-server los tools mantienen su nombre original.
|
||
|
||
### API de gestión MCP
|
||
|
||
```bash
|
||
# Ver estado de todos los MCP servers activos (por sesión)
|
||
curl http://localhost:8001/api/v1/mcp/status
|
||
|
||
# Hot-reload del template (re-lee mcp.json, no afecta sesiones activas)
|
||
curl -X POST http://localhost:8001/api/v1/mcp/reload
|
||
```
|
||
|
||
### Ciclo de vida
|
||
|
||
1. `POST /sessions` con `mcp_env` → arranca subprocesos MCP para esa sesión
|
||
2. `POST /sessions/{id}/messages` → el agente usa los tools del MCP de esa sesión
|
||
3. Si el server se reinicia y la sesión sigue en Redis → el MCP se reconecta automáticamente al siguiente mensaje
|
||
4. `DELETE /sessions/{id}` → mata subprocesos MCP de esa sesión
|
||
|
||
Los resultados de tools **nunca** entran al contexto como raw output — se resumen como artifacts.
|
||
|
||
## Configuración
|
||
|
||
Variables de entorno con prefijo `AGENTIC_`:
|
||
|
||
| Variable | Default | Descripción |
|
||
|----------|---------|-------------|
|
||
| `AGENTIC_ANTHROPIC_API_KEY` | — | API key de Anthropic |
|
||
| `AGENTIC_OPENAI_API_KEY` | — | API key de OpenAI |
|
||
| `AGENTIC_DEFAULT_MODEL_PROVIDER` | `claude` | `claude` o `openai` |
|
||
| `AGENTIC_DEFAULT_MODEL_ID` | `claude-sonnet-4-20250514` | Modelo por defecto |
|
||
| `AGENTIC_REDIS_HOST` | `localhost` | Host de Redis |
|
||
| `AGENTIC_REDIS_PORT` | `6379` | Puerto de Redis |
|
||
| `AGENTIC_REDIS_DB` | `0` | Base de datos Redis |
|
||
| `AGENTIC_REDIS_PASSWORD` | — | Password de Redis |
|
||
| `AGENTIC_MAX_TOKENS` | `4096` | Max tokens de salida por llamada |
|
||
| `AGENTIC_CONTEXT_MAX_TOKENS` | `120000` | Max context window |
|
||
| `AGENTIC_MAX_EXECUTION_STEPS` | `25` | Max steps por task |
|
||
| `AGENTIC_MAX_EXECUTION_TIMEOUT_SECONDS` | `300` | Timeout global (5 min) |
|
||
| `AGENTIC_SUBAGENT_MAX_STEPS` | `10` | Max iterations por subagent |
|
||
| `AGENTIC_MCP_CONFIG_PATH` | — | Ruta a `mcp.json` (multi-MCP per-session) |
|
||
| `AGENTIC_MCP_SERVER_COMMAND` | — | Legacy: comando del servidor MCP (single) |
|
||
| `AGENTIC_MCP_SERVER_ARGS` | `[]` | Legacy: argumentos del servidor MCP |
|
||
| `AGENTIC_MCP_TIMEOUT_SECONDS` | `30` | Timeout por tool call |
|
||
| `AGENTIC_DEBUG` | `false` | Logging verbose |
|
||
|
||
## Redis Key Structure
|
||
|
||
```
|
||
agentic:session:{id} → SessionState JSON (TTL 24h)
|
||
agentic:session:{id}:artifacts → Hash de ArtifactSummary
|
||
agentic:session:{id}:events → Lista de SSE events (cap 500)
|
||
agentic:session:{id}:lock → Execution lock (SETNX, TTL 5min)
|
||
agentic:sessions:index → Set de session IDs activos
|
||
agentic:memory:knowledge:{doc_id} → MemoryDocument JSON
|
||
agentic:memory:knowledge:_index → Set de doc IDs
|
||
agentic:embeddings:knowledge:{doc_id} → Embedding vector (1536 floats JSON)
|
||
agentic:memory:_tag:{tag} → Set de doc IDs por tag
|
||
agentic:memory:_type:{type} → Set de doc IDs por tipo
|
||
```
|
||
|
||
## Project Structure
|
||
|
||
```
|
||
.
|
||
├── src/
|
||
│ ├── main.py # FastAPI app, lifecycle, wiring
|
||
│ ├── config.py # Pydantic settings
|
||
│ ├── models/ # Pydantic v2 data models
|
||
│ │ ├── session.py # SessionState, TaskState, TaskStep
|
||
│ │ ├── context.py # ContextPackage, MemoryDocument
|
||
│ │ ├── agent.py # AgentProfile, SubAgentDefinition
|
||
│ │ ├── artifacts.py # ArtifactSummary
|
||
│ │ └── tools.py # ToolExecution, ToolDefinition
|
||
│ ├── context/ # Context Engine (core)
|
||
│ │ ├── engine.py # Prompt assembly, knowledge filtering, debug
|
||
│ │ └── compactor.py # Compaction, summarization, tiktoken
|
||
│ ├── memory/ # Persistent memory
|
||
│ │ ├── store.py # Redis-backed memory + similarity search
|
||
│ │ └── embeddings.py # OpenAI embedding service
|
||
│ ├── adapters/ # Model provider adapters
|
||
│ │ ├── base.py # ModelAdapter interface
|
||
│ │ ├── claude_adapter.py # Anthropic Claude (streaming)
|
||
│ │ └── openai_adapter.py # OpenAI GPT (streaming)
|
||
│ ├── mcp/ # MCP (per-session, multi-server)
|
||
│ │ ├── client.py # stdio transport, tool registry
|
||
│ │ ├── config.py # mcp.json parser (Pydantic)
|
||
│ │ ├── manager.py # MCPManager (aggregates tools, routes calls)
|
||
│ │ └── registry.py # Per-session MCP lifecycle
|
||
│ ├── orchestrator/ # Agent orchestration
|
||
│ │ ├── engine.py # Pipeline + error recovery + timeout
|
||
│ │ ├── router.py # Step-to-agent routing
|
||
│ │ └── agents/
|
||
│ │ ├── base.py # Shared execution loop
|
||
│ │ ├── planner.py # Plan decomposition
|
||
│ │ ├── coder.py # Implementation
|
||
│ │ ├── collector.py # Context gathering
|
||
│ │ └── reviewer.py # Validation
|
||
│ ├── streaming/ # SSE streaming
|
||
│ │ └── sse.py # Event emitter + Redis persistence
|
||
│ ├── storage/ # Persistence
|
||
│ │ └── redis.py # Sessions, events, locks
|
||
│ └── api/ # REST endpoints
|
||
│ └── routes.py # Sessions, messages, knowledge, debug
|
||
├── dashboard/ # Testing UI (vanilla JS, zero build)
|
||
│ ├── index.html
|
||
│ ├── css/main.css
|
||
│ └── js/
|
||
│ ├── app.js # State, SSE handlers
|
||
│ ├── api.js # Fetch + EventSource
|
||
│ └── components/ # Sidebar, chat, event log, inspector, timeline
|
||
├── mcp-server/ # Acai MCP server (Node.js, stdio)
|
||
├── docs/ # Knowledge base documents (*.md)
|
||
├── mcp.json # MCP server template (qué servers arrancar)
|
||
├── mcp.json.example # Ejemplo con múltiples servers
|
||
├── Dockerfile
|
||
├── docker-compose.yml
|
||
├── requirements.txt
|
||
└── .env.example
|
||
```
|