"""Template — Python tools wrapper. Reemplazar REPLACE_* con los valores de tu caso. Una función por endpoint mockeado o por acción atómica. Máx 10 funciones por archivo (regla A1 — si necesitás más, partí en specialists). Patrón: - Cada función usa `@observable_tool` (NO `@tool` directo — regla T1). - Cada función con input no-trivial declara un schema Pydantic con coerción explícita (regla T2 — issue I-003). - BASE_URL viene del env de la connection, nunca hardcoded (regla T4). - Compat shim inline al inicio del archivo (regla T3 — issue I-010). """ from __future__ import annotations # === inline compat shim (NO eliminar — TRM lo necesita) === try: from ibm_watsonx_orchestrate.agent_builder.tools import tool as _adk_tool except ImportError: def _adk_tool(*args, **kwargs): def deco(f): return f return deco if not args else deco(args[0]) # ============================================================ import os import requests from pydantic import BaseModel, field_validator from typing import Optional # Imports de helpers del template — ABSOLUTOS, no relativos (regla T3). # Cuando este archivo se importe en el TRM, los _observability/_coercion # tienen que estar en sys.path. El deploy script los inlinea o los # distribuye junto. try: from wxo.tools.python._observable_tool import observable_tool from wxo.tools.python._coercion_helpers import _coerce_int, _coerce_list, _coerce_bool except ImportError: # Fallback si los helpers no están disponibles en el TRM: # observable_tool degrada a @tool puro, coerción degrada a identidad. def observable_tool(*, name, domain="", description=None, _bypass_tracing=False): def deco(f): return _adk_tool(name=name, description=description)(f) if description else _adk_tool(name=name)(f) return deco def _coerce_int(v): return int(v) if isinstance(v, str) and v.strip().isdigit() else v def _coerce_list(v): return [v] if isinstance(v, str) else v def _coerce_bool(v): return v # BASE_URL viene de la connection. NUNCA hardcoded. BASE_URL = os.environ.get("BASE_URL", "") TIMEOUT_SEC = int(os.environ.get("TOOL_TIMEOUT_SEC", "10")) # ─── Input schemas with coercion ──────────────────────────────────────────── class LookupInput(BaseModel): username: str class ExecuteInput(BaseModel): target_id: int options: list[str] = [] notify: bool = True # Coerciones explícitas — el LLM stringifica todo (issue I-003) _coerce_id = field_validator("target_id", mode="before")(_coerce_int) _coerce_opts = field_validator("options", mode="before")(_coerce_list) _coerce_notify = field_validator("notify", mode="before")(_coerce_bool) # ─── Tools ────────────────────────────────────────────────────────────────── @observable_tool( name="REPLACE_lookup", domain="REPLACE_domain", description="REPLACE — busca un recurso por identificador y devuelve sus datos." ) def REPLACE_lookup(username: str) -> dict: """REPLACE descripción de qué hace esta tool. Args: username: REPLACE qué es el username Returns: dict con campos: REPLACE qué campos devuelve """ resp = requests.post( f"{BASE_URL}/users/lookup", json={"username": username}, timeout=TIMEOUT_SEC, ) resp.raise_for_status() return resp.json() @observable_tool( name="REPLACE_execute", domain="REPLACE_domain", description="REPLACE — ejecuta la acción principal contra el sistema externo." ) def REPLACE_execute(target_id: int, options: Optional[list[str]] = None, notify: bool = True) -> dict: """REPLACE descripción. Args: target_id: REPLACE options: REPLACE notify: REPLACE Returns: dict con: REPLACE """ payload = { "target_id": target_id, "options": options or [], "notify": notify, } resp = requests.post( f"{BASE_URL}/execute", json=payload, timeout=TIMEOUT_SEC, ) resp.raise_for_status() return resp.json() @observable_tool( name="REPLACE_get_status", domain="REPLACE_domain", description="REPLACE — devuelve el estado actual de un recurso." ) def REPLACE_get_status(resource_id: str) -> dict: """REPLACE descripción.""" resp = requests.get( f"{BASE_URL}/status/{resource_id}", timeout=TIMEOUT_SEC, ) resp.raise_for_status() return resp.json()