feat: v1 — boilerplate WxO + web
Boilerplate completo para construir soluciones agénticas sobre IBM watsonx Orchestrate (ADK 2.x) con una capa web encima. Destilado de cotemar-poc-n1 y dun-casos-prueba. Incluye: - 11 docs (best practices, architecture patterns, ADK cheatsheet, observability, tool authoring, deployment, RUNBOOK, DEPLOY_TO_NEW_WOX, known-issues con 16 errores reales y fix, eval strategy, INDEX) - 13 templates WxO (orchestrator/specialist/single-meta agents, 3 connections, KB + runbook, observable_tool decorator, coercion helpers, Python tools, OpenAPI spec, backend filter endpoint, webhook validator HMAC, MCP connection) - 6 scripts (deploy idempotente con fallback ADK, undeploy, reset, check-adk-version, new-specialist scaffold, eval-agents runner) - 4 eval pieces (linter de best practices, runner, smoke-test, direct backend probe) + scenario templates - 8 subagentes Claude (.claude/agents/) — wxo-architect, wxo-agent-author, wxo-tool-author, runbook-author, mock-builder, backend-tool-builder, eval-author, web-layer-builder - Skill bundle fit-wxo-bootstrap/ (SKILL.md + 3 templates) listo para copiar a ~/.claude/skills/ - Web layer default FastAPI+HTMX con vista timeline observable + endpoint receptor de trazas - Docker compose Coolify-ready (healthcheck wget -qO-, sin labels Traefik, network split internal:true) - CI Gitea workflow con lint + smoke Best practices enforced por evals/lint_wxo_yaml.py: - A1: máx 10 tools por agente - A3: orchestrator sin tools de remediación - A6: agente sin propósito (react sin tools ni KB) - T1: @observable_tool obligatorio, no @tool directo - T3: _compat shim inline en tools.py - D1: docker-compose sin wget --spider - I-005: OpenAPI con description per-operation - I-009: sin labels Traefik manuales Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
253
docs/wxo-best-practices.md
Normal file
253
docs/wxo-best-practices.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# WxO Best Practices
|
||||
|
||||
Destiladas de Cotemar (`cotemar-poc-n1`) y Dun (`dun-casos-prueba`). Cada regla
|
||||
fue paga con dolor — el linter en `evals/lint_wxo_yaml.py` enforcea las
|
||||
críticas, el resto es disciplina humana.
|
||||
|
||||
## 🏗 Arquitectura de agentes
|
||||
|
||||
### A1 — Máximo 10 tools por agente
|
||||
LLM se confunde con catálogos grandes. Más de 10 → degrada la precisión de
|
||||
selección de tool. Si necesitás más, partí en specialists. **(enforced)**
|
||||
|
||||
### A2 — Especialistas por dominio, nada de "todistas"
|
||||
Un agente que sabe AD + Ops + RRHH falla más que tres que saben cada uno lo
|
||||
suyo. Aunque sean específicos chicos, separá. **(enforced — orchestrator no puede tener tools de remediación)**
|
||||
|
||||
### A3 — El orchestrator nunca remedia
|
||||
Su único poder es clasificar, delegar y crear/leer tickets. Si necesitás
|
||||
`reset_password`, `restart_service`, `create_user` — eso vive en un
|
||||
specialist. **(enforced)**
|
||||
|
||||
### A4 — Specialists se importan ANTES que el orchestrator
|
||||
El orchestrator declara a los specialists como `collaborators`. Si no
|
||||
existen aún en el tenant, el import del orchestrator falla. El
|
||||
`deploy-wxo.sh` ya respeta el orden, no lo cambies.
|
||||
|
||||
### A5 — Una KB por agente (least privilege) cuando hay KB
|
||||
El especialista AD no debe poder leer el runbook de Ops. Si todos
|
||||
comparten una KB, el LLM cita el runbook equivocado y razona desde la
|
||||
fuente equivocada.
|
||||
|
||||
### A6 — KB es opcional
|
||||
Agentes API-driven o MCP-driven no necesitan KB. **No declares KB vacía
|
||||
solo "por completitud"** — eso confunde el linter. Si no la necesitás,
|
||||
omitila. **(enforced — agente sin KB ni tools falla)**
|
||||
|
||||
### A7 — Patrón "meta-tool" para flujos lineales
|
||||
**Crítico.** langgraph en WxO tiene `recursion_limit=30` hardcoded. Cada
|
||||
turno ReAct quema ~10 frames → más de 2 tool calls explota. Si tu flujo es
|
||||
**lineal y conocido** (caso A → B → C → D), colapsá los 4 tools en uno
|
||||
solo (`run_full_case`) que el backend orquesta internamente. El agente
|
||||
solo invoca esa meta-tool. La observabilidad de los substeps se preserva
|
||||
con `write_audit` (ver §O1).
|
||||
|
||||
## 🧠 Prompting
|
||||
|
||||
### P1 — Hardcodear el modelo, documentar el fallback
|
||||
`llm: groq/openai/gpt-oss-120b` es el preferred para tool-calling.
|
||||
Fallback: `meta-llama/llama-3-3-70b-instruct` PERO con la advertencia de
|
||||
que después del turno ~3 emite tool calls como texto plano (ver
|
||||
`known-issues.md`).
|
||||
|
||||
### P2 — REGLA #0 explícita en el prompt
|
||||
> "NUNCA escribas la tool call como texto. Usá el protocolo de tool_calls."
|
||||
|
||||
Esto **es necesario** porque el LLM (especialmente llama) tiene drift y
|
||||
empieza a printear tool calls. Codificar el anti-pattern en el prompt
|
||||
reduce la frecuencia.
|
||||
|
||||
### P3 — Instrucciones con escenarios concretos
|
||||
No "sos un agente útil que ayuda con tickets". Sí: ejemplos enumerados de
|
||||
input → razonamiento → output esperado. Mínimo 3 ejemplos por agente. Ver
|
||||
`cotemar-poc-n1/wxo/agents/mesa_n1_cotemar.agent.yaml` como referencia.
|
||||
|
||||
### P4 — Tabla de escalamiento explícita en el prompt
|
||||
Si el agente puede escalar, la decisión "qué razón → qué grupo → qué
|
||||
persona" tiene que estar tabulada en el prompt, no implícita. El LLM no
|
||||
inventa, sigue tablas.
|
||||
|
||||
### P5 — `style: react` por defecto
|
||||
Para agentes con tools. Si no hace tool calling, podés usar `style: chat`,
|
||||
pero es raro. **(enforced — si un agente declara tools pero no `react`, warning)**
|
||||
|
||||
## 🛠 Tools
|
||||
|
||||
### T1 — `@observable_tool` siempre, nunca `@tool` directo
|
||||
El decorator captura inputs/outputs/latency/side_effects y los emite
|
||||
como traza estructurada. Sin esto perdés visibilidad y las evals no pueden
|
||||
verificar comportamiento intermedio. **(enforced)**
|
||||
|
||||
### T2 — Coerción de tipos en todo schema Pydantic
|
||||
LLMs estringifican ints y arrays (mandan `"95727067"` cuando el schema
|
||||
pide `int`). Pydantic 422 → agent falla. Toda input model lleva:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, field_validator
|
||||
from wxo.tools.python._coercion_helpers import _coerce_int, _coerce_list
|
||||
|
||||
class MyInput(BaseModel):
|
||||
company_id: int
|
||||
groups: list[str]
|
||||
_coerce_company_id = field_validator("company_id", mode="before")(_coerce_int)
|
||||
_coerce_groups = field_validator("groups", mode="before")(_coerce_list)
|
||||
```
|
||||
|
||||
**(enforced)**
|
||||
|
||||
### T3 — `_compat.py` inline en cada archivo de tools
|
||||
La ADK importa cada `tools.py` standalone. Si tu archivo hace
|
||||
`from .._compat import ...`, el TRM falla con `ModuleNotFoundError`.
|
||||
El shim de compat se inlinea al inicio de cada archivo. El template ya
|
||||
lo trae. **(enforced)**
|
||||
|
||||
### T4 — Toda tool lee `BASE_URL` del environment de su connection
|
||||
Nunca hardcodear la URL del sistema externo. La connection inyecta
|
||||
`BASE_URL`. Cambio de tenant = cambio de credencial, no de código.
|
||||
|
||||
### T5 — Una connection = un dominio = un `app_id`
|
||||
3 conexiones (unus, dynatrace, ad) en Cotemar = 3 dominios. Mezclar
|
||||
varios dominios en una connection rompe el aislamiento de credenciales.
|
||||
|
||||
### T6 — Conexiones en BOTH `draft` Y `live`
|
||||
Si solo configurás `draft`, el `agents deploy --env live` falla con
|
||||
`kid not found`. El `deploy-wxo.sh` hace los dos pases siempre.
|
||||
|
||||
### T7 — Tipos de tool: elegir el correcto
|
||||
- **OpenAPI** (preferido si tenés backend): un solo spec, ADK importa todo, version-controlled
|
||||
- **Python `@tool`** (preferido para mocks): rápido para prototipar, fácil de testear
|
||||
- **MCP** (preferido si el sistema externo lo expone): trae types + permisos
|
||||
- Ver `tool-authoring-guide.md` para el árbol de decisión.
|
||||
|
||||
### T8 — OpenAPI: `description` per-operation OBLIGATORIO
|
||||
ADK falla con "No description provided" si solo hay `summary`. Si tu
|
||||
backend usa FastAPI sin docstrings, exponer el spec así:
|
||||
|
||||
```python
|
||||
op["description"] = op.get("summary") or "Endpoint XYZ"
|
||||
```
|
||||
|
||||
**(enforced — el linter lee el spec y verifica)**
|
||||
|
||||
### T9 — OpenAPI: `security` per-operation, NO global
|
||||
ADK no hereda el `security` global. Re-declaralo en cada operación.
|
||||
|
||||
### T10 — Filtrar el spec OpenAPI que exponés a WxO
|
||||
No mandes el openapi.json completo de tu backend. Filtralo a una
|
||||
allowlist `PUBLIC_TOOLS` y resolvé `$ref` transitivamente para que solo
|
||||
se expongan los schemas usados. Ver `wxo/tools/openapi/_backend_filter_endpoint.py`.
|
||||
|
||||
## 🔌 Conexiones
|
||||
|
||||
### C1 — `key_value` para mocks sin auth, `api_key` para producción
|
||||
Mocks demo: `auth_type: key_value` con un solo campo `BASE_URL`. Producción:
|
||||
`auth_type: api_key` con header **custom** (no `Authorization`).
|
||||
|
||||
### C2 — Header custom, no Bearer
|
||||
Usar `Authorization: Bearer …` choca con el IAM token que Orchestrate ya
|
||||
inyecta para llamar al tool. Usar `X-Orchestrate-Token` o similar.
|
||||
|
||||
### C3 — `set-credentials` en cada deploy
|
||||
Tu `deploy-wxo.sh` invoca `connections set-credentials` siempre, no solo
|
||||
al crear. Rotación de secretos = re-deploy del script.
|
||||
|
||||
### C4 — Fallback de subcomandos ADK
|
||||
Diferentes versiones de la ADK usan `set-identifier --url` vs
|
||||
`configure --url`. El script intenta los dos y warnea si ninguno existe.
|
||||
|
||||
## 📚 Knowledge Bases
|
||||
|
||||
### KB1 — Runbooks como `.txt` con secciones fijas
|
||||
- **Trigger** (cuándo aplica)
|
||||
- **Precondiciones** (qué chequeo antes)
|
||||
- **Pasos** (1, 2, 3 — verbo en imperativo)
|
||||
- **Éxito** (qué confirma que salió bien)
|
||||
- **Fallo** (qué hacer si falla cada paso)
|
||||
- **Escalamiento** (a quién/cómo si excede autoridad)
|
||||
|
||||
### KB2 — Embeddings: `ibm/slate-125m-english-rtrvr`
|
||||
Default sano para español + inglés mezclados. Si tu corpus es solo
|
||||
español, evaluá modelos multilingual de IBM.
|
||||
|
||||
## 🚢 Deploy / Coolify
|
||||
|
||||
### D1 — Healthcheck con `wget -qO-` o `curl -sf`
|
||||
`wget --spider` es buggy en busybox → marca container unhealthy → Traefik
|
||||
da 503. **(enforced en lint del docker-compose)**
|
||||
|
||||
### D2 — NO declarar labels Traefik manuales en docker-compose
|
||||
Coolify v4 los autogenera del panel FQDN. Si los declarás vos, sale
|
||||
"TRAEFIK DEFAULT CERT" en lugar de Let's Encrypt. Solo:
|
||||
```yaml
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=coolify"
|
||||
```
|
||||
|
||||
### D3 — Network split: `internal: true` para servicios sin internet
|
||||
Solo el web/landing va al network `coolify`. Backend, DB, workers → red
|
||||
interna. Mejor aislamiento + cero exposición accidental.
|
||||
|
||||
### D4 — `--force-recreate` después de cambiar `.env`
|
||||
`restart` no relee variables de entorno. Si cambiás `.env`, hacé
|
||||
`docker compose up -d --force-recreate`.
|
||||
|
||||
## 🔍 Observabilidad
|
||||
|
||||
### O1 — `write_audit("tool.<name>", payload)` desde el backend
|
||||
Cada handler escribe un audit log con el `case_id` o `correlation_id`. La
|
||||
UI reconstruye el timeline desde acá, no desde trazas de Orchestrate
|
||||
(que son volátiles y no queryable).
|
||||
|
||||
### O2 — Outputs persistidos para granular retry
|
||||
`ExecutionStep.output_payload` guarda `content_b64`, `zip_b64`,
|
||||
`package_url`. Si el caso falla en el paso 4, reanudás desde 3 sin
|
||||
regenerar 1-2.
|
||||
|
||||
### O3 — Webhook receiver con HMAC-SHA256
|
||||
Si recibís webhooks de WxO (Plan A o run lifecycle), validá firma:
|
||||
`X-Orchestrate-Timestamp` + `Nonce` + `Signature`, skew tolerance 300s,
|
||||
408 si stale, 401 si firma mala.
|
||||
|
||||
### O4 — Watchdog para estados atascados
|
||||
Si un caso queda en `executing` >90s, el siguiente `GET` lo auto-promueve
|
||||
a `paused` para que el usuario reintente. Sin esto, conexiones caídas
|
||||
dejan basura.
|
||||
|
||||
## 🧪 Evals
|
||||
|
||||
### E1 — Cuatro capas de eval
|
||||
1. **Native** — `orchestrate agents test <agent> --input-file scenario.json` (fixture-based)
|
||||
2. **Agent behavior** — verifica delegation/escalation correcta sobre dataset
|
||||
3. **Runbook compliance** — verifica que el ticket final tenga campos esperados
|
||||
4. **Web layer** — Playwright o requests asserts sobre la UI
|
||||
|
||||
Ver `eval-strategy.md` para detalles.
|
||||
|
||||
### E2 — Fixtures JSON versionados
|
||||
Bajo `evals/scenarios/*.json`, ejecutables con `orchestrate agents test`.
|
||||
Cada scenario cubre un caso (happy path, edge case, failure).
|
||||
|
||||
### E3 — Smoke test en bash
|
||||
`evals/smoke-test.sh` cubre login → create → execute → poll → assert.
|
||||
Útil para CI y para reproducir bugs reportados.
|
||||
|
||||
### E4 — Direct backend probe
|
||||
`evals/direct-backend-probe.sh` golpea el backend directamente con curl +
|
||||
el token para aislar "¿es problema del agente o de la auth?". 200 con
|
||||
error de lógica → agent issue. 401 → connection/credentials.
|
||||
|
||||
## 🚦 Versionado del ADK
|
||||
|
||||
### V1 — Pin a `ibm-watsonx-orchestrate==2.1.0`
|
||||
Cambios entre minor versions han roto schemas. `requirements.txt` pinea
|
||||
exact version.
|
||||
|
||||
### V2 — `check-adk-version.sh` avisa de versiones nuevas
|
||||
Compara el pin con el último en PyPI y warneaa. **NO actualices sin
|
||||
probar todas las evals primero**.
|
||||
|
||||
---
|
||||
|
||||
Para ver dónde se aplica cada regla, leé el código del linter:
|
||||
[`../evals/lint_wxo_yaml.py`](../evals/lint_wxo_yaml.py).
|
||||
Reference in New Issue
Block a user