feat: v1 — boilerplate WxO + web
Some checks failed
CI — Lint + Evals / lint (push) Has been cancelled
CI — Lint + Evals / smoke (push) Has been cancelled

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:
Felipe Arentsen
2026-05-16 14:59:44 +00:00
commit b089c2ef18
70 changed files with 6739 additions and 0 deletions

253
docs/wxo-best-practices.md Normal file
View 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).