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:
90
wxo/tools/python/_coercion_helpers.py
Normal file
90
wxo/tools/python/_coercion_helpers.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Pydantic coercion helpers for tool input schemas.
|
||||
|
||||
LLMs stringify everything — they send `"95727067"` when your schema
|
||||
expects `int`, or `'[{"id":1}]'` when you expect `list[dict]`. Pydantic
|
||||
strict mode returns 422 and the agent fails.
|
||||
|
||||
Use these as `mode="before"` validators on every tool input schema.
|
||||
|
||||
Example:
|
||||
|
||||
from pydantic import BaseModel, field_validator
|
||||
from wxo.tools.python._coercion_helpers import _coerce_int, _coerce_list
|
||||
|
||||
class ResetPasswordInput(BaseModel):
|
||||
user_id: int
|
||||
groups: list[str]
|
||||
_coerce_user_id = field_validator("user_id", mode="before")(_coerce_int)
|
||||
_coerce_groups = field_validator("groups", mode="before")(_coerce_list)
|
||||
|
||||
See `docs/known-issues.md#i-003` for the full story.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _coerce_int(v: Any) -> Any:
|
||||
"""Accepts int or stringified int."""
|
||||
if isinstance(v, str):
|
||||
s = v.strip()
|
||||
if s.lstrip("-").isdigit():
|
||||
return int(s)
|
||||
return v
|
||||
|
||||
|
||||
def _coerce_float(v: Any) -> Any:
|
||||
if isinstance(v, str):
|
||||
try:
|
||||
return float(v.strip())
|
||||
except ValueError:
|
||||
pass
|
||||
return v
|
||||
|
||||
|
||||
def _coerce_bool(v: Any) -> Any:
|
||||
"""Accepts bool, 'true'/'false', '1'/'0', 'yes'/'no'."""
|
||||
if isinstance(v, bool):
|
||||
return v
|
||||
if isinstance(v, str):
|
||||
s = v.strip().lower()
|
||||
if s in ("true", "1", "yes", "y"):
|
||||
return True
|
||||
if s in ("false", "0", "no", "n"):
|
||||
return False
|
||||
return v
|
||||
|
||||
|
||||
def _coerce_list(v: Any) -> Any:
|
||||
"""Accepts list, stringified JSON list, or single value (wrapped)."""
|
||||
if isinstance(v, list):
|
||||
return v
|
||||
if isinstance(v, str):
|
||||
s = v.strip()
|
||||
if s.startswith("[") and s.endswith("]"):
|
||||
try:
|
||||
parsed = json.loads(s)
|
||||
if isinstance(parsed, list):
|
||||
return parsed
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
# Fallback: wrap single value
|
||||
return [v]
|
||||
return v
|
||||
|
||||
|
||||
def _coerce_dict(v: Any) -> Any:
|
||||
"""Accepts dict or stringified JSON dict."""
|
||||
if isinstance(v, dict):
|
||||
return v
|
||||
if isinstance(v, str):
|
||||
s = v.strip()
|
||||
if s.startswith("{") and s.endswith("}"):
|
||||
try:
|
||||
parsed = json.loads(s)
|
||||
if isinstance(parsed, dict):
|
||||
return parsed
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return v
|
||||
Reference in New Issue
Block a user