Selector de agentes
This commit is contained in:
@@ -38,9 +38,10 @@ COPY agenticSystem/mcp-server/ ./mcp-server/
|
||||
# Copiar codigo fuente Python
|
||||
COPY agenticSystem/src/ ./src/
|
||||
|
||||
# Copiar configuracion MCP y documentacion
|
||||
# Copiar configuracion MCP, documentacion y agentes
|
||||
COPY agenticSystem/mcp.json ./mcp.json
|
||||
COPY agenticSystem/docs/ ./docs/
|
||||
COPY agenticSystem/agents/ ./agents/
|
||||
|
||||
# Crear directorio para mount point de webs
|
||||
RUN mkdir -p /opt/acai/webs
|
||||
|
||||
15
agents/acai/agent.yaml
Normal file
15
agents/acai/agent.yaml
Normal 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
|
||||
@@ -1,11 +1,4 @@
|
||||
"""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.
|
||||
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
|
||||
|
||||
@@ -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/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
|
||||
15
agents/accessibility/agent.yaml
Normal file
15
agents/accessibility/agent.yaml
Normal 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
|
||||
74
agents/accessibility/system.md
Normal file
74
agents/accessibility/system.md
Normal 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.
|
||||
15
agents/analytics/agent.yaml
Normal file
15
agents/analytics/agent.yaml
Normal 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
|
||||
79
agents/analytics/system.md
Normal file
79
agents/analytics/system.md
Normal 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.
|
||||
15
agents/code-reviewer/agent.yaml
Normal file
15
agents/code-reviewer/agent.yaml
Normal 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
|
||||
74
agents/code-reviewer/system.md
Normal file
74
agents/code-reviewer/system.md
Normal 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
15
agents/content/agent.yaml
Normal 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
53
agents/content/system.md
Normal 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
15
agents/qa/agent.yaml
Normal 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
59
agents/qa/system.md
Normal 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
15
agents/seo/agent.yaml
Normal 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
54
agents/seo/system.md
Normal 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.
|
||||
@@ -8,3 +8,4 @@ openai>=1.60.0,<2.0.0
|
||||
httpx>=0.28.0,<1.0.0
|
||||
sse-starlette>=2.2.0,<3.0.0
|
||||
tiktoken>=0.7.0,<1.0.0
|
||||
pyyaml>=6.0,<7.0
|
||||
|
||||
@@ -33,6 +33,7 @@ class CreateSessionRequest(BaseModel):
|
||||
default_factory=dict,
|
||||
description="Per-project env vars for MCP servers (e.g. ACAI_WEB_URL, ACAI_PROJECT_DIR)",
|
||||
)
|
||||
agent_id: str = "acai"
|
||||
|
||||
|
||||
class CreateSessionResponse(BaseModel):
|
||||
@@ -43,6 +44,7 @@ class CreateSessionResponse(BaseModel):
|
||||
class SendMessageRequest(BaseModel):
|
||||
message: str
|
||||
stream: bool = False
|
||||
agent_id: str | None = None
|
||||
|
||||
|
||||
class SessionResponse(BaseModel):
|
||||
@@ -53,6 +55,7 @@ class SessionResponse(BaseModel):
|
||||
completed_tasks: list[str] = Field(default_factory=list)
|
||||
created_at: str
|
||||
updated_at: str
|
||||
agent_id: str = "acai"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -70,6 +73,7 @@ def set_dependencies(
|
||||
sse_emitter: Any,
|
||||
claude_emitter: Any = None,
|
||||
mcp_registry: Any = None,
|
||||
agent_registry: Any = None,
|
||||
) -> None:
|
||||
_deps["storage"] = storage
|
||||
_deps["model_adapter"] = model_adapter
|
||||
@@ -78,6 +82,7 @@ def set_dependencies(
|
||||
_deps["sse"] = sse_emitter
|
||||
_deps["claude_sse"] = claude_emitter
|
||||
_deps["mcp_registry"] = mcp_registry
|
||||
_deps["agent_registry"] = agent_registry
|
||||
|
||||
|
||||
def _get_storage():
|
||||
@@ -92,7 +97,11 @@ def _get_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."""
|
||||
return OrchestratorEngine(
|
||||
model_adapter=_deps["model_adapter"],
|
||||
@@ -100,6 +109,7 @@ def _build_orchestrator(mcp_manager) -> OrchestratorEngine:
|
||||
mcp_client=mcp_manager,
|
||||
memory_store=_deps["memory_store"],
|
||||
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)
|
||||
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()
|
||||
session = SessionState(
|
||||
project_profile=body.project_profile,
|
||||
immutable_rules=body.immutable_rules,
|
||||
metadata=body.metadata,
|
||||
)
|
||||
session.agent_id = body.agent_id
|
||||
# Store mcp_env in session metadata for reconnection
|
||||
if 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_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
|
||||
orchestrator = _build_orchestrator(mcp_manager or MCPManager())
|
||||
orchestrator = _build_orchestrator(mcp_manager or MCPManager(), agent_profile)
|
||||
|
||||
if body.stream:
|
||||
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,
|
||||
created_at=session.created_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
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -141,7 +141,7 @@ class ContextEngine:
|
||||
conv_len = len(conversation) if conversation else 0
|
||||
debug_entry = {
|
||||
"timestamp": time.time(),
|
||||
"agent": agent.role.value,
|
||||
"agent": agent.role,
|
||||
"agent_name": agent.name,
|
||||
"total_tokens": total_tokens,
|
||||
"sections": section_summary,
|
||||
@@ -161,7 +161,7 @@ class ContextEngine:
|
||||
logger.info(
|
||||
"Context built for [%s/%s] — %d sections, ~%d tokens, artifacts=%d, conversation=%d msgs",
|
||||
session.session_id[:8],
|
||||
agent.role.value,
|
||||
agent.role,
|
||||
len(sections),
|
||||
total_tokens,
|
||||
len(artifacts) if artifacts else 0,
|
||||
|
||||
16
src/main.py
16
src/main.py
@@ -26,6 +26,7 @@ from .context.engine import ContextEngine
|
||||
from .mcp.registry import MCPRegistry
|
||||
from .memory.store import MemoryStore
|
||||
from .orchestrator.engine import OrchestratorEngine
|
||||
from .orchestrator.registry import AgentRegistry
|
||||
from .storage.redis import RedisStorage
|
||||
from .streaming.claude_format import ClaudeFormatEmitter, DualEmitter
|
||||
from .streaming.sse import SSEEmitter
|
||||
@@ -63,10 +64,16 @@ async def lifespan(app: FastAPI):
|
||||
# 3. Initialize memory store
|
||||
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)
|
||||
|
||||
# 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:
|
||||
config_path = pathlib.Path(settings.mcp_config_path)
|
||||
if not config_path.is_absolute():
|
||||
@@ -83,7 +90,7 @@ async def lifespan(app: FastAPI):
|
||||
})
|
||||
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)
|
||||
set_dependencies(
|
||||
storage=redis_storage,
|
||||
@@ -93,9 +100,10 @@ async def lifespan(app: FastAPI):
|
||||
sse_emitter=dual_emitter,
|
||||
claude_emitter=claude_emitter,
|
||||
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
|
||||
try:
|
||||
kb_result = await _load_knowledge_from_dir("docs")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from .session import SessionState, TaskState
|
||||
from .context import ContextPackage, MemoryDocument, ContextSection
|
||||
from .agent import AgentProfile, SubAgentDefinition, AgentRole
|
||||
from .agent import AgentProfile, SubAgentDefinition
|
||||
from .artifacts import ArtifactSummary
|
||||
from .tools import ToolExecution, ToolDefinition
|
||||
|
||||
@@ -12,7 +12,6 @@ __all__ = [
|
||||
"ContextSection",
|
||||
"AgentProfile",
|
||||
"SubAgentDefinition",
|
||||
"AgentRole",
|
||||
"ArtifactSummary",
|
||||
"ToolExecution",
|
||||
"ToolDefinition",
|
||||
|
||||
@@ -2,26 +2,21 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class AgentRole(StrEnum):
|
||||
ORCHESTRATOR = "orchestrator"
|
||||
PLANNER = "planner"
|
||||
CODER = "coder"
|
||||
COLLECTOR = "collector"
|
||||
REVIEWER = "reviewer"
|
||||
|
||||
|
||||
class AgentProfile(BaseModel):
|
||||
"""Describes the identity and capabilities of an agent."""
|
||||
|
||||
role: AgentRole
|
||||
role: str # Agent id libre (ej: "acai", "seo", "content")
|
||||
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)
|
||||
model_id: str | None = None
|
||||
temperature: float | None = None
|
||||
@@ -32,9 +27,9 @@ class AgentProfile(BaseModel):
|
||||
"project_profile",
|
||||
"knowledge_base",
|
||||
"task_state",
|
||||
"working_context",
|
||||
]
|
||||
)
|
||||
stream_deltas: bool = True # Si emite deltas por SSE al usuario
|
||||
|
||||
|
||||
class SubAgentDefinition(BaseModel):
|
||||
|
||||
@@ -82,6 +82,7 @@ class SessionState(BaseModel):
|
||||
|
||||
session_id: str = Field(default_factory=lambda: uuid.uuid4().hex)
|
||||
status: SessionStatus = SessionStatus.IDLE
|
||||
agent_id: str = "acai" # Agente seleccionado para esta sesión
|
||||
project_profile: dict[str, Any] = Field(default_factory=dict)
|
||||
immutable_rules: list[str] = Field(default_factory=list)
|
||||
current_task: TaskState | None = None
|
||||
|
||||
@@ -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,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