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:
43
scripts/check-adk-version.sh
Executable file
43
scripts/check-adk-version.sh
Executable 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
176
scripts/deploy-wxo.sh
Executable 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
63
scripts/new-specialist.sh
Executable 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
56
scripts/reset-wxo.sh
Executable 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
33
scripts/undeploy-wxo.sh
Executable 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"
|
||||
Reference in New Issue
Block a user