Las lecciones anteriores explican cada pieza por separado. Falta ver cómo se conectan en un sistema concreto y qué código hay detrás de cada conexión. Esta lección recorre un caso completo con enlaces al código, fijados a un commit de cada repositorio.
Una advertencia antes de empezar: Adapta es un prototipo funcional sin auditar, docket es beta de un solo operador y la flota de ejecución de Tack no está en un release etiquetado. Ninguno está endurecido para producción; los usamos porque su código se puede leer línea por línea.
El escenario y la arquitectura
Adapta aporta el conocimiento (y el comportamiento, si hace falta), docket gobierna las acciones y Tack registra el trabajo de construirlo.
empleado
| pregunta
v
+----------------+ /v1/chat/completions +--------------------+
| app de soporte |----------------------->| Adapta |
| (tu código) |<-----------------------| RAG + adapter |
+----------------+ respuesta + citas | Postgres, Chroma |
| +--------------------+
| tool call "open_ticket"
v
+----------------+ ask +----------------------+
| docket-runtime |------->| aprobación humana |
| política+audit |<-------| docket approve/deny |
+----------------+ +----------------------+
|
v API de tickets
Tack: ítems y runners para el trabajo de construir lo anterior
La "app de soporte" es código tuyo: ninguno de los tres productos la trae. El pegamento, la autenticación de usuarios y la interfaz siguen siendo tu responsabilidad.
1. Conocimiento: ingerir y recuperar con Adapta
Adapta se levanta con make up (README.md) sobre Docker Compose (docker-compose.yml): API FastAPI, worker, Postgres, Redis y ChromaDB. Para basta con CPU.
El flujo por API es: crear proyecto, subir documentos, crear endpoint y emitir una llave (POST …/keys, omitido aquí). Extracto de docs/user-guide/knowledge-and-behavior.md (allí el proyecto es de tipo finetune; para solo conocimiento, el tipo es rag):
PROJECT_ID=$(curl -s localhost:8000/v1/projects \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d "{\"name\": \"Support assistant\", \"type\": \"finetune\",
\"base_model\": \"qwen2.5-3b-instruct\", \"team_id\": \"$TEAM_ID\"}" | jq -r .id)
curl -s localhost:8000/v1/projects/$PROJECT_ID/files \
-H "Authorization: Bearer $TOKEN" \
-F file=@espressomat-9000-service-manual.pdf
SLUG=$(curl -s -X POST localhost:8000/v1/projects/$PROJECT_ID/endpoint \
-H "Authorization: Bearer $TOKEN" | jq -r .slug)
Veamos qué ocurre detrás de cada llamada:
- Subida.
upload_filevalida la extensión, guarda el archivo, registra la fila comopending, agenda la indexación y responde202. - Indexación.
_index_fileparsea, parte y calcula en un hilo aparte, y marcaindexedofailed. El está enchunk_texty la escritura en Chroma enindex_chunks, que usaupsertpara que reindexar no duplique filas. - Endpoint.
create_endpointse niega a crear uno para un proyecto RAG sin chunks indexados. - Consulta.
_resolve_endpointverifica la llaveadp_contra su hash y exige que el campomodelcoincida con el slug del endpoint (líneas 81–88). Luegoretrievehace la ,_build_systeminyecta los chunks numerados y_fit_contextdescarta los menos relevantes si el no cabe.
La app llama al endpoint con cualquier cliente compatible con OpenAI y recibe además citations. La teoría está en Embeddings y RAG.
2. Comportamiento: cuándo entrenar un adapter
Los hechos van en RAG. Un se justifica cuando quieres una forma estable que el prompt no sostiene: etiquetas de un conjunto cerrado, un JSON fijo, la plantilla de la casa (Fine-tuning, LoRA y QLoRA). Aquí, el candidato es el clasificador de tickets. En Adapta se sube un dataset JSONL, se crea un job y se espera el veredicto del :
El gate mide perplejidad sobre ejemplos reservados, no exactitud de la etiqueta: frena adapters que no aprendieron nada, pero no reemplaza tus propias métricas (Evaluar modelos).
3. Actuar con seguridad: una herramienta gobernada con docket
Como explica Gobernanza y human-in-the-loop, la decisión de actuar la toma código, no el modelo.
docket se usa como CLI, como no interactivo o como librería embebida. Para una aplicación propia conviene la tercera: docket-runtime despacha tus herramientas por el mismo punto de control de política, aprobación, traza y auditoría que la CLI. Se construye desde el código fuente; no está publicado en ningún índice de paquetes.
El ejemplo real del repositorio es examples/runtime_embed.py. Adaptado a nuestro caso queda así (ilustrativo; create_ticket_in_helpdesk es tu código):
from docket_runtime import Runtime, Tool, ToolCall, ToolContext, ToolOutcome
def open_ticket(args, ctx):
ticket_id = create_ticket_in_helpdesk(args["title"], args["queue"])
return ToolOutcome(True, f"opened {ticket_id}")
runtime = Runtime()
runtime.register(Tool(
"open_ticket", "Open a support ticket in a queue.",
{"type": "object", "required": ["title", "queue"],
"properties": {"title": {"type": "string"}, "queue": {"type": "string"}}},
open_ticket, kind="write",
))
result = runtime.dispatch(
ToolCall(call_id, "open_ticket", arguments_json),
ToolContext(agent_id="support-bot", role="support", project="helpdesk"),
)
La política es un JSON en ~/.docket/policies/. Una llamada se evalúa como texto con la forma nombre clave=valor (render_tool_call), así que un patrón anclado al nombre basta (ilustrativo, con la misma forma que la plantilla real high-risk-deploy.json):
{
"id": "approve-open-ticket",
"applies_to": ["*"],
"hook": "pre_tool_call",
"match": {"type": "regex", "pattern": "^open_ticket\\b"},
"action": "require_approval",
"message": "Opening a ticket needs a support lead."
}
El recorrido de una llamada dentro de dispatch_tool:
- Herramienta desconocida o argumentos ilegibles o faltantes:
deny. evaluate_tool_callcombina el clasificador de comandos (solo para herramientasexec) conpolicy_eval_detail. Gana lo más restrictivo.- Con
ask, se auditatool.askyapproval_createguarda un registroapr-…redactado;wait_for_approvalespera respuesta hasta un plazo (config.py); vencer equivale a denegar. - Una persona responde con
docket approve/docket deny, o por HTTP, o Telegram. - Cada decisión va al encadenado por hash (
audit_log,verify_chain); los eventos, a trazas JSONL (trace_event).
docket approve # list pending approvals
docket approve apr-<uuid> # grant one
docket audit verify # walk the hash chain
docket policies test prueba una política sin ejecutar nada (docs/commands.md). Si prefieres que docket maneje el bucle completo, run_agent_turn despacha cada tool call por el mismo dispatch_tool.
4. Seguir el trabajo: ítems y runners en Tack
Construir el asistente implica muchas tareas: indexar manuales, escribir open_ticket, armar el dataset, redactar políticas. Tack es un gestor de proyectos que además puede entregar un ítem a un (Claude Code o Codex) y registrar la ejecución en su historial.
tack init "Support assistant" --type software
tack add "Write the open_ticket tool" --project <id> --type task
tack execution create <ITEM_ID> --runner <RUNNER_ID> \
--agent-profile <PROFILE_ID> --harness claude-code ...
tack execution get <REQUEST_ID>
execution create está abreviado; la versión completa está en agent-runners.md y el cuerpo se arma en create_execution_body. El scheduler elige por capacidades (select_runner); el runner escribe su journal antes de tocar el workspace (run_claimed) y lanza el harness vía HarnessAdapter; los estados válidos están en validate_transition. y fencing: Ejecución durable de agentes.
5. Un chat en producción: el widget de yielab.com
Como contraste, un chat que sí está desplegado: el widget de preventa de este sitio. Su repositorio es privado, así que solo describimos la arquitectura.
- Runtime. Next.js sobre Cloudflare Workers con el adaptador OpenNext. El endpoint es sin estado: el navegador reenvía la conversación en cada turno y el servidor la trata como no confiable.
- Modelo. Un modelo de Anthropic vía Vercel AI Gateway, que centraliza la llave y el tope de gasto. Una llamada por turno, sin reintentos.
- Controles previos, del más barato al más caro. Tamaño del cuerpo, origen, anti-bot con Turnstile y luego un ticket firmado con HMAC, tope de turnos, rate limiting por IP y un contador diario en KV. Fallan cerrados; el modelo va al final.
- Grounding. Sin RAG: el catálogo de servicios cabe en el system prompt y se arma con una lista explícita de campos, para que los precios nunca lleguen al modelo.
- Salida. Un guard revisa la respuesta completa (dinero, un canario que delata filtraciones del prompt, largo) y, si falla, muestra un mensaje fijo. El correo del lead se arma en el servidor desde un JSON validado, nunca con texto libre del modelo.
- . Eventos sin contenido del visitante van a logs y Workers Analytics Engine; las conversaciones, a Langfuse (región UE) como spans OpenTelemetry, en segundo plano.
- . Una suite offline y determinista corre en cada build; la comparación pagada entre modelos sigue pendiente según el roadmap.
6. Antes de producción real
| Área | Qué agregar |
|---|---|
| Autenticación | SSO para empleados delante de tu app |
| Tenancy y permisos | Filtro de documentos por usuario; docket no tiene eje de tenant |
| Secretos | Gestor de secretos; rotar las credenciales por defecto de Adapta; TLS delante de docket serve |
| Evals en CI | Golden set con documento esperado; casos adversarios de open_ticket |
| Monitoreo | /health/deep de Adapta; alertas por aprobaciones pendientes |
| Auditoría de datos | Adapta aún no tiene log de auditoría ni soft-delete (TODO.md) |
Para CTOs
- Decide qué compras, qué adoptas y qué escribes; la aplicación, el SSO y la integración con el helpdesk son siempre tuyos.
- Exige evidencia del estado real (known limits, roadmap, tests) y presupuesta una revisión de seguridad antes de conectar un prototipo a datos sensibles.
Fuentes
- OpenNext para Cloudflare
- Vercel AI Gateway
- Cloudflare Turnstile
- Rate limiting en Cloudflare Workers
- Langfuse y OpenTelemetry
Para llevar
- Los componentes resuelven partes; la aplicación, la identidad y el pegamento son tuyos.
- En Adapta, el eval gate impide servir un adapter no verificado.
- En docket, toda acción pasa por
dispatch_tool: política, aprobación con vencimiento que deniega y auditoría encadenada. - Tack sigue y ejecuta el trabajo de ingeniería; no orquesta el asistente en ejecución.
- En producción, los controles baratos van antes del modelo y la salida se revisa completa.
Comprueba lo aprendido
¿Qué impide que Adapta sirva un adapter que no aprendió nada?
create_endpoint exige un job con eval_passed=True, y el worker solo lo marca si el adapter supera el umbral absoluto o mejora claramente al modelo base sobre el held-out.
Nadie responde la aprobación de `open_ticket`. ¿Qué pasa?
wait_for_approval vence y resuelve el registro como denegado; dispatch_tool devuelve approval_timeout sin ejecutar nada.
¿Por qué el chat de yielab.com no usa RAG?
Su corpus es un catálogo pequeño que cabe en el system prompt; RAG solo se justifica con un corpus grande o cambiante.