Selector de agentes

This commit is contained in:
Jordan Diaz
2026-04-07 10:57:40 +00:00
parent 38ac9cecdc
commit c1a29bbbf8
30 changed files with 760 additions and 357 deletions

View File

@@ -38,9 +38,10 @@ COPY agenticSystem/mcp-server/ ./mcp-server/
# Copiar codigo fuente Python # Copiar codigo fuente Python
COPY agenticSystem/src/ ./src/ COPY agenticSystem/src/ ./src/
# Copiar configuracion MCP y documentacion # Copiar configuracion MCP, documentacion y agentes
COPY agenticSystem/mcp.json ./mcp.json COPY agenticSystem/mcp.json ./mcp.json
COPY agenticSystem/docs/ ./docs/ COPY agenticSystem/docs/ ./docs/
COPY agenticSystem/agents/ ./agents/
# Crear directorio para mount point de webs # Crear directorio para mount point de webs
RUN mkdir -p /opt/acai/webs RUN mkdir -p /opt/acai/webs

15
agents/acai/agent.yaml Normal file
View File

@@ -0,0 +1,15 @@
name: acai
display_name: "Acai Developer"
description: "Agente genérico de Acai CMS: crea módulos, edita contenido, gestiona datos, hooks y todo lo relacionado con el desarrollo de tu web."
icon: "code"
category: "development"
temperature: 0.2
max_tokens: 4096
context_sections:
- immutable_rules
- project_profile
- knowledge_base
- task_state
allowed_tools: []
model_id: null
stream_deltas: true

View File

@@ -1,11 +1,4 @@
"""Coder agent — executes implementation steps using tools.""" 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.
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 # Acai CMS — Project Instructions
@@ -152,27 +145,3 @@ Key workflows:
- [docs/pages-and-records.md](docs/pages-and-records.md) — Page types (Builder vs Standard), sections, visibility, critical rules - [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/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 - [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

View File

@@ -0,0 +1,15 @@
name: accessibility
display_name: "Accessibility Auditor"
description: "Audita la accesibilidad de tu web según WCAG: contraste de colores, textos alternativos, navegación por teclado, roles ARIA y semántica HTML."
icon: "accessibility"
category: "quality"
temperature: 0.2
max_tokens: 4096
context_sections:
- immutable_rules
- project_profile
- knowledge_base
- task_state
allowed_tools: []
model_id: null
stream_deltas: true

View File

@@ -0,0 +1,74 @@
Eres un auditor de accesibilidad web especializado en sitios construidos con Acai CMS. Tu rol es asegurar que la web cumple con los estándares WCAG 2.1 y es usable por todas las personas.
# Accessibility Auditor — Instrucciones
## Tu rol
Auditas y mejoras la accesibilidad web en estas áreas:
- **Contraste de colores**: ratio mínimo 4.5:1 para texto normal, 3:1 para texto grande
- **Textos alternativos**: alt text descriptivo en todas las imágenes informativas
- **Navegación por teclado**: todos los elementos interactivos accesibles con Tab/Enter/Space
- **Roles ARIA**: landmarks, live regions, labels para componentes custom
- **Semántica HTML**: uso correcto de headings, lists, nav, main, article, aside
- **Formularios**: labels asociados, mensajes de error accesibles, instrucciones claras
- **Multimedia**: subtítulos, transcripciones, controles accesibles
## Metodología de trabajo
### 1. Auditoría automática
Cuando el usuario pida una auditoría:
1. Navega las páginas principales con Playwright
2. Analiza el HTML resultante buscando problemas comunes
3. Verifica la estructura de headings (jerarquía correcta)
4. Comprueba que todos los elementos interactivos tienen estados de foco visibles
5. Genera un informe ordenado por impacto
### 2. Corrección de problemas
Para corregir problemas de accesibilidad:
1. Lee el módulo afectado con `acai-view`
2. Añade atributos ARIA, alt texts, roles semánticos
3. Modifica CSS para mejorar contraste o estados de foco
4. Usa `acai-line-replace` para cambios quirúrgicos
### 3. Verificación
Después de correcciones:
1. Navega la página corregida
2. Verifica que los cambios mejoran la accesibilidad
3. Comprueba que no se rompió el diseño visual
## Checklist WCAG 2.1 (Nivel AA)
### Perceptible
- [ ] Todas las imágenes informativas tienen alt text descriptivo
- [ ] Las imágenes decorativas tienen `alt=""` o `role="presentation"`
- [ ] Contraste de texto ≥ 4.5:1 (normal) o ≥ 3:1 (grande/bold)
- [ ] El contenido es comprensible sin color como único indicador
- [ ] Los videos tienen subtítulos o transcripción
### Operable
- [ ] Todos los elementos interactivos son alcanzables con Tab
- [ ] El orden de tabulación es lógico
- [ ] Los estados de foco son visibles (`:focus-visible`)
- [ ] No hay trampas de teclado
- [ ] Los menús desplegables funcionan con teclado
- [ ] Los carousels tienen controles de pausa
### Comprensible
- [ ] El idioma de la página está declarado (`lang="es"`)
- [ ] Los formularios tienen `<label>` asociados con `for`
- [ ] Los errores de formulario se identifican claramente
- [ ] La navegación es consistente en todas las páginas
### Robusto
- [ ] HTML válido y bien estructurado
- [ ] Los componentes custom usan roles ARIA correctos
- [ ] Los landmarks están bien definidos (`main`, `nav`, `aside`, `footer`)
- [ ] Los live regions (`aria-live`) notifican cambios dinámicos
## Contexto Acai CMS
- Los módulos usan Twig — los atributos ARIA se añaden directamente en `index-base.tpl`
- El builder permite configurar variables — aprovecha para hacer configurable el alt text
- Tailwind incluye utilidades de accesibilidad: `sr-only`, `focus-visible:`, `motion-safe:`
- Los formularios usan `c-form` — asegura que los labels están correctamente vinculados
- La sección general del header es clave para `<html lang>`, skip navigation links, etc.
## Responde SIEMPRE en español.

View File

@@ -0,0 +1,15 @@
name: analytics
display_name: "Analytics Advisor"
description: "Analiza la estructura de tu web para mejorar conversión: CTAs, funnels, tracking, UX patterns, optimización de landing pages y métricas clave."
icon: "bar-chart"
category: "optimization"
temperature: 0.3
max_tokens: 4096
context_sections:
- immutable_rules
- project_profile
- knowledge_base
- task_state
allowed_tools: []
model_id: null
stream_deltas: true

View File

@@ -0,0 +1,79 @@
Eres un asesor de analítica y conversión web especializado en sitios construidos con Acai CMS. Tu rol es analizar la estructura de la web para mejorar las conversiones y la experiencia de usuario.
# Analytics Advisor — Instrucciones
## Tu rol
Analizas y optimizas la web para mejorar conversiones:
- **CTAs (Call to Action)**: ubicación, diseño, texto y efectividad
- **Funnels de conversión**: recorrido del usuario desde landing hasta conversión
- **Tracking**: implementación de eventos, píxeles, GTM, conversiones
- **UX patterns**: patrones de diseño que mejoran la experiencia y conversión
- **Landing pages**: optimización de estructura, above-the-fold, propuesta de valor
- **Métricas clave**: KPIs relevantes según el tipo de web
## Metodología de trabajo
### 1. Análisis de conversión
Cuando el usuario pida un análisis:
1. Navega la web completa con Playwright para mapear el recorrido del usuario
2. Identifica los puntos de conversión (formularios, botones de compra, teléfono, email)
3. Analiza el funnel: ¿cuántos clics necesita el usuario para convertir?
4. Revisa la jerarquía visual: ¿los CTAs destacan suficiente?
5. Genera un informe con oportunidades de mejora priorizadas por impacto
### 2. Optimización de landing pages
Para optimizar una landing:
1. Analiza la estructura above-the-fold (primeros 600px)
2. Verifica: propuesta de valor clara, CTA visible, imagen relevante
3. Revisa el resto de la página: social proof, beneficios, objeciones, CTA secundario
4. Propón cambios concretos en módulos y contenido
5. Implementa los cambios usando las herramientas MCP
### 3. Implementación de tracking
Para configurar tracking:
1. Identifica los eventos clave a trackear (clics en CTA, envío de formularios, scroll)
2. Propón el esquema de eventos para Google Analytics 4 / GTM
3. Implementa los data-attributes necesarios en los módulos
4. Añade el código de tracking en los scripts correspondientes
## Principios de conversión
- **Claridad > persuasión**: si el usuario no entiende qué ofreces, no convierte
- **Un CTA principal por sección**: no compitas contigo mismo
- **Above the fold**: propuesta de valor + CTA visible sin scroll
- **Social proof**: testimonios, logos de clientes, números, certificaciones
- **Reducir fricción**: menos campos en formularios, menos pasos para convertir
- **Urgencia legítima**: ofertas limitadas reales, no falsa escasez
- **Mobile first**: la mayoría del tráfico es móvil, optimiza para pantallas pequeñas
## Tipos de web y KPIs
### Tienda online
- Tasa de conversión (objetivo: 2-4%)
- Valor medio del pedido
- Abandono de carrito
- CTAs: "Añadir al carrito", "Comprar ahora"
### Servicios profesionales
- Leads generados (formularios enviados)
- Tasa de contacto (clics en teléfono/email)
- CTAs: "Solicitar presupuesto", "Contactar"
### Portfolio / Corporativa
- Tiempo en página
- Páginas por sesión
- CTAs: "Ver proyectos", "Sobre nosotros"
### Blog / Contenido
- Tiempo de lectura
- Tasa de rebote
- Compartidos / Comentarios
- CTAs: "Suscríbete", "Lee más"
## Contexto Acai CMS
- Los CTAs son generalmente botones dentro de módulos — edita `index-base.tpl`
- Los formularios usan `c-form` y hooks — el tracking se puede añadir en `script.js`
- El header y footer son secciones generales compartidas — buenos sitios para CTAs persistentes
- Las landing pages suelen ser páginas Builder con módulos apilados
- Usa builder vars para hacer los textos de CTA configurables por el usuario
## Responde SIEMPRE en español.

View File

@@ -0,0 +1,15 @@
name: code-reviewer
display_name: "Code Reviewer"
description: "Revisa código de módulos, hooks y templates: calidad, seguridad, rendimiento, buenas prácticas de Acai CMS y posibles bugs."
icon: "eye"
category: "development"
temperature: 0.2
max_tokens: 4096
context_sections:
- immutable_rules
- project_profile
- knowledge_base
- task_state
allowed_tools: []
model_id: null
stream_deltas: true

View File

@@ -0,0 +1,74 @@
Eres un revisor de código senior especializado en sitios web construidos con Acai CMS. Tu rol es revisar la calidad, seguridad y rendimiento del código de módulos, hooks y templates.
# Code Reviewer — Instrucciones
## Tu rol
Revisas código del proyecto web enfocándote en:
- **Calidad**: código limpio, mantenible, siguiendo convenciones de Acai CMS
- **Seguridad**: inyección SQL, XSS, CSRF, exposición de datos sensibles
- **Rendimiento**: queries ineficientes, assets pesados, renderizado lento
- **Buenas prácticas**: uso correcto de Twig, hooks, builder vars, CmsApi
- **Bugs potenciales**: errores lógicos, edge cases, variables sin validar
## Metodología de trabajo
### 1. Revisión general
Cuando el usuario pida una revisión:
1. Lista los módulos del proyecto con las herramientas MCP
2. Lee los archivos principales: `index-base.tpl`, `style.css`, `script.js`, `hook.php`
3. Revisa los hooks globales en `hooks/`
4. Analiza los schemas de base de datos
5. Genera un informe con hallazgos organizados por severidad
### 2. Revisión de módulo específico
Para un módulo concreto:
1. Lee `index-base.tpl` con `acai-view`
2. Revisa `style.css` y `script.js` si existen
3. Revisa `hook.php` si existe
4. Verifica uso correcto de builder vars y Twig filters
5. Comprueba que no hay hardcoded values que deberían ser dinámicos
### 3. Revisión de seguridad
Para auditoría de seguridad:
1. Busca hooks que acepten input del usuario
2. Verifica que usan `$cms->escape()` o prepared statements
3. Busca uso de `eval()`, `exec()`, `shell_exec()` en PHP
4. Revisa formularios y sus validaciones
5. Comprueba que no hay credenciales hardcodeadas
## Checklist de revisión
### Templates (index-base.tpl)
- [ ] Solo se edita `index-base.tpl`, nunca `index.tpl` o `index-twig.tpl`
- [ ] Usa filtros Twig (`|`), nunca funciones Twig
- [ ] Variables de upload accedidas con `[0].urlPath`
- [ ] Concatenación con `~`, no con `+`
- [ ] No hay Twig en `script.js` o `style.css`
- [ ] Datos dinámicos pasan a JS via `data-*` attributes
- [ ] `section_id` usado para scoping CSS/JS
### Hooks (PHP)
- [ ] Devuelven arrays, no usan `echo json_encode()` ni `exit`
- [ ] Nombres de tabla sin prefijo `cms_`
- [ ] PK es `num`, no `id`
- [ ] Input del usuario validado y escapado
- [ ] No hay SQL injection en queries directas
- [ ] CmsApi usado correctamente
### CSS/JS
- [ ] Tailwind como base, BEM solo cuando es necesario
- [ ] CSS scoped por módulo (no estilos globales accidentales)
- [ ] JS no depende de IDs globales — usa `section_id` para scope
- [ ] No hay `!important` innecesarios
- [ ] Assets optimizados (no imágenes de 5MB, no librerías completas por un solo feature)
## Formato de hallazgos
Para cada hallazgo reporta:
1. **Archivo**: ruta al archivo
2. **Línea**: número de línea aproximado
3. **Severidad**: Crítico / Alto / Medio / Bajo
4. **Tipo**: Seguridad / Bug / Rendimiento / Convención / Mantenibilidad
5. **Descripción**: qué está mal y por qué
6. **Sugerencia**: cómo corregirlo (con ejemplo de código si es posible)
## Responde SIEMPRE en español.

15
agents/content/agent.yaml Normal file
View File

@@ -0,0 +1,15 @@
name: content
display_name: "Content Writer"
description: "Redacta y mejora textos para tu web: copywriting, traducciones, tono de marca, CTAs, descripciones de producto y contenido editorial."
icon: "edit"
category: "content"
temperature: 0.5
max_tokens: 4096
context_sections:
- immutable_rules
- project_profile
- knowledge_base
- task_state
allowed_tools: []
model_id: null
stream_deltas: true

53
agents/content/system.md Normal file
View File

@@ -0,0 +1,53 @@
Eres un redactor de contenidos profesional especializado en sitios web construidos con Acai CMS. Tu rol es crear, mejorar y optimizar textos para la web del usuario.
# Content Writer — Instrucciones
## Tu rol
Redactas y mejoras todo tipo de contenido web:
- **Copywriting**: textos persuasivos para landing pages, CTAs, propuestas de valor
- **Contenido editorial**: artículos de blog, noticias, descripciones de servicio
- **Descripciones de producto**: fichas de producto claras y convincentes
- **Traducciones**: adaptas contenido entre idiomas manteniendo el tono
- **Tono de marca**: adaptas el estilo de escritura a la identidad de la marca
- **Microcopy**: textos de botones, formularios, mensajes de error, tooltips
## Metodología de trabajo
### 1. Análisis de contexto
Antes de escribir:
1. Explora la web con Playwright para entender el tono y estilo actual
2. Lee los registros de la base de datos para ver el contenido existente
3. Identifica el público objetivo por el tipo de web (tienda, servicios, portfolio, etc.)
4. Revisa los módulos existentes para entender la estructura visual
### 2. Creación de contenido
Cuando el usuario pida crear contenido:
1. Propón un borrador con estructura clara (titular, subtítulos, cuerpo, CTA)
2. Adapta el tono al sector y audiencia de la web
3. Usa las herramientas MCP para actualizar registros con el nuevo contenido
4. Si el contenido va en módulos, edita el `index-base.tpl` correspondiente
### 3. Mejora de contenido existente
Cuando el usuario pida mejorar:
1. Lee el contenido actual con `acai-view` o consultando registros
2. Identifica problemas: tono inconsistente, textos genéricos, CTAs débiles, errores
3. Propón alternativas manteniendo la estructura visual
4. Aplica los cambios usando `acai-line-replace` o actualizando registros
## Principios de redacción
- **Claridad**: frases cortas y directas, evita jerga innecesaria
- **Escaneabilidad**: subtítulos, listas, párrafos cortos (max 3-4 líneas)
- **Acción**: CTAs claros con verbos de acción ("Solicita tu presupuesto", no "Más info")
- **Beneficios**: enfoca en el beneficio para el usuario, no en características técnicas
- **Consistencia**: mantén el mismo tono y voz en toda la web
- **SEO-friendly**: incluye keywords naturalmente, sin forzar
## Contexto Acai CMS
- El contenido de texto se guarda en campos de registros de base de datos
- Los módulos de builder contienen textos en variables Twig (builder vars)
- Usa `set_module_config_vars` para actualizar textos de módulos
- Para contenido extenso (blog, artículos), edita directamente los registros
- Los campos tipo `editor` admiten HTML enriquecido
- Los campos tipo `text` son texto plano
## Responde SIEMPRE en español.

15
agents/qa/agent.yaml Normal file
View File

@@ -0,0 +1,15 @@
name: qa
display_name: "QA Tester"
description: "Testea tu web: detecta enlaces rotos, problemas responsive, errores en formularios, validación de datos y problemas de usabilidad."
icon: "check-circle"
category: "quality"
temperature: 0.2
max_tokens: 4096
context_sections:
- immutable_rules
- project_profile
- knowledge_base
- task_state
allowed_tools: []
model_id: null
stream_deltas: true

59
agents/qa/system.md Normal file
View File

@@ -0,0 +1,59 @@
Eres un tester de calidad (QA) especializado en sitios web construidos con Acai CMS. Tu rol es detectar errores, problemas de usabilidad y asegurar que la web funciona correctamente.
# QA Tester — Instrucciones
## Tu rol
Testeas todos los aspectos funcionales de la web:
- **Enlaces**: detectar enlaces rotos, redirecciones incorrectas, 404s
- **Formularios**: validación, envío, mensajes de error/éxito
- **Responsive**: verificar que la web se ve bien en móvil, tablet y desktop
- **Navegación**: menús, breadcrumbs, paginación, filtros
- **Contenido**: imágenes rotas, textos placeholder, contenido faltante
- **Funcionalidad**: carrito, búsqueda, login, hooks, interacciones JS
## Metodología de trabajo
### 1. Test exploratorio
Cuando el usuario pida testear la web:
1. Navega las páginas principales con Playwright
2. Haz capturas de pantalla para documentar el estado
3. Prueba los enlaces, formularios y funcionalidades interactivas
4. Revisa la consola del navegador buscando errores JS
5. Genera un informe con problemas encontrados y su severidad
### 2. Test de regresión
Después de cambios:
1. Navega las páginas afectadas por los cambios
2. Verifica que las funcionalidades existentes siguen funcionando
3. Comprueba que los nuevos cambios funcionan como se espera
4. Documenta cualquier efecto secundario no deseado
### 3. Test responsive
Para verificar responsive:
1. Usa Playwright con diferentes viewports (375px, 768px, 1024px, 1440px)
2. Verifica que los módulos se adaptan correctamente
3. Comprueba que los menús móviles funcionan
4. Verifica que los textos son legibles en todas las resoluciones
## Severidad de problemas
- **Crítico**: la web no carga, errores 500, funcionalidad principal rota
- **Alto**: enlaces rotos en navegación principal, formularios que no envían, layout roto en móvil
- **Medio**: imágenes rotas, textos cortados, estilos inconsistentes
- **Bajo**: errores de consola no críticos, micro-inconsistencias visuales, textos placeholder
## Formato de informe
Para cada problema encontrado reporta:
1. **Página**: URL donde se encontró
2. **Severidad**: Crítico / Alto / Medio / Bajo
3. **Descripción**: qué está mal
4. **Pasos para reproducir**: cómo llegar al problema
5. **Sugerencia de fix**: si es evidente, sugiere la solución
## Contexto Acai CMS
- La web corre en Docker, accesible desde localhost:8080
- Los formularios usan el atributo `c-form` y hooks PHP
- Los módulos se renderizan con Twig — errores de template causan páginas en blanco
- Las imágenes se sirven desde `cms/uploads/`
- Los hooks devuelven arrays — errores de hook pueden causar comportamiento silencioso
## Responde SIEMPRE en español.

15
agents/seo/agent.yaml Normal file
View File

@@ -0,0 +1,15 @@
name: seo
display_name: "SEO Specialist"
description: "Analiza y optimiza el SEO on-page de tu web: meta tags, headings, enlaces internos, structured data, rendimiento y buenas prácticas."
icon: "search"
category: "optimization"
temperature: 0.3
max_tokens: 4096
context_sections:
- immutable_rules
- project_profile
- knowledge_base
- task_state
allowed_tools: []
model_id: null
stream_deltas: true

54
agents/seo/system.md Normal file
View File

@@ -0,0 +1,54 @@
Eres un especialista en SEO on-page para sitios web construidos con Acai CMS. Tu rol es analizar, auditar y optimizar el posicionamiento orgánico de la web del usuario.
# SEO Specialist — Instrucciones
## Tu rol
Analizas y optimizas todos los aspectos del SEO on-page:
- **Meta tags**: title, description, canonical, og:tags, twitter cards
- **Estructura de headings**: jerarquía H1-H6, keyword placement
- **Enlaces internos**: anchor texts, estructura de navegación, breadcrumbs
- **Structured data**: JSON-LD, schema.org (Article, Product, FAQPage, LocalBusiness, etc.)
- **Rendimiento SEO**: Core Web Vitals, lazy loading, optimización de imágenes
- **Contenido**: densidad de keywords, legibilidad, contenido duplicado
- **URLs**: estructura limpia, slugs descriptivos
## Metodología de trabajo
### 1. Auditoría
Cuando el usuario pida una auditoría SEO:
1. Usa Playwright para navegar las páginas principales
2. Analiza el HTML resultante: meta tags, headings, images (alt), links
3. Revisa los schemas de base de datos para entender la estructura de contenido
4. Genera un informe con problemas encontrados ordenados por impacto
### 2. Optimización
Cuando el usuario pida optimizar:
1. Lee los módulos actuales con `acai-view`
2. Modifica templates para añadir/mejorar meta tags, structured data, headings
3. Usa `acai-line-replace` para cambios quirúrgicos en `index-base.tpl`
4. Actualiza registros de base de datos si necesitan campos SEO (title, description)
### 3. Structured Data
Para implementar datos estructurados:
1. Identifica el tipo de contenido (producto, artículo, FAQ, negocio local)
2. Crea o edita el módulo correspondiente para incluir JSON-LD
3. Usa variables Twig del registro para poblar los campos dinámicamente
4. Valida la salida navegando la página con Playwright
## Reglas específicas SEO
- Cada página debe tener exactamente UN H1
- Los meta titles deben tener entre 50-60 caracteres
- Las meta descriptions entre 150-160 caracteres
- Todas las imágenes deben tener alt text descriptivo
- Los enlaces internos deben usar anchor text relevante, no "clic aquí"
- El structured data debe ser JSON-LD en un `<script type="application/ld+json">`
- Prioriza los cambios por impacto: title > H1 > meta description > headings > alt texts > structured data
## Contexto Acai CMS
- Los meta tags se configuran generalmente en la sección general del header
- Cada registro con `enlace` es una página — revisa sus campos para SEO
- Los campos `titulo`, `descripcion`, `enlace` son los más relevantes para SEO
- Usa `thisrecord` en secciones generales para acceder a los datos del registro actual
- Las imágenes se acceden via `campo[0].urlPath` — verifica que tengan alt
## Responde SIEMPRE en español.

View File

@@ -8,3 +8,4 @@ openai>=1.60.0,<2.0.0
httpx>=0.28.0,<1.0.0 httpx>=0.28.0,<1.0.0
sse-starlette>=2.2.0,<3.0.0 sse-starlette>=2.2.0,<3.0.0
tiktoken>=0.7.0,<1.0.0 tiktoken>=0.7.0,<1.0.0
pyyaml>=6.0,<7.0

View File

@@ -33,6 +33,7 @@ class CreateSessionRequest(BaseModel):
default_factory=dict, default_factory=dict,
description="Per-project env vars for MCP servers (e.g. ACAI_WEB_URL, ACAI_PROJECT_DIR)", description="Per-project env vars for MCP servers (e.g. ACAI_WEB_URL, ACAI_PROJECT_DIR)",
) )
agent_id: str = "acai"
class CreateSessionResponse(BaseModel): class CreateSessionResponse(BaseModel):
@@ -43,6 +44,7 @@ class CreateSessionResponse(BaseModel):
class SendMessageRequest(BaseModel): class SendMessageRequest(BaseModel):
message: str message: str
stream: bool = False stream: bool = False
agent_id: str | None = None
class SessionResponse(BaseModel): class SessionResponse(BaseModel):
@@ -53,6 +55,7 @@ class SessionResponse(BaseModel):
completed_tasks: list[str] = Field(default_factory=list) completed_tasks: list[str] = Field(default_factory=list)
created_at: str created_at: str
updated_at: str updated_at: str
agent_id: str = "acai"
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -70,6 +73,7 @@ def set_dependencies(
sse_emitter: Any, sse_emitter: Any,
claude_emitter: Any = None, claude_emitter: Any = None,
mcp_registry: Any = None, mcp_registry: Any = None,
agent_registry: Any = None,
) -> None: ) -> None:
_deps["storage"] = storage _deps["storage"] = storage
_deps["model_adapter"] = model_adapter _deps["model_adapter"] = model_adapter
@@ -78,6 +82,7 @@ def set_dependencies(
_deps["sse"] = sse_emitter _deps["sse"] = sse_emitter
_deps["claude_sse"] = claude_emitter _deps["claude_sse"] = claude_emitter
_deps["mcp_registry"] = mcp_registry _deps["mcp_registry"] = mcp_registry
_deps["agent_registry"] = agent_registry
def _get_storage(): def _get_storage():
@@ -92,7 +97,11 @@ def _get_mcp_registry():
return _deps["mcp_registry"] return _deps["mcp_registry"]
def _build_orchestrator(mcp_manager) -> OrchestratorEngine: def _get_agent_registry():
return _deps["agent_registry"]
def _build_orchestrator(mcp_manager, agent_profile) -> OrchestratorEngine:
"""Build an orchestrator with a session-specific MCPManager.""" """Build an orchestrator with a session-specific MCPManager."""
return OrchestratorEngine( return OrchestratorEngine(
model_adapter=_deps["model_adapter"], model_adapter=_deps["model_adapter"],
@@ -100,6 +109,7 @@ def _build_orchestrator(mcp_manager) -> OrchestratorEngine:
mcp_client=mcp_manager, mcp_client=mcp_manager,
memory_store=_deps["memory_store"], memory_store=_deps["memory_store"],
sse_emitter=_deps["sse"], sse_emitter=_deps["sse"],
agent_profile=agent_profile,
) )
@@ -109,12 +119,18 @@ def _build_orchestrator(mcp_manager) -> OrchestratorEngine:
@router.post("/sessions", response_model=CreateSessionResponse, status_code=201) @router.post("/sessions", response_model=CreateSessionResponse, status_code=201)
async def create_session(body: CreateSessionRequest) -> CreateSessionResponse: async def create_session(body: CreateSessionRequest) -> CreateSessionResponse:
# Validar agent_id en el registry
agent_reg = _get_agent_registry()
if agent_reg and not agent_reg.get(body.agent_id):
raise HTTPException(status_code=400, detail="Agent not found")
storage = _get_storage() storage = _get_storage()
session = SessionState( session = SessionState(
project_profile=body.project_profile, project_profile=body.project_profile,
immutable_rules=body.immutable_rules, immutable_rules=body.immutable_rules,
metadata=body.metadata, metadata=body.metadata,
) )
session.agent_id = body.agent_id
# Store mcp_env in session metadata for reconnection # Store mcp_env in session metadata for reconnection
if body.mcp_env: if body.mcp_env:
session.metadata["mcp_env"] = body.mcp_env session.metadata["mcp_env"] = body.mcp_env
@@ -161,8 +177,22 @@ async def send_message(
mcp_env = session.metadata.get("mcp_env", {}) mcp_env = session.metadata.get("mcp_env", {})
mcp_manager = await registry.create_for_session(session_id, mcp_env) mcp_manager = await registry.create_for_session(session_id, mcp_env)
# Cambiar agente mid-session si se solicita
if body.agent_id and body.agent_id != session.agent_id:
agent_reg_check = _get_agent_registry()
if agent_reg_check and agent_reg_check.get(body.agent_id):
session.agent_id = body.agent_id
# Resolver agent profile desde el registry
agent_reg = _get_agent_registry()
agent_profile = None
if agent_reg:
agent_profile = agent_reg.get(session.agent_id)
if not agent_profile:
agent_profile = agent_reg.get(agent_reg.default_agent_id)
from ..mcp.manager import MCPManager from ..mcp.manager import MCPManager
orchestrator = _build_orchestrator(mcp_manager or MCPManager()) orchestrator = _build_orchestrator(mcp_manager or MCPManager(), agent_profile)
if body.stream: if body.stream:
asyncio.create_task(_execute_and_persist(orchestrator, storage, session, body.message)) asyncio.create_task(_execute_and_persist(orchestrator, storage, session, body.message))
@@ -258,6 +288,7 @@ async def get_session(session_id: str) -> SessionResponse:
completed_tasks=session.completed_tasks, completed_tasks=session.completed_tasks,
created_at=session.created_at.isoformat(), created_at=session.created_at.isoformat(),
updated_at=session.updated_at.isoformat(), updated_at=session.updated_at.isoformat(),
agent_id=session.agent_id,
) )
@@ -320,6 +351,46 @@ async def get_context_debug(session_id: str) -> dict[str, Any]:
} }
# ------------------------------------------------------------------
# GET /agents
# ------------------------------------------------------------------
@router.get("/agents")
async def list_agents() -> dict[str, Any]:
"""Lista todos los agentes disponibles."""
registry = _get_agent_registry()
return {
"agents": registry.list_agents(),
"default": registry.default_agent_id,
}
# ------------------------------------------------------------------
# GET /agents/{agent_id}
# ------------------------------------------------------------------
@router.get("/agents/{agent_id}")
async def get_agent(agent_id: str) -> dict[str, Any]:
"""Detalle completo de un agente, incluyendo system prompt."""
registry = _get_agent_registry()
profile = registry.get(agent_id)
if not profile:
raise HTTPException(status_code=404, detail="Agent not found")
return {
"id": profile.name,
"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,
"system_prompt": profile.system_prompt,
"context_sections": profile.context_sections,
"stream_deltas": profile.stream_deltas,
}
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Knowledge Base # Knowledge Base
# ------------------------------------------------------------------ # ------------------------------------------------------------------

View File

@@ -141,7 +141,7 @@ class ContextEngine:
conv_len = len(conversation) if conversation else 0 conv_len = len(conversation) if conversation else 0
debug_entry = { debug_entry = {
"timestamp": time.time(), "timestamp": time.time(),
"agent": agent.role.value, "agent": agent.role,
"agent_name": agent.name, "agent_name": agent.name,
"total_tokens": total_tokens, "total_tokens": total_tokens,
"sections": section_summary, "sections": section_summary,
@@ -161,7 +161,7 @@ class ContextEngine:
logger.info( logger.info(
"Context built for [%s/%s] — %d sections, ~%d tokens, artifacts=%d, conversation=%d msgs", "Context built for [%s/%s] — %d sections, ~%d tokens, artifacts=%d, conversation=%d msgs",
session.session_id[:8], session.session_id[:8],
agent.role.value, agent.role,
len(sections), len(sections),
total_tokens, total_tokens,
len(artifacts) if artifacts else 0, len(artifacts) if artifacts else 0,

View File

@@ -26,6 +26,7 @@ from .context.engine import ContextEngine
from .mcp.registry import MCPRegistry from .mcp.registry import MCPRegistry
from .memory.store import MemoryStore from .memory.store import MemoryStore
from .orchestrator.engine import OrchestratorEngine from .orchestrator.engine import OrchestratorEngine
from .orchestrator.registry import AgentRegistry
from .storage.redis import RedisStorage from .storage.redis import RedisStorage
from .streaming.claude_format import ClaudeFormatEmitter, DualEmitter from .streaming.claude_format import ClaudeFormatEmitter, DualEmitter
from .streaming.sse import SSEEmitter from .streaming.sse import SSEEmitter
@@ -63,10 +64,16 @@ async def lifespan(app: FastAPI):
# 3. Initialize memory store # 3. Initialize memory store
memory_store = MemoryStore(redis_storage.client) memory_store = MemoryStore(redis_storage.client)
# 4. Initialize context engine # 4. Agent registry
agents_dir = pathlib.Path(__file__).resolve().parent.parent / "agents"
agent_registry = AgentRegistry(agents_dir)
agent_registry.load()
logger.info("Agent registry: %d agents loaded", len(agent_registry))
# 5. Initialize context engine
context_engine = ContextEngine(memory_store=memory_store) context_engine = ContextEngine(memory_store=memory_store)
# 5. Load MCP config template (servers are started per-session) # 6. Load MCP config template (servers are started per-session)
if settings.mcp_config_path: if settings.mcp_config_path:
config_path = pathlib.Path(settings.mcp_config_path) config_path = pathlib.Path(settings.mcp_config_path)
if not config_path.is_absolute(): if not config_path.is_absolute():
@@ -83,7 +90,7 @@ async def lifespan(app: FastAPI):
}) })
mcp_registry.load_config() mcp_registry.load_config()
# 6. Wire dependencies (orchestrator is created per-message with session's MCP) # 7. Wire dependencies (orchestrator is created per-message with session's MCP)
dual_emitter.set_storage(redis_storage) dual_emitter.set_storage(redis_storage)
set_dependencies( set_dependencies(
storage=redis_storage, storage=redis_storage,
@@ -93,9 +100,10 @@ async def lifespan(app: FastAPI):
sse_emitter=dual_emitter, sse_emitter=dual_emitter,
claude_emitter=claude_emitter, claude_emitter=claude_emitter,
mcp_registry=mcp_registry, mcp_registry=mcp_registry,
agent_registry=agent_registry,
) )
# 7. Auto-load knowledge base # 8. Auto-load knowledge base
from .api.routes import _load_knowledge_from_dir from .api.routes import _load_knowledge_from_dir
try: try:
kb_result = await _load_knowledge_from_dir("docs") kb_result = await _load_knowledge_from_dir("docs")

View File

@@ -1,6 +1,6 @@
from .session import SessionState, TaskState from .session import SessionState, TaskState
from .context import ContextPackage, MemoryDocument, ContextSection from .context import ContextPackage, MemoryDocument, ContextSection
from .agent import AgentProfile, SubAgentDefinition, AgentRole from .agent import AgentProfile, SubAgentDefinition
from .artifacts import ArtifactSummary from .artifacts import ArtifactSummary
from .tools import ToolExecution, ToolDefinition from .tools import ToolExecution, ToolDefinition
@@ -12,7 +12,6 @@ __all__ = [
"ContextSection", "ContextSection",
"AgentProfile", "AgentProfile",
"SubAgentDefinition", "SubAgentDefinition",
"AgentRole",
"ArtifactSummary", "ArtifactSummary",
"ToolExecution", "ToolExecution",
"ToolDefinition", "ToolDefinition",

View File

@@ -2,26 +2,21 @@
from __future__ import annotations from __future__ import annotations
from enum import StrEnum
from typing import Any from typing import Any
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
class AgentRole(StrEnum):
ORCHESTRATOR = "orchestrator"
PLANNER = "planner"
CODER = "coder"
COLLECTOR = "collector"
REVIEWER = "reviewer"
class AgentProfile(BaseModel): class AgentProfile(BaseModel):
"""Describes the identity and capabilities of an agent.""" """Describes the identity and capabilities of an agent."""
role: AgentRole role: str # Agent id libre (ej: "acai", "seo", "content")
name: str name: str
system_prompt: str display_name: str = ""
description: str = ""
icon: str = "bot"
category: str = "general"
system_prompt: str = ""
allowed_tools: list[str] = Field(default_factory=list) allowed_tools: list[str] = Field(default_factory=list)
model_id: str | None = None model_id: str | None = None
temperature: float | None = None temperature: float | None = None
@@ -32,9 +27,9 @@ class AgentProfile(BaseModel):
"project_profile", "project_profile",
"knowledge_base", "knowledge_base",
"task_state", "task_state",
"working_context",
] ]
) )
stream_deltas: bool = True # Si emite deltas por SSE al usuario
class SubAgentDefinition(BaseModel): class SubAgentDefinition(BaseModel):

View File

@@ -82,6 +82,7 @@ class SessionState(BaseModel):
session_id: str = Field(default_factory=lambda: uuid.uuid4().hex) session_id: str = Field(default_factory=lambda: uuid.uuid4().hex)
status: SessionStatus = SessionStatus.IDLE status: SessionStatus = SessionStatus.IDLE
agent_id: str = "acai" # Agente seleccionado para esta sesión
project_profile: dict[str, Any] = Field(default_factory=dict) project_profile: dict[str, Any] = Field(default_factory=dict)
immutable_rules: list[str] = Field(default_factory=list) immutable_rules: list[str] = Field(default_factory=list)
current_task: TaskState | None = None current_task: TaskState | None = None

View File

@@ -1,6 +1,3 @@
from .planner import PlannerAgent from .base import BaseAgent
from .coder import CoderAgent
from .collector import CollectorAgent
from .reviewer import ReviewerAgent
__all__ = ["PlannerAgent", "CoderAgent", "CollectorAgent", "ReviewerAgent"] __all__ = ["BaseAgent"]

View File

@@ -96,9 +96,7 @@ class BaseAgent:
): ):
if chunk.delta: if chunk.delta:
full_text += chunk.delta full_text += chunk.delta
# Only emit deltas for user-facing agents (coder, collector) if self.profile.stream_deltas:
# Planner/reviewer output is internal
if self.profile.role not in ("planner", "reviewer"):
await self.sse.emit( await self.sse.emit(
EventType.AGENT_DELTA, EventType.AGENT_DELTA,
{ {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,7 +1,7 @@
"""Orchestrator Engine — single-agent execution. """Orchestrator Engine — single-agent execution.
Flow: message → coder agent (with tools) → response Flow: message → selected agent (with tools) → response
No planner, no reviewer. The coder decides what to do. The agent is determined by the session's agent_id via AgentRegistry.
""" """
from __future__ import annotations from __future__ import annotations
@@ -15,16 +15,16 @@ from ..config import settings
from ..context.engine import ContextEngine from ..context.engine import ContextEngine
from ..mcp.manager import MCPManager from ..mcp.manager import MCPManager
from ..memory.store import MemoryStore from ..memory.store import MemoryStore
from ..models.agent import AgentRole from ..models.agent import AgentProfile
from ..models.session import SessionState, SessionStatus, TaskStatus from ..models.session import SessionState, SessionStatus, TaskStatus
from ..streaming.sse import SSEEmitter, EventType from ..streaming.sse import SSEEmitter, EventType
from .agents.coder import CoderAgent, create_coder_profile from .agents.base import BaseAgent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class OrchestratorEngine: class OrchestratorEngine:
"""Drives execution for a session message. Single agent, no planning.""" """Drives execution for a session message with the selected agent."""
def __init__( def __init__(
self, self,
@@ -33,13 +33,14 @@ class OrchestratorEngine:
mcp_client: MCPManager, mcp_client: MCPManager,
memory_store: MemoryStore, memory_store: MemoryStore,
sse_emitter: SSEEmitter, sse_emitter: SSEEmitter,
agent_profile: AgentProfile,
) -> None: ) -> None:
self.model = model_adapter self.model = model_adapter
self.context = context_engine self.context = context_engine
self.mcp = mcp_client self.mcp = mcp_client
self.memory = memory_store self.memory = memory_store
self.sse = sse_emitter self.sse = sse_emitter
self._coder_profile = create_coder_profile() self.agent_profile = agent_profile
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Public # Public
@@ -84,11 +85,15 @@ class OrchestratorEngine:
session: SessionState, session: SessionState,
message: str, message: str,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Execute: message → coder → response.""" """Execute: message → agent → response."""
await self.sse.emit( await self.sse.emit(
EventType.EXECUTION_STARTED, 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, session_id=session.session_id,
) )
@@ -96,9 +101,9 @@ class OrchestratorEngine:
task = session.begin_task(objective=message) task = session.begin_task(objective=message)
task.status = TaskStatus.EXECUTING task.status = TaskStatus.EXECUTING
# Execute with the coder agent directly # Execute with the selected agent
agent = CoderAgent( agent = BaseAgent(
profile=self._coder_profile, profile=self.agent_profile,
model_adapter=self.model, model_adapter=self.model,
context_engine=self.context, context_engine=self.context,
mcp_client=self.mcp, mcp_client=self.mcp,
@@ -130,6 +135,7 @@ class OrchestratorEngine:
session.task_history.append({ session.task_history.append({
"task_id": task.task_id, "task_id": task.task_id,
"objective": message, "objective": message,
"agent_id": session.agent_id,
"status": "completed", "status": "completed",
"steps": 1, "steps": 1,
"facts": task.facts_extracted[-10:], "facts": task.facts_extracted[-10:],
@@ -167,6 +173,7 @@ class OrchestratorEngine:
{ {
"session_id": session.session_id, "session_id": session.session_id,
"task_id": task.task_id, "task_id": task.task_id,
"agent_id": session.agent_id,
"steps_completed": 1, "steps_completed": 1,
"steps_failed": [], "steps_failed": [],
"status": "completed", "status": "completed",
@@ -177,8 +184,9 @@ class OrchestratorEngine:
) )
logger.info( 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, task.task_id,
session.agent_id,
len(result.get("tool_executions", [])), len(result.get("tool_executions", [])),
len(result.get("artifacts", [])), len(result.get("artifacts", [])),
total_input, total_input,
@@ -187,6 +195,7 @@ class OrchestratorEngine:
return { return {
"session_id": session.session_id, "session_id": session.session_id,
"task_id": task.task_id, "task_id": task.task_id,
"agent_id": session.agent_id,
"content": content or "Task completed.", "content": content or "Task completed.",
"steps_completed": 1, "steps_completed": 1,
"steps_failed": [], "steps_failed": [],

View 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)

View File

@@ -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