Selector de agentes
This commit is contained in:
@@ -1,6 +1,3 @@
|
||||
from .planner import PlannerAgent
|
||||
from .coder import CoderAgent
|
||||
from .collector import CollectorAgent
|
||||
from .reviewer import ReviewerAgent
|
||||
from .base import BaseAgent
|
||||
|
||||
__all__ = ["PlannerAgent", "CoderAgent", "CollectorAgent", "ReviewerAgent"]
|
||||
__all__ = ["BaseAgent"]
|
||||
|
||||
@@ -96,9 +96,7 @@ class BaseAgent:
|
||||
):
|
||||
if chunk.delta:
|
||||
full_text += chunk.delta
|
||||
# Only emit deltas for user-facing agents (coder, collector)
|
||||
# Planner/reviewer output is internal
|
||||
if self.profile.role not in ("planner", "reviewer"):
|
||||
if self.profile.stream_deltas:
|
||||
await self.sse.emit(
|
||||
EventType.AGENT_DELTA,
|
||||
{
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
"""Coder agent — executes implementation steps using tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ...models.agent import AgentProfile, AgentRole
|
||||
from .base import BaseAgent
|
||||
|
||||
CODER_SYSTEM_PROMPT = """Eres el asistente de desarrollo de Acai CMS. Ayudas al usuario con su web: crear módulos, editar contenido, explorar páginas, gestionar datos, y responder preguntas.
|
||||
|
||||
# Acai CMS — Project Instructions
|
||||
|
||||
This is an Acai CMS website project. Follow these instructions when working with the codebase.
|
||||
|
||||
## Environment
|
||||
|
||||
- The site runs in Docker, typically at **http://localhost:8080**
|
||||
- You can make HTTP requests to test pages, APIs, or form submissions
|
||||
- If you need to inspect the live site, use browser tools (Playwright MCP) or HTTP requests to localhost:8080
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── template/estandar/
|
||||
│ ├── modulos/ # Builder modules (visual components)
|
||||
│ │ └── <module-id>/
|
||||
│ │ ├── index-base.tpl # Twig template (source — EDIT THIS)
|
||||
│ │ ├── style.css # Module styles
|
||||
│ │ └── script.js # Module JavaScript
|
||||
│ │ ├── index.tpl # Compiled (auto-generated, do NOT edit)
|
||||
│ │ ├── index-twig.tpl # Compiled (auto-generated, do NOT edit)
|
||||
│ │ └── builder.json # Compiled builder vars (auto-generated, do NOT edit)
|
||||
│ ├── css/ # Global CSS
|
||||
│ └── js/ # Global JavaScript
|
||||
├── hooks/ # PHP hooks (server-side logic)
|
||||
├── cms/
|
||||
│ ├── data/schema/ # Database table schemas (JSON)
|
||||
│ ├── lib/plugins/ # CMS plugins
|
||||
│ └── uploads/ # Uploaded media files
|
||||
├── .acai # Project config (domain, tokens, DB credentials)
|
||||
├── .docker/
|
||||
│ ├── .env # Docker environment (DB credentials)
|
||||
│ ├── docker-compose.yml
|
||||
│ ├── tunnel-url.txt # Public tunnel URL (if active)
|
||||
│ └── bore-db-url.txt # Database tunnel URL (if active)
|
||||
└── database.sql # Database dump
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Modules (`template/estandar/modulos/`)
|
||||
Visual components that the site builder uses. Each module is a self-contained unit with its own template (Twig + Acai attributes), CSS, and JS. Modules are placed on pages via the drag-and-drop builder. The editable file is always `index-base.tpl`.
|
||||
|
||||
- Include other modules: `<module_id :param1="value1"></module_id>`
|
||||
- Each module instance gets a unique `section_id` variable for anchors/scoping
|
||||
- Use `interno` variable to detect CMS editor mode vs public view
|
||||
|
||||
See [docs/modular-system.md](docs/modular-system.md) for detailed rules.
|
||||
|
||||
### Pages
|
||||
Every record with an `enlace` field is a page. Pages are either **Builder** (modular) or **Standard**:
|
||||
|
||||
- **Builder**: `controlador` = `cms/lib/plugins/builder_saas/controlador.php` — content via modules
|
||||
- **Standard**: `controlador` = `cms/lib/plugins/builder_saas/controlador_tabla.php` — content in record fields
|
||||
|
||||
**Critical**: Never change `enlace` or `controlador` of existing pages unless explicitly asked.
|
||||
|
||||
See [docs/pages-and-records.md](docs/pages-and-records.md) for full details.
|
||||
|
||||
### General Sections
|
||||
Database-backed templates (headers, footers, record views) that use the `thisrecord` variable to access record fields. They use the same Twig + Acai attribute engine as modules.
|
||||
|
||||
- Upload fields return arrays: `thisrecord.image[0].urlPath`
|
||||
- Foreign keys use `_num` suffix: `category_num`
|
||||
|
||||
See [docs/modular-system.md](docs/modular-system.md) for details.
|
||||
|
||||
### Hooks (`hooks/`)
|
||||
PHP files that execute server-side logic. Triggered by:
|
||||
- Twig filter: `'hooks/module_id/' | hook({param: value})`
|
||||
- HTML tag: `<hook result="var" endpoint="/hooks/module_id/" :param="value"></hook>`
|
||||
- JavaScript: `CmsApi.hook('/hooks/module_id/', {param: value}, callback)`
|
||||
- Form action: via `c-form` attribute
|
||||
|
||||
There are two valid hook locations:
|
||||
- Global hooks in `hooks/hooks.<hook-id>.php` for reusable/shared server-side logic
|
||||
- Module-specific hooks in `template/estandar/modulos/<module-id>/hook.php` for logic owned by a single module
|
||||
|
||||
How to reference them:
|
||||
- Global hook `hooks/hooks.calcular_precio.php` -> endpoint `/hooks/calcular_precio/`
|
||||
- Module hook `template/estandar/modulos/hero_banner/hook.php` -> endpoint `/hooks/hero_banner/`
|
||||
- Module hook `template/estandar/modulos/buscadorapartados_hjd8s/hook.php` -> endpoint `/hooks/buscadorapartados_hjd8s/`
|
||||
|
||||
Rule of thumb:
|
||||
- If the logic is only used by one module, prefer that module's `hook.php`
|
||||
- If the logic will be reused by several modules/pages, create a global hook in `hooks/`
|
||||
- Return arrays from hooks; do not use `echo json_encode(...)` or `exit`
|
||||
|
||||
See [docs/hooks-and-api.md](docs/hooks-and-api.md) for usage.
|
||||
|
||||
**Important:** Table names in CmsApi/Twig do NOT use the `cms_` prefix. The primary key is always `num`, never `id`.
|
||||
|
||||
## Acai Core (web-base)
|
||||
|
||||
The project workspace contains only the **customization layer** (modules, hooks, schemas, uploads). The CMS core (routing, rendering engine, admin panel, APIs) lives in a separate directory called **web-base** that is mounted as a Docker volume.
|
||||
|
||||
The web-base path can be obtained via: `GET http://localhost:9090/api/web-base-path`
|
||||
|
||||
Do NOT modify web-base files — they are shared across all projects.
|
||||
|
||||
## Critical Rules
|
||||
|
||||
1. **Before working with any area (hooks, modules, templates, CSS/JS, etc.), read the corresponding documentation in `docs/` first.** Do not guess or assume — always consult the docs before taking action.
|
||||
2. **NEVER use `mkdir` to create directories.** Instead, use `acai-write` to create the first file inside the directory — this creates parent directories automatically. For example, to create a new module, directly write the `index-base.tpl` file.
|
||||
3. Only edit `index-base.tpl` in modules — `index.tpl`, `index-twig.tpl`, and `builder.json` are auto-generated
|
||||
4. Editing or creating any `index-base.tpl` through `acai-write` or `acai-line-replace` triggers automatic compilation. `compile_module` is only for manual recovery when you need to force a recompile without changing the file.
|
||||
5. `script.js` and `style.css` are static files — do NOT use Twig syntax inside them. Pass dynamic values from `index-base.tpl` via `data-*` attributes.
|
||||
6. Use Twig **filters** (with `|`), never Twig functions
|
||||
7. Table names without `cms_` prefix everywhere
|
||||
8. Primary key is `num`, never `id`
|
||||
9. Upload fields are arrays — access with `[0].urlPath`
|
||||
10. Tailwind CSS as primary styling, custom CSS scoped with BEM when needed
|
||||
11. Twig concatenation uses `~` operator: `'value=' ~ variable`
|
||||
12. `enlace` (link) fields already include slashes — **NEVER modify an existing enlace** unless explicitly asked
|
||||
13. **NEVER modify the `controlador` field** of existing records — it defines whether a page is Builder or Standard
|
||||
14. All CmsApi/Twig variables and field names should be extracted from the schemas in `cms/data/schema/<nombre_de_tabla>.ini.php` before use. Do not guess variable names or field types.
|
||||
15. NEVER make up a field or table name. Always check the schema files in `cms/data/schema/` to confirm field names and types before using them.
|
||||
|
||||
## MCP Tools
|
||||
|
||||
This project has MCP tools for managing modules, records, media, and more. **Before starting any task, consult the tools reference for the correct workflow.**
|
||||
|
||||
See [docs/mcp-tools-reference.md](docs/mcp-tools-reference.md) for the complete list of available tools and step-by-step workflows.
|
||||
|
||||
Key workflows:
|
||||
- **Create module**: Read [docs/module-creation-guide.md](docs/module-creation-guide.md) first → create files with `acai-write` / refine with `acai-line-replace` → automatic compile on `index-base.tpl` → `add_module_to_record` (returns sectionId) → `set_module_config_vars` (returns uploadFields) → images via uploadFields. Use `compile_module` only if you need a manual recompile without editing the file.
|
||||
- **Edit module**: `acai-view` → `acai-line-replace` (or `acai-write` for full rewrites) → automatic compile on `index-base.tpl`
|
||||
- **Add images**: use `uploadFields` from `set_module_config_vars` response → `upload_record_image`
|
||||
- **Generate images**: `generate_image` → `upload_record_image` with returned URL
|
||||
|
||||
## Documentation
|
||||
|
||||
- [docs/modular-system.md](docs/modular-system.md) — Modules, general sections, global variables
|
||||
- [docs/builder-fields.md](docs/builder-fields.md) — Builder field types, Acai attributes, c-form, components
|
||||
- [docs/twig-filters.md](docs/twig-filters.md) — Twig filters reference (get, hook, module, queryDB, etc.)
|
||||
- [docs/hooks-and-api.md](docs/hooks-and-api.md) — PHP hooks, CmsApi, CocoDB, record creation
|
||||
- [docs/css-js-conventions.md](docs/css-js-conventions.md) — CSS/JS/Vue 3, Tailwind, BEM, native components
|
||||
- [docs/quick-reference.md](docs/quick-reference.md) — Cheat sheet: domain rules, field types, filters
|
||||
- [docs/production-patterns.md](docs/production-patterns.md) — Real production patterns (header, zigzag, FAQ, forms)
|
||||
- [docs/vue-builder-rules.md](docs/vue-builder-rules.md) — CMS-VUE rules (tabs, colorpicker, components)
|
||||
- [docs/vue-builder-examples.md](docs/vue-builder-examples.md) — Vue builder examples (Banner Slideshow, etc.)
|
||||
- [docs/pages-and-records.md](docs/pages-and-records.md) — Page types (Builder vs Standard), sections, visibility, critical rules
|
||||
- [docs/module-creation-guide.md](docs/module-creation-guide.md) — Module creation workflow, style reference, field types
|
||||
- [docs/mcp-tools-reference.md](docs/mcp-tools-reference.md) — MCP tools reference, available tools, workflows
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def create_coder_profile() -> AgentProfile:
|
||||
return AgentProfile(
|
||||
role=AgentRole.CODER,
|
||||
name="coder",
|
||||
system_prompt=CODER_SYSTEM_PROMPT,
|
||||
allowed_tools=[], # All tools allowed
|
||||
temperature=0.2,
|
||||
max_tokens=4096,
|
||||
context_sections=[
|
||||
"immutable_rules",
|
||||
"project_profile",
|
||||
"knowledge_base",
|
||||
"task_state",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class CoderAgent(BaseAgent):
|
||||
"""Executes implementation steps."""
|
||||
pass
|
||||
@@ -1,46 +0,0 @@
|
||||
"""Collector agent — gathers context and information."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ...models.agent import AgentProfile, AgentRole
|
||||
from .base import BaseAgent
|
||||
|
||||
COLLECTOR_SYSTEM_PROMPT = """Eres un Agente Recolector de Contexto. Tu rol es recopilar información necesaria para una tarea.
|
||||
|
||||
## Instrucciones
|
||||
- Lee archivos, busca en el código, explora documentación.
|
||||
- Produce un resumen estructurado de lo que encontraste.
|
||||
- Extrae hechos clave, restricciones y dependencias.
|
||||
- NO modifiques nada — solo observa y reporta.
|
||||
- Responde SIEMPRE en español.
|
||||
|
||||
## Formato de salida
|
||||
Produce un resumen estructurado:
|
||||
1. Archivos relevantes y sus propósitos
|
||||
2. Patrones o convenciones encontrados
|
||||
3. Dependencias o restricciones
|
||||
4. Recomendaciones para el paso de implementación
|
||||
"""
|
||||
|
||||
|
||||
def create_collector_profile() -> AgentProfile:
|
||||
return AgentProfile(
|
||||
role=AgentRole.COLLECTOR,
|
||||
name="collector",
|
||||
system_prompt=COLLECTOR_SYSTEM_PROMPT,
|
||||
allowed_tools=[], # All tools allowed (read-only preferred)
|
||||
temperature=0.1,
|
||||
max_tokens=2048,
|
||||
context_sections=[
|
||||
"immutable_rules",
|
||||
"project_profile",
|
||||
"knowledge_base",
|
||||
"task_state",
|
||||
"working_context",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class CollectorAgent(BaseAgent):
|
||||
"""Gathers context and information for tasks."""
|
||||
pass
|
||||
@@ -1,129 +0,0 @@
|
||||
"""Planner agent — decomposes objectives into executable plans."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ...models.agent import AgentProfile, AgentRole
|
||||
from ...models.session import SessionState, TaskStep, TaskStatus
|
||||
from .base import BaseAgent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PLANNER_SYSTEM_PROMPT = """Eres un Agente Planificador de Acai CMS. Tu rol es analizar el mensaje del usuario y decidir cómo responder.
|
||||
|
||||
## Instrucciones
|
||||
- PRIMERO revisa el historial de conversación para entender el contexto.
|
||||
- Si el mensaje es conversacional (saludos, preguntas sobre la conversación, datos personales mencionados antes, preguntas generales que NO requieren consultar la base de datos ni herramientas) → devuelve una respuesta directa usando la información del historial.
|
||||
- SOLO genera un plan de ejecución cuando el usuario pide explícitamente una acción sobre su web: crear módulos, editar contenido, explorar páginas, consultar tablas de la base de datos, etc.
|
||||
- "¿Cómo me llamo?" es conversacional (responde del historial), NO es una consulta a la base de datos.
|
||||
- Responde SIEMPRE en español.
|
||||
|
||||
## Formato de salida
|
||||
|
||||
### Para respuestas directas (saludos, preguntas simples):
|
||||
{
|
||||
"direct_response": "Tu respuesta aquí. Sé amable y conciso."
|
||||
}
|
||||
|
||||
### Para tareas que requieren herramientas:
|
||||
{
|
||||
"plan": [
|
||||
{"description": "descripción del paso", "agent_role": "coder|collector|reviewer"},
|
||||
...
|
||||
],
|
||||
"constraints": ["restricciones o notas importantes"],
|
||||
"facts": ["hechos establecidos del análisis"]
|
||||
}
|
||||
|
||||
## REGLAS CRÍTICAS para planes:
|
||||
- Máximo 2-3 steps. Un coder competente puede hacer múltiples acciones en un solo step.
|
||||
- Para CREAR UN MÓDULO: 1 step coder (crea archivos + añade a página + configura vars). NO necesita steps separados para cada acción.
|
||||
- Para EXPLORAR: 1 step coder (consulta tablas + lista módulos).
|
||||
- Para EDITAR CONTENIDO: 1 step coder (lee + modifica).
|
||||
- SOLO añade un step reviewer si la tarea es compleja (crear módulo con hook, editar múltiples páginas).
|
||||
- NUNCA generes steps redundantes (ej: "crear módulo" + "añadir a página" + "configurar vars" son UN SOLO step).
|
||||
|
||||
Devuelve SOLO el objeto JSON, sin comentarios fuera."""
|
||||
|
||||
|
||||
def create_planner_profile() -> AgentProfile:
|
||||
return AgentProfile(
|
||||
role=AgentRole.PLANNER,
|
||||
name="planner",
|
||||
system_prompt=PLANNER_SYSTEM_PROMPT,
|
||||
allowed_tools=[], # Planner doesn't use tools
|
||||
temperature=0.2,
|
||||
max_tokens=2048,
|
||||
context_sections=[
|
||||
"immutable_rules",
|
||||
"project_profile",
|
||||
"knowledge_base",
|
||||
"task_state",
|
||||
"artifact_memory",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class PlannerAgent(BaseAgent):
|
||||
"""Generates execution plans from objectives."""
|
||||
|
||||
async def plan(self, session: SessionState) -> tuple[list[TaskStep] | str, dict[str, int]]:
|
||||
"""Generate a plan or a direct response.
|
||||
|
||||
Returns:
|
||||
(steps, usage) if plan needed
|
||||
(direct_response_string, usage) if no plan needed
|
||||
"""
|
||||
result = await self.execute(session, max_steps=1)
|
||||
usage = result.get("usage", {"input_tokens": 0, "output_tokens": 0})
|
||||
content = result["content"].strip()
|
||||
|
||||
# Parse the JSON from the model output
|
||||
try:
|
||||
json_str = content
|
||||
if "```" in content:
|
||||
start = content.find("{")
|
||||
end = content.rfind("}") + 1
|
||||
if start >= 0 and end > start:
|
||||
json_str = content[start:end]
|
||||
|
||||
parsed = json.loads(json_str)
|
||||
|
||||
# Check for direct response (no plan needed)
|
||||
if "direct_response" in parsed:
|
||||
return parsed["direct_response"], usage
|
||||
|
||||
# Build plan steps
|
||||
steps: list[TaskStep] = []
|
||||
for item in parsed.get("plan", []):
|
||||
steps.append(
|
||||
TaskStep(
|
||||
description=item.get("description", ""),
|
||||
agent_role=item.get("agent_role", "coder"),
|
||||
status=TaskStatus.PENDING,
|
||||
)
|
||||
)
|
||||
|
||||
if session.current_task:
|
||||
session.current_task.constraints.extend(
|
||||
parsed.get("constraints", [])
|
||||
)
|
||||
session.current_task.facts_extracted.extend(
|
||||
parsed.get("facts", [])
|
||||
)
|
||||
|
||||
return steps, usage
|
||||
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
logger.warning("Failed to parse planner output: %s", e)
|
||||
return [
|
||||
TaskStep(
|
||||
description=session.current_task.objective
|
||||
if session.current_task
|
||||
else "Execute task",
|
||||
agent_role="coder",
|
||||
)
|
||||
], usage
|
||||
@@ -1,47 +0,0 @@
|
||||
"""Reviewer agent — validates outputs and provides feedback."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ...models.agent import AgentProfile, AgentRole
|
||||
from .base import BaseAgent
|
||||
|
||||
REVIEWER_SYSTEM_PROMPT = """Eres un Agente Revisor. Tu rol es validar el trabajo realizado por otros agentes.
|
||||
|
||||
## Instrucciones
|
||||
- Revisa los artefactos producidos en esta sesión.
|
||||
- Verifica corrección, completitud y calidad.
|
||||
- Identifica problemas, bugs o piezas faltantes.
|
||||
- Proporciona retroalimentación accionable.
|
||||
- Responde SIEMPRE en español.
|
||||
|
||||
## Formato de salida
|
||||
Produce una revisión estructurada:
|
||||
1. **Estado**: APROBADO | NECESITA_CAMBIOS | RECHAZADO
|
||||
2. **Problemas**: Lista de problemas encontrados
|
||||
3. **Sugerencias**: Mejoras a considerar
|
||||
4. **Hechos**: Nuevos hechos establecidos durante la revisión
|
||||
"""
|
||||
|
||||
|
||||
def create_reviewer_profile() -> AgentProfile:
|
||||
return AgentProfile(
|
||||
role=AgentRole.REVIEWER,
|
||||
name="reviewer",
|
||||
system_prompt=REVIEWER_SYSTEM_PROMPT,
|
||||
allowed_tools=[], # All tools allowed
|
||||
temperature=0.1,
|
||||
max_tokens=2048,
|
||||
context_sections=[
|
||||
"immutable_rules",
|
||||
"project_profile",
|
||||
"knowledge_base",
|
||||
"task_state",
|
||||
"artifact_memory",
|
||||
"working_context",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class ReviewerAgent(BaseAgent):
|
||||
"""Reviews and validates work products."""
|
||||
pass
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Orchestrator Engine — single-agent execution.
|
||||
|
||||
Flow: message → coder agent (with tools) → response
|
||||
No planner, no reviewer. The coder decides what to do.
|
||||
Flow: message → selected agent (with tools) → response
|
||||
The agent is determined by the session's agent_id via AgentRegistry.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -15,16 +15,16 @@ from ..config import settings
|
||||
from ..context.engine import ContextEngine
|
||||
from ..mcp.manager import MCPManager
|
||||
from ..memory.store import MemoryStore
|
||||
from ..models.agent import AgentRole
|
||||
from ..models.agent import AgentProfile
|
||||
from ..models.session import SessionState, SessionStatus, TaskStatus
|
||||
from ..streaming.sse import SSEEmitter, EventType
|
||||
from .agents.coder import CoderAgent, create_coder_profile
|
||||
from .agents.base import BaseAgent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OrchestratorEngine:
|
||||
"""Drives execution for a session message. Single agent, no planning."""
|
||||
"""Drives execution for a session message with the selected agent."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -33,13 +33,14 @@ class OrchestratorEngine:
|
||||
mcp_client: MCPManager,
|
||||
memory_store: MemoryStore,
|
||||
sse_emitter: SSEEmitter,
|
||||
agent_profile: AgentProfile,
|
||||
) -> None:
|
||||
self.model = model_adapter
|
||||
self.context = context_engine
|
||||
self.mcp = mcp_client
|
||||
self.memory = memory_store
|
||||
self.sse = sse_emitter
|
||||
self._coder_profile = create_coder_profile()
|
||||
self.agent_profile = agent_profile
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public
|
||||
@@ -84,11 +85,15 @@ class OrchestratorEngine:
|
||||
session: SessionState,
|
||||
message: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Execute: message → coder → response."""
|
||||
"""Execute: message → agent → response."""
|
||||
|
||||
await self.sse.emit(
|
||||
EventType.EXECUTION_STARTED,
|
||||
{"session_id": session.session_id, "message": message[:200]},
|
||||
{
|
||||
"session_id": session.session_id,
|
||||
"agent_id": session.agent_id,
|
||||
"message": message[:200],
|
||||
},
|
||||
session_id=session.session_id,
|
||||
)
|
||||
|
||||
@@ -96,9 +101,9 @@ class OrchestratorEngine:
|
||||
task = session.begin_task(objective=message)
|
||||
task.status = TaskStatus.EXECUTING
|
||||
|
||||
# Execute with the coder agent directly
|
||||
agent = CoderAgent(
|
||||
profile=self._coder_profile,
|
||||
# Execute with the selected agent
|
||||
agent = BaseAgent(
|
||||
profile=self.agent_profile,
|
||||
model_adapter=self.model,
|
||||
context_engine=self.context,
|
||||
mcp_client=self.mcp,
|
||||
@@ -130,6 +135,7 @@ class OrchestratorEngine:
|
||||
session.task_history.append({
|
||||
"task_id": task.task_id,
|
||||
"objective": message,
|
||||
"agent_id": session.agent_id,
|
||||
"status": "completed",
|
||||
"steps": 1,
|
||||
"facts": task.facts_extracted[-10:],
|
||||
@@ -167,6 +173,7 @@ class OrchestratorEngine:
|
||||
{
|
||||
"session_id": session.session_id,
|
||||
"task_id": task.task_id,
|
||||
"agent_id": session.agent_id,
|
||||
"steps_completed": 1,
|
||||
"steps_failed": [],
|
||||
"status": "completed",
|
||||
@@ -177,8 +184,9 @@ class OrchestratorEngine:
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Task %s completed (%d tools, %d artifacts, %d input tokens)",
|
||||
"Task %s completed (agent=%s, %d tools, %d artifacts, %d input tokens)",
|
||||
task.task_id,
|
||||
session.agent_id,
|
||||
len(result.get("tool_executions", [])),
|
||||
len(result.get("artifacts", [])),
|
||||
total_input,
|
||||
@@ -187,6 +195,7 @@ class OrchestratorEngine:
|
||||
return {
|
||||
"session_id": session.session_id,
|
||||
"task_id": task.task_id,
|
||||
"agent_id": session.agent_id,
|
||||
"content": content or "Task completed.",
|
||||
"steps_completed": 1,
|
||||
"steps_failed": [],
|
||||
|
||||
138
src/orchestrator/registry.py
Normal file
138
src/orchestrator/registry.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""Agent Registry — descubrimiento dinámico de agentes desde carpetas."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from ..models.agent import AgentProfile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgentRegistry:
|
||||
"""Descubre y carga agentes desde subcarpetas de agents_dir.
|
||||
|
||||
Cada subcarpeta debe contener:
|
||||
- agent.yaml — metadata y configuración del agente
|
||||
- system.md — system prompt en markdown
|
||||
"""
|
||||
|
||||
def __init__(self, agents_dir: Path) -> None:
|
||||
self._agents: dict[str, AgentProfile] = {}
|
||||
self._metadata: dict[str, dict[str, Any]] = {}
|
||||
self._agents_dir = agents_dir
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Carga
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def load(self) -> None:
|
||||
"""Escanea agents_dir y carga todos los agentes encontrados."""
|
||||
self._agents.clear()
|
||||
self._metadata.clear()
|
||||
|
||||
if not self._agents_dir.is_dir():
|
||||
logger.warning("Agents directory not found: %s", self._agents_dir)
|
||||
return
|
||||
|
||||
for agent_dir in sorted(self._agents_dir.iterdir()):
|
||||
if not agent_dir.is_dir():
|
||||
continue
|
||||
|
||||
yaml_path = agent_dir / "agent.yaml"
|
||||
prompt_path = agent_dir / "system.md"
|
||||
|
||||
if not yaml_path.exists():
|
||||
logger.warning("Skipping %s — agent.yaml not found", agent_dir.name)
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(yaml_path, encoding="utf-8") as f:
|
||||
meta = yaml.safe_load(f) or {}
|
||||
|
||||
system_prompt = ""
|
||||
if prompt_path.exists():
|
||||
system_prompt = prompt_path.read_text(encoding="utf-8")
|
||||
|
||||
agent_id = meta.get("name", agent_dir.name)
|
||||
|
||||
profile = AgentProfile(
|
||||
role=agent_id,
|
||||
name=agent_id,
|
||||
display_name=meta.get("display_name", agent_id),
|
||||
description=meta.get("description", ""),
|
||||
icon=meta.get("icon", "bot"),
|
||||
category=meta.get("category", "general"),
|
||||
system_prompt=system_prompt,
|
||||
allowed_tools=meta.get("allowed_tools", []),
|
||||
model_id=meta.get("model_id"),
|
||||
temperature=meta.get("temperature"),
|
||||
max_tokens=meta.get("max_tokens"),
|
||||
context_sections=meta.get("context_sections", [
|
||||
"immutable_rules",
|
||||
"project_profile",
|
||||
"knowledge_base",
|
||||
"task_state",
|
||||
]),
|
||||
stream_deltas=meta.get("stream_deltas", True),
|
||||
)
|
||||
|
||||
self._agents[agent_id] = profile
|
||||
self._metadata[agent_id] = meta
|
||||
logger.info("Loaded agent: %s (%s)", agent_id, meta.get("display_name", agent_id))
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to load agent from %s: %s", agent_dir.name, e)
|
||||
|
||||
logger.info("Agent registry loaded: %d agents", len(self._agents))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Consultas
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def get(self, agent_id: str) -> AgentProfile | None:
|
||||
"""Devuelve el AgentProfile para un agent_id."""
|
||||
return self._agents.get(agent_id)
|
||||
|
||||
def get_metadata(self, agent_id: str) -> dict[str, Any] | None:
|
||||
"""Devuelve la metadata completa (yaml) de un agente."""
|
||||
return self._metadata.get(agent_id)
|
||||
|
||||
def get_system_prompt(self, agent_id: str) -> str | None:
|
||||
"""Devuelve el system prompt de un agente."""
|
||||
profile = self._agents.get(agent_id)
|
||||
return profile.system_prompt if profile else None
|
||||
|
||||
def list_agents(self) -> list[dict[str, Any]]:
|
||||
"""Lista todos los agentes con metadata (sin system prompt)."""
|
||||
result = []
|
||||
for agent_id, profile in self._agents.items():
|
||||
result.append({
|
||||
"id": agent_id,
|
||||
"display_name": profile.display_name,
|
||||
"description": profile.description,
|
||||
"icon": profile.icon,
|
||||
"category": profile.category,
|
||||
"temperature": profile.temperature,
|
||||
"max_tokens": profile.max_tokens,
|
||||
"model_id": profile.model_id,
|
||||
})
|
||||
return result
|
||||
|
||||
@property
|
||||
def default_agent_id(self) -> str:
|
||||
return "acai"
|
||||
|
||||
@property
|
||||
def agent_ids(self) -> list[str]:
|
||||
return list(self._agents.keys())
|
||||
|
||||
def __contains__(self, agent_id: str) -> bool:
|
||||
return agent_id in self._agents
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._agents)
|
||||
@@ -1,60 +0,0 @@
|
||||
"""Agent router — selects the right subagent for each step."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from ..models.agent import AgentRole
|
||||
from ..models.session import TaskStep
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Keyword-based routing hints
|
||||
_ROLE_KEYWORDS: dict[AgentRole, list[str]] = {
|
||||
AgentRole.COLLECTOR: [
|
||||
"gather", "collect", "read", "explore", "search", "find",
|
||||
"discover", "analyze", "investigate", "research", "scan",
|
||||
"understand", "review existing",
|
||||
],
|
||||
AgentRole.CODER: [
|
||||
"implement", "write", "create", "build", "code", "fix",
|
||||
"modify", "refactor", "add", "update", "generate", "develop",
|
||||
"edit", "change", "configure", "set up",
|
||||
],
|
||||
AgentRole.REVIEWER: [
|
||||
"review", "validate", "check", "verify", "test", "audit",
|
||||
"inspect", "evaluate", "assess", "confirm",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def route_step(step: TaskStep) -> AgentRole:
|
||||
"""Determine which agent role should handle this step.
|
||||
|
||||
Uses the step's declared agent_role if valid, otherwise falls back
|
||||
to keyword-based routing.
|
||||
"""
|
||||
# Respect explicit assignment
|
||||
declared = step.agent_role.lower()
|
||||
try:
|
||||
role = AgentRole(declared)
|
||||
return role
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Keyword-based fallback
|
||||
desc_lower = step.description.lower()
|
||||
scores: dict[AgentRole, int] = {role: 0 for role in _ROLE_KEYWORDS}
|
||||
|
||||
for role, keywords in _ROLE_KEYWORDS.items():
|
||||
for kw in keywords:
|
||||
if kw in desc_lower:
|
||||
scores[role] += 1
|
||||
|
||||
best = max(scores, key=lambda r: scores[r])
|
||||
if scores[best] > 0:
|
||||
logger.info("Routed step '%s' to %s (score=%d)", step.description[:60], best, scores[best])
|
||||
return best
|
||||
|
||||
# Default to coder
|
||||
return AgentRole.CODER
|
||||
Reference in New Issue
Block a user