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

43
scripts/check-adk-version.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Compara la versión pineada (2.1.0) con la última disponible en PyPI.
# Warnea si hay nueva pero NO actualiza automáticamente — Felipe debe correr
# las evals antes de bumpear.
set -euo pipefail
PINNED="2.1.0"
PACKAGE="ibm-watsonx-orchestrate"
echo "→ Checking ADK version..."
echo " Pinned: $PINNED"
INSTALLED=$(pip show "$PACKAGE" 2>/dev/null | grep -i '^version:' | awk '{print $2}' || echo "not-installed")
echo " Installed: $INSTALLED"
LATEST=$(curl -s "https://pypi.org/pypi/$PACKAGE/json" | python3 -c "import sys, json; print(json.load(sys.stdin)['info']['version'])" 2>/dev/null || echo "unknown")
echo " Latest on PyPI: $LATEST"
if [ "$INSTALLED" != "$PINNED" ]; then
echo ""
echo "⚠ Installed ($INSTALLED) does NOT match pinned ($PINNED)."
echo " Run: pip install '${PACKAGE}==${PINNED}'"
fi
if [ "$LATEST" != "$PINNED" ] && [ "$LATEST" != "unknown" ]; then
echo ""
echo " Nueva versión disponible en PyPI: $LATEST (pin: $PINNED)"
echo " ANTES de actualizar:"
echo " 1. Crear branch chore/adk-$LATEST"
echo " 2. pip install '${PACKAGE}==${LATEST}'"
echo " 3. ./evals/eval-agents.sh ← TODAS deben pasar"
echo " 4. Si pasan, bumpear el pin en wxo/tools/python/requirements.txt y este script"
fi
echo ""
echo "→ Verificando modelo preferido (gpt-oss-120b)..."
if orchestrate models list 2>/dev/null | grep -q "gpt-oss-120b"; then
echo " ✓ gpt-oss-120b disponible"
else
echo " ⚠ gpt-oss-120b NO encontrado en este tenant"
echo " Fallback documentado: meta-llama/llama-3-3-70b-instruct (con caveat I-002)"
fi

176
scripts/deploy-wxo.sh Executable file
View File

@@ -0,0 +1,176 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Deploy completo de la solución WxO. Idempotente, re-runnable.
#
# Hace en orden:
# 1. Connections (draft Y live — issue I-004)
# 2. Tools (Python + OpenAPI según haya)
# 3. KBs
# 4. Agents (specialists primero — issue A4)
# 5. Deploy a live de todos los agents
# 6. Channel webchat (si hay landing)
#
# Variables de control:
# PUBLIC_HOST — host público donde corre tu stack
# WXO_ENV_NAME — env del ADK a usar (default: el activo)
# SKIP_TOOLS — saltar tools
# SKIP_KB — saltar KBs
# SKIP_CHANNEL — saltar canal webchat
# AGENTS_GLOB — patrón para detectar agents (default: wxo/agents/*.agent.yaml)
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
# ─── Config (con defaults razonables) ───────────────────────────────────────
PUBLIC_HOST="${PUBLIC_HOST:-mi-cliente.fitlabs.dev}"
PUBLIC_BASE="https://${PUBLIC_HOST}"
SKIP_TOOLS="${SKIP_TOOLS:-false}"
SKIP_KB="${SKIP_KB:-false}"
SKIP_CHANNEL="${SKIP_CHANNEL:-false}"
AGENTS_GLOB="${AGENTS_GLOB:-wxo/agents/*.agent.yaml}"
echo "════════════════════════════════════════════════════════════════"
echo " WxO deploy — $PUBLIC_BASE"
echo " env activo : $(orchestrate env list 2>/dev/null | grep ACTIVE || echo '???')"
echo "════════════════════════════════════════════════════════════════"
# ─── 1. CONNECTIONS ─────────────────────────────────────────────────────────
# Auto-detect: cada wxo/connections/*.yaml define un app_id.
# Mapping del BASE_URL se hace por convención:
# nombre del archivo (sin extensión) → /<basename>
# Override por archivo en `scripts/deploy-wxo.conf`.
echo "→ [1/6] Connections"
for f in wxo/connections/*.yaml; do
[ -f "$f" ] || continue
app_id=$(grep -E '^name:' "$f" | head -1 | awk '{print $2}')
[ -z "$app_id" ] && { echo " skip $f (no name)"; continue; }
# Convención: BASE_URL = $PUBLIC_BASE/<basename_sin_connection>
basename="${f##*/}"
basename="${basename%_connection.yaml}"
basename="${basename%.yaml}"
base_url="${PUBLIC_BASE}/${basename}"
echo "$app_id$base_url"
orchestrate connections add -a "$app_id" 2>/dev/null || true
for ENV in draft live; do
# Detectar kind (key_value | api_key | mcp) del YAML
kind=$(grep -E '^auth_type:' "$f" | awk '{print $2}')
[ -z "$kind" ] && kind="key_value"
orchestrate connections configure -a "$app_id" --env "$ENV" --type team --kind "$kind" 2>/dev/null || true
orchestrate connections set-credentials -a "$app_id" --env "$ENV" -e "BASE_URL=${base_url}" 2>/dev/null || true
done
done
# ─── 2. TOOLS PYTHON ────────────────────────────────────────────────────────
if [ "$SKIP_TOOLS" != "true" ]; then
echo "→ [2/6] Tools (Python)"
if [ -f wxo/tools/python/requirements.txt ]; then
for f in wxo/tools/python/*.py; do
[ -f "$f" ] || continue
# Saltar los archivos de soporte (empiezan con _)
base="${f##*/}"
case "$base" in _*) continue ;; esac
# Derivar app_id del nombre (convención: <app>_tools.py → app_demo o app)
app_id="${base%_tools.py}_demo"
echo "$f → app-id $app_id"
orchestrate tools import -k python \
-f "$f" \
-r wxo/tools/python/requirements.txt \
--app-id "$app_id" || echo " (already imported, skipping)"
done
fi
# ─── 3. TOOLS OPENAPI ─────────────────────────────────────────────────────
echo "→ [3/6] Tools (OpenAPI)"
for f in wxo/tools/openapi/*.yaml wxo/tools/openapi/*.json; do
[ -f "$f" ] || continue
base="${f##*/}"
case "$base" in _*) continue ;; esac
# Derivar app_id del nombre
app_id="${base%.yaml}"
app_id="${app_id%.json}"
echo "$f → app-id $app_id"
orchestrate tools import -k openapi -f "$f" --app-id "$app_id" || echo " (already imported, skipping)"
done
fi
# ─── 4. KNOWLEDGE BASES ─────────────────────────────────────────────────────
if [ "$SKIP_KB" != "true" ]; then
echo "→ [4/6] Knowledge bases"
for f in wxo/knowledge_base/*.kb.yaml; do
[ -f "$f" ] || continue
base="${f##*/}"
case "$base" in _*) continue ;; esac
echo "$f"
orchestrate knowledge-bases import -f "$f" || echo " (already imported, skipping)"
done
fi
# ─── 5. AGENTS ──────────────────────────────────────────────────────────────
# IMPORTANTE: specialists antes que orchestrators (issue A4).
# Convención: si el archivo se llama *orchestrator* o *n1_* va último.
echo "→ [5/6] Agents"
SPECIALIST_FILES=()
ORCHESTRATOR_FILES=()
for f in $AGENTS_GLOB; do
[ -f "$f" ] || continue
base="${f##*/}"
case "$base" in _*) continue ;; esac
case "$base" in
*orchestrator*|*n1_*) ORCHESTRATOR_FILES+=("$f") ;;
*) SPECIALIST_FILES+=("$f") ;;
esac
done
for f in "${SPECIALIST_FILES[@]}"; do
echo " • specialist $f"
orchestrate agents import -f "$f"
done
for f in "${ORCHESTRATOR_FILES[@]}"; do
echo " • orchestrator $f"
orchestrate agents import -f "$f"
done
# Deploy a live de todos
echo "→ [5b/6] Deploying agents to LIVE"
for f in "${SPECIALIST_FILES[@]}" "${ORCHESTRATOR_FILES[@]}"; do
name=$(grep -E '^name:' "$f" | head -1 | awk '{print $2}')
[ -z "$name" ] && continue
echo "$name → live"
orchestrate agents deploy --name "$name" --env live || echo " (deploy failed, see logs)"
done
# ─── 6. CHANNEL WEBCHAT ─────────────────────────────────────────────────────
if [ "$SKIP_CHANNEL" != "true" ] && [ ${#ORCHESTRATOR_FILES[@]} -gt 0 ]; then
echo "→ [6/6] Channel webchat (bound to orchestrator)"
for f in "${ORCHESTRATOR_FILES[@]}"; do
name=$(grep -E '^name:' "$f" | head -1 | awk '{print $2}')
[ -z "$name" ] && continue
orchestrate channels create webchat \
--agent "$name" \
--name "${name} - Web" 2>/dev/null || echo " (channel for $name already exists)"
done
fi
# ─── DONE ───────────────────────────────────────────────────────────────────
echo "════════════════════════════════════════════════════════════════"
echo " ✓ Deploy completado."
echo ""
echo " Próximos pasos manuales:"
echo " 1. orchestrate agents list → capturar agentId y agentEnvironmentId del orchestrator"
echo " 2. Pegarlos en tu landing HTML y env vars"
echo " 3. WxO Console → Settings → Embed Security → OFF (sin esto, embed falla)"
echo " 4. ./evals/smoke-test.sh → validar end-to-end"
echo "════════════════════════════════════════════════════════════════"

63
scripts/new-specialist.sh Executable file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Scaffolding de un nuevo specialist: copia los templates y rellena REPLACE_*.
# Uso:
# ./scripts/new-specialist.sh <name> <domain>
# Ejemplo:
# ./scripts/new-specialist.sh ad_specialist_acme identidad
set -euo pipefail
if [ $# -lt 2 ]; then
echo "Usage: $0 <specialist-name> <domain>"
echo "Example: $0 ad_specialist_acme identidad"
exit 1
fi
NAME="$1"
DOMAIN="$2"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
AGENT_FILE="$ROOT/wxo/agents/${NAME}.agent.yaml"
TOOLS_FILE="$ROOT/wxo/tools/python/${DOMAIN}_tools.py"
KB_FILE="$ROOT/wxo/knowledge_base/kb_${DOMAIN}.kb.yaml"
CONN_FILE="$ROOT/wxo/connections/${DOMAIN}_connection.yaml"
# Copiar agent template y reemplazar
if [ ! -f "$AGENT_FILE" ]; then
cp "$ROOT/wxo/agents/_template-specialist.agent.yaml" "$AGENT_FILE"
sed -i.bak "s/REPLACE_specialist_name/${NAME}/g; s/REPLACE_DOMINIO/${DOMAIN}/g; s/REPLACE_domain/${DOMAIN}/g; s/REPLACE_kb_name/kb_${DOMAIN}/g" "$AGENT_FILE"
rm "${AGENT_FILE}.bak"
echo "✓ Created: $AGENT_FILE"
else
echo "⚠ Already exists: $AGENT_FILE — skipping"
fi
if [ ! -f "$TOOLS_FILE" ]; then
cp "$ROOT/wxo/tools/python/_template_tools.py" "$TOOLS_FILE"
sed -i.bak "s/REPLACE_domain/${DOMAIN}/g" "$TOOLS_FILE"
rm "${TOOLS_FILE}.bak"
echo "✓ Created: $TOOLS_FILE"
fi
if [ ! -f "$KB_FILE" ]; then
cp "$ROOT/wxo/knowledge_base/_template.kb.yaml" "$KB_FILE"
sed -i.bak "s/REPLACE_kb_name/kb_${DOMAIN}/g; s/REPLACE_DOMINIO/${DOMAIN}/g" "$KB_FILE"
rm "${KB_FILE}.bak"
echo "✓ Created: $KB_FILE (acordate de agregar los runbooks)"
fi
if [ ! -f "$CONN_FILE" ]; then
cp "$ROOT/wxo/connections/_template-keyvalue.yaml" "$CONN_FILE"
sed -i.bak "s/REPLACE_app_name/${DOMAIN}_demo/g; s/REPLACE_SISTEMA/${DOMAIN}/g" "$CONN_FILE"
rm "${CONN_FILE}.bak"
echo "✓ Created: $CONN_FILE"
fi
echo ""
echo "Siguientes pasos:"
echo " 1. Editar $AGENT_FILE — rellenar REPLACE_* restantes"
echo " 2. Editar $TOOLS_FILE — implementar las tools de ${DOMAIN}"
echo " 3. Crear runbook bajo wxo/knowledge_base/runbooks/ y referenciarlo en $KB_FILE"
echo " 4. Si ${DOMAIN} no es un mock, considerá usar _template-apikey-header.yaml en lugar de keyvalue"
echo " 5. Si ${NAME} debe ser collaborator de un orchestrator, agregalo allí"
echo " 6. ./scripts/deploy-wxo.sh"

56
scripts/reset-wxo.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Borra TODO lo desplegado en el tenant WxO activo: agents, KBs, tools, connections, channels.
# Para volver a empezar from scratch.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
echo "════════════════════════════════════════════════════════════════"
echo " WxO reset — env: $(orchestrate env list 2>/dev/null | grep ACTIVE || echo '???')"
echo " ESTO BORRA TODO. Esperando 5 segundos para que canceles con Ctrl+C..."
sleep 5
# Channels
echo "→ Deleting channels"
orchestrate channels list 2>/dev/null | tail -n +2 | awk '{print $1}' | while read -r ch; do
[ -z "$ch" ] && continue
orchestrate channels delete --id "$ch" 2>/dev/null || true
done
# Agents
echo "→ Removing agents"
for f in wxo/agents/*.agent.yaml; do
[ -f "$f" ] || continue
name=$(grep -E '^name:' "$f" | head -1 | awk '{print $2}')
[ -z "$name" ] && continue
orchestrate agents remove --name "$name" 2>/dev/null || true
done
# KBs
echo "→ Removing knowledge bases"
for f in wxo/knowledge_base/*.kb.yaml; do
[ -f "$f" ] || continue
name=$(grep -E '^name:' "$f" | head -1 | awk '{print $2}')
[ -z "$name" ] && continue
orchestrate knowledge-bases remove --name "$name" 2>/dev/null || true
done
# Tools
echo "→ Removing tools (Python + OpenAPI)"
orchestrate tools list 2>/dev/null | tail -n +2 | awk '{print $1}' | while read -r t; do
[ -z "$t" ] && continue
orchestrate tools remove --name "$t" 2>/dev/null || true
done
# Connections
echo "→ Removing connections"
for f in wxo/connections/*.yaml; do
[ -f "$f" ] || continue
name=$(grep -E '^name:' "$f" | head -1 | awk '{print $2}')
[ -z "$name" ] && continue
orchestrate connections remove -a "$name" 2>/dev/null || true
done
echo "✓ Reset completo. Podés re-correr ./scripts/deploy-wxo.sh"

33
scripts/undeploy-wxo.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Undeploy quirúrgico: borra solo lo que está en este repo, no toda la tenant.
# Más conservador que reset-wxo.sh.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
echo "→ Removing agents declared in this repo"
for f in wxo/agents/*.agent.yaml; do
[ -f "$f" ] || continue
base="${f##*/}"
case "$base" in _*) continue ;; esac
name=$(grep -E '^name:' "$f" | head -1 | awk '{print $2}')
[ -z "$name" ] && continue
echo "$name"
orchestrate agents remove --name "$name" 2>/dev/null || true
done
echo "→ Removing KBs declared in this repo"
for f in wxo/knowledge_base/*.kb.yaml; do
[ -f "$f" ] || continue
base="${f##*/}"
case "$base" in _*) continue ;; esac
name=$(grep -E '^name:' "$f" | head -1 | awk '{print $2}')
[ -z "$name" ] && continue
echo "$name"
orchestrate knowledge-bases remove --name "$name" 2>/dev/null || true
done
echo "→ Skipping tools and connections (use reset-wxo.sh for full clean)"
echo "✓ Done"