# GFIBER-708 (Spike A) — Propuesta técnica: replicar Triple T dentro de Upsell 2.0

## Contexto

**GFIBER-708** ("Spike A - Create experiment to replicate the Triple T functionality in Upsell 2.0") es un ticket tipo Spike sin descripción, asignado a Alejandro, estado *In Development*. Existe un hermano **GFIBER-709** ("Spike B", mismo título, asignado a Christian Barillas, estado *Blocked*) — son dos experimentos/propuestas paralelas que el equipo comparará antes de decidir el camino. **GFIBER-609** ("Take down the old GCP deployments for Triple T and Upsell", estado Analysis) confirma que el destino final es decomisionar el stack GCP legacy una vez migrado.

**Triple T** hoy es una herramienta de troubleshooting asistido por IA sobre GCP (FastAPI + LangGraph + Firestore), basada en un **grafo de nodos**: el agente de call center sigue una guía paso a paso (ej. "¿el cliente no tiene wifi? → revisar conexión del router → revisar luz del router → …"), con los nodos agrupados en clusters ("initial triage", "diagnóstico de layer físico") por un LLM de clustering. Corre en un proyecto GCP separado del stack Next.js/Supabase de este repo (ver página Docmost `Triple_T_—_Arquitectura_(GCP-Backend)`).

El usuario (asignado al Spike A) quiere que esta propuesta gire alrededor de un **chat agéntico** (no un árbol de clicks) que ayude al agente en lenguaje natural, pero que **siga fielmente el procedimiento definido** — sin inventar ni saltarse pasos. Discutimos y descartamos explícitamente:
- **LoRA fine-tuning**: no iterable (cualquier cambio al procedimiento exige re-entrenar), no garantiza orden estricto, fuera de escala para 2 ingenieros/3 meses.
- **RAG puro** (similitud semántica sobre texto libre): bueno para Q&A abierto, pero no lleva estado de "qué paso ya se hizo" ni garantiza no saltarse pasos del procedimiento.
- **Árbol de decisión puro** (el approach legacy): garantiza el orden pero es rígido y no conversacional.

Landing en: **chat agéntico con tool-calling sobre un árbol de troubleshooting versionado como JSON** (sin RAG como columna vertebral). Confirmado con el usuario. Ya existe en este repo el 80% de la infraestructura necesaria:

- [`app/(protected)/upsell-evaluator/api/copilot/route.ts`](app/(protected)/upsell-evaluator/api/copilot/route.ts) — loop agéntico acotado (`MAX_TOOL_ROUNDS`) contra Fuelix (gateway Claude de la empresa), con tools de solo-lectura ejecutadas server-side.
- [`utils/ai/copilot.ts`](utils/ai/copilot.ts) — `buildSystemPrompt` (base + "gem" persona), `COPILOT_TOOLS` (catálogo de tools como data), parsing de resultado estructurado.
- [`utils/ai/copilot-tools.ts`](utils/ai/copilot-tools.ts) — ejecución real de las tools contra Supabase.
- [`utils/ai/fuelix.ts`](utils/ai/fuelix.ts) — cliente Anthropic apuntado a Fuelix.
- Tabla `gems` (migración `20260606120000_create_gems.sql`) — ya permite inyectar instrucciones adicionales por sesión/persona.

**Supabase no soporta un motor de grafos nativo** (Apache AGE no está entre las extensiones habilitadas, y tiene problemas de compatibilidad de versión) — pero no lo necesitamos, y tampoco conviene modelarlo como tablas relacionales (`nodes`+`edges`): Triple T hoy es **un solo árbol** ("Internet Troubleshooting"), generado como unidad desde un Google Doc, y su escalabilidad futura pensada es "un árbol por guía adicional" (varios documentos pequeños, no un grafo gigante interconectado). Eso es exactamente el caso donde **modelar el árbol completo como JSON versionado** gana: hay precedente directo en el propio ecosistema — la tabla `configuration` de Apsel (BigQuery) versiona configuraciones completas del clasificador con `status active/inactive`, subiendo una versión nueva y despromoviendo la anterior sin tocar código. El mismo patrón acá: una tabla `troubleshooting_guides` (Postgres/Supabase) con columna `tree jsonb` + `version` + `status active/inactive`. El agente carga el árbol activo completo (o el subárbol relevante) y lo navega con funciones puras en TypeScript — no SQL, no recursive CTEs. Relacional (nodos/edges) seguiría siendo mejor *si* más adelante se necesitara edición de nodos individuales vía UI de admin o analytics por nodo vía SQL, pero eso no es parte del alcance de esta spike.

**Adenda — persistencia de datos capturados durante el chat de troubleshooting:** el usuario notó que el plan original no contemplaba que Upsell Evaluator ya captura datos del cliente/ticket, y preguntó si convendría construir un **cliente y servidor MCP** para que el chat guarde datos mencionados en la conversación. Investigación + exploración del código (agente Explore) concluyeron que **no** — MCP resuelve el problema de exponer un catálogo de tools a *múltiples clientes de IA distintos* o de aislar ejecución por gobernanza/auditoría; aquí el copilot vive en el mismo runtime Next.js que ejecutaría la tool, así que MCP solo agregaría latencia, overhead de tokens y un proceso más que mantener, sin ningún beneficio real (ver fuentes: [supabase/supabase#679](https://github.com/supabase/supabase/discussions/679), comparativa MCP vs tool-calling nativo 2026). Se confirmó además que las tools actuales (`utils/ai/copilot-tools.ts`) son **estrictamente de solo lectura** (`get_client_by_gaid`, `search_clients`, `recent_tickets`, `sales_summary` — solo `select`, nunca mutan) — este sería el primer caso de escritura del catálogo.

El usuario precisó el alcance: los datos a persistir son del **ticket** (`upsell_tickets`), no del perfil del cliente (`clients`) — específicamente datos de troubleshooting (ej. qué se revisó, qué encontró el agente), separados de la razón de la llamada que **ya se captura hoy** vía la columna existente `upsell_tickets.issue_type` (enum, incluye `internet_wifi`). Y quiere que el guardado no sea autónomo: **el agente de IA propone, el humano confirma con un click** antes de persistir — mismo criterio de seguridad que ya aplican a otros campos sensibles del ticket (`initiation_type`, `objection_handled_correctly`, `sales_coach_notes` — todos con Server Actions dedicadas y restricción de rol).

Dado ese "confirmar con click", **no hace falta una tool con efecto de escritura para el LLM** — alcanza con extender el mismo contrato de salida estructurada que ya usa el copilot (`CopilotResult` en `utils/ai/copilot.ts`, hoy `{ mood, replies, objection?, next_action? }`) con un campo nuevo `proposed_troubleshooting_data?: { label, value }[]`, renderizarlo como chips/botones de confirmación en la UI de chat, y persistir el click con un nuevo Server Action (mismo patrón que `saveClientSurvey`/`updateTicket` en `app/(protected)/upsell-evaluator/actions.ts`) que hace un append al nuevo campo jsonb del ticket.

## Entregable

Documento de **propuesta técnica** (no código de producto) en **Docmost**, siguiendo el patrón ya usado por otros planes del space GFiber (ej. `Upsell_State_Override_—_Plan_Historial_de_Cambios`, `GFIBER-673_—_Propuesta_Tutorial_in-app`).

**Ya creado en esta sesión** (turno anterior):
- Página padre: **`GFIBER-708_—_Propuesta_Troubleshooting_Agéntico_en_Upsell_2.0`** (id `019fa9bc-84e5-76d9-b8d1-ec8bba0f7cff`), hija de `GFiber_Pilot_Extension`.
- Subpágina **`Plan_(English)`** (id `019fa9bd-d66c-7462-9b1a-c0855ade6f37`).
- Subpágina **`Plan_(Español)`** (id `019fa9bd-4959-76ee-8a92-9f2861e87e2e`).

**Pendiente en esta iteración:** actualizar (`mcp__docmost__update_page`) ambas subpáginas para agregar la nueva sección 6 (persistencia de datos de troubleshooting en el ticket) descrita abajo — no se recrean las páginas, se extienden.

Ambas versiones deben cubrir el mismo contenido (no es un resumen vs. detalle — son traducciones equivalentes, como en los planes previos del space).

## Contenido del documento

1. **Resumen ejecutivo** — qué es Triple T hoy, por qué se está evaluando reemplazarlo/replicarlo dentro de Upsell 2.0, y que este documento es la propuesta "Spike A" (mencionar la existencia de "Spike B" en paralelo, sin asumir su contenido).
2. **Comparación de 4 enfoques** con tradeoffs concretos (árbol de decisión, RAG puro, fine-tuning/LoRA, agéntico+árbol-versionado) — la tabla de razones ya desarrollada en la conversación.
3. **Arquitectura recomendada**:
   - Modelo de datos: tabla `troubleshooting_guides` en Supabase Postgres con columna `tree jsonb` (el árbol completo: nodos, contenido en markdown, símbolos de síntomas/triggers, referencias a nodos siguientes, agrupación por cluster) + `version` + `status active/inactive` — mismo patrón de versionado que `configuration` de Apsel. Reemplaza el grafo de Firestore. Sin extensión de grafos, sin recursive CTEs: el árbol activo se carga completo y se navega con funciones puras en TypeScript.
   - Nuevas tools de agente (mismo esquema que `COPILOT_TOOLS`): p. ej. `get_next_candidate_steps(symptoms | current_node_id)`, `get_node(id)`, `search_nodes_by_symptom(text)` — operan sobre el JSON ya cargado en memoria, no hacen queries SQL por nodo. Todas de solo lectura, ejecutadas server-side como las existentes en `copilot-tools.ts`.
   - System prompt / "gem" de troubleshooting: instrucción explícita de **nunca inventar ni saltarse un paso** — siempre grounded en el resultado de una tool call antes de sugerir la siguiente acción al agente/cliente.
   - Reuso del loop agéntico ya existente en `copilot/route.ts` (bounded tool rounds, Fuelix) — nueva ruta o extensión de la existente, a definir en el documento como pregunta abierta de implementación (no se decide en esta spike).
   - Migración de contenido: el árbol actual "Internet Troubleshooting" (hoy en Firestore, generado desde un Google Doc) se migraría a un único registro `tree jsonb` en `troubleshooting_guides` — mencionar como trabajo de datos, no de código, fuera del alcance de esta spike.
4. **Qué NO cubre esta spike** — no incluye el pipeline de generación automática desde Google Doc ni el LLM-clustering de Triple T original (se asume carga estática/manual de nodos por ahora); no incluye migración de datos real ni decommission de GCP (eso es GFIBER-609, posterior).
5. **Próximos pasos** — validación con Morossini/equipo, comparación con Spike B (GFIBER-709) cuando se desbloquee, y qué se necesitaría para pasar de spike a implementación real.
6. **Persistencia de datos de troubleshooting en el ticket** (sección nueva):
   - **¿Por qué no MCP?** — un párrafo explicando que MCP resuelve exposición de tools a múltiples clientes de IA o aislamiento por gobernanza, ninguno de los cuales aplica aquí (copilot y tool corren en el mismo proceso Next.js); agregar MCP sumaría latencia/overhead de tokens sin beneficio. Se reutiliza el mismo tool-calling nativo (Anthropic) ya implementado.
   - **Modelo de datos:** nueva columna en `upsell_tickets` (migración nueva, ej. `troubleshooting_log jsonb not null default '[]'::jsonb`) — un array de entradas confirmadas `{ node_id, label, value, confirmed_at }`, por-ticket (no por-cliente): cada llamada/sesión de troubleshooting es su propio registro, coherente con que `upsell_tickets` ya es el log append-only por interacción. Distinto de `clients.discovery_answers` (perfil estable del hogar) y de `upsell_tickets.issue_type` (la razón de la llamada, que **ya existe** como enum — ej. `internet_wifi` — y no requiere cambios).
   - **Sin tool de escritura para el LLM:** dado que el usuario quiere confirmación humana con un click (no autoguardado silencioso), basta con extender el contrato de salida estructurada que ya devuelve el copilot (`CopilotResult` en `utils/ai/copilot.ts`) con un campo opcional `proposed_troubleshooting_data?: { label: string; value: string }[]`. La UI de chat renderiza cada propuesta como un chip/botón "Guardar"; al hacer click se llama un Server Action nuevo (mismo patrón que `saveClientSurvey`/`updateTicket` en `app/(protected)/upsell-evaluator/actions.ts`) que hace `append` al array `troubleshooting_log` del ticket activo.
   - **GAID + Case ID obligatorios al inicio de la conversación:** el chat de troubleshooting debe pedir el **GAID** y el **Case ID** como primer paso, antes de cualquier diagnóstico — son identificadores obligatorios y ya compartidos entre sistemas (GAID identifica al cliente/`clients.gaid`, Case ID cruza con el sistema de case-management vía `upsell_tickets.case_id`, ambos ya existentes en el esquema). Esto no es un dato más a proponer-y-confirmar como el resto: es el paso de identificación que resuelve **a qué ticket** pertenece la sesión — reutiliza `lookupClient(gaidInput)` (ya existente en `actions.ts`) para resolver/crear el `client_id`, y el mismo ticket en curso (identificado por `case_id`) es al que se le hace `append` en `troubleshooting_log`. Sin estos dos datos, no hay ticket al cual asociar ninguna propuesta de guardado — por eso van primero, no como una propuesta más entre otras.
   - **Fuera de alcance de esta spike:** la implementación real de la migración, el Server Action, y el cambio de UI — esta sección documenta el diseño para que el equipo lo evalúe junto con el resto de la propuesta, no lo construye.

## Verificación

- Ambas subpáginas (`Plan_(English)`, `Plan_(Español)`) actualizadas con la sección 6 nueva, vía `mcp__docmost__update_page`, verificado con `mcp__docmost__get_page` tras la escritura.
- Contenido revisado por el usuario antes de considerar la spike cerrada — no se toca el estado del ticket Jira GFIBER-708 a menos que el usuario lo pida explícitamente, y no se ejecuta ninguna migración de Supabase real (esto es solo el documento de propuesta).
