Amadeus — Inyección de políticas de viáticos en IA (BD + 2 prompts)
Diseño aprobado por Sergio el 2026-05-27 a alto nivel (tabla en BD, ambos prompts, contexto BD para AnalizadorTicket). D1-D5 cerradas 2026-05-28 (sección 5) → ready-to-build, sin decisiones pendientes. Parte del milestone de consumos (
amadeus-consumos-milestone.md), Bloque 0b, arranque tentativo 2026-06-04.
Contexto
Hoy hay 3 servicios IA en app/Services/Vision/:
AnalizadorTicketService(línea 23) — visión per-ticket aislada. Prompt embebido enSYSTEM_PROMPT(línea 109). NO conoce el viaje ni los otros consumos.ResumidorAuditoriaService(línea 21) — texto+JSON conflags_agregados. Prompt en línea 26.AuditorViajeService(línea 24) — orquestador: disparaAnalizarFotoConsumoJobpor consumo (línea 126), arma flags, llama Resumidor, persiste enauditorias_viaje.
Tabla auditorias_viaje existe (migration 2026_05_26_000508).
Texto-política (6 párrafos de Gustavo, $150/comida, $100 libres/día, agua/sueros separados, facturación obligatoria, 2 personas, fotos al teléfono de facturación, compras con previo aviso) ya capturado en chat de sesión 2026-05-27.
1. Tabla politicas_viaticos
Decisión: una sola fila vigente + tabla histórico (NO flag activa ni rangos vigente_desde/hasta). Razón: políticas escasas (1-3/año), edición simple en Nova, politica_hash en auditorias_viaje da traceability sin complejidad de rangos.
politicas_viaticos
├─ id
├─ texto (TEXT)
├─ resumen_estructurado (JSON nullable) — opcional, ver decisión 2 abajo
├─ vigente (boolean default true)
├─ hash_sha1 (char(40), boot() lo deriva del texto)
├─ notas_edicion (text nullable)
├─ editado_por_id (FK usuarios nullable)
├─ timestamps
politicas_viaticos_historial (append-only)
├─ id, politica_id, texto, hash_sha1, resumen_estructurado
├─ editado_por_id, notas_edicion, snapshot_at
Workflow: observer updating en PoliticaViaticos → INSERT en _historial con el row anterior antes del save.
Migration auditorias_viaje: ADD politica_hash CHAR(40) NULL + index.
Nova resource:
- Editables:
texto(Textarea grande),notas_edicion(Text). - Read-only:
hash_sha1,editado_por,created_at, HasMany al historial. - Permisos: gate
politicas-viaticos.edit(solo Gustavo/Sergio).
2. Contexto BD para el AnalizadorTicket
Decisión: prompt enriquecido con contexto pre-cargado, NO tool-calling agentic (tool-calling sumaría 2-4 round-trips → ~$0.05 extra/ticket vs $0.008 hoy = inaceptable).
JSON de contexto (~1.5-2K tokens extra/ticket):
{
"politica_vigente": { "texto": "...", "hash": "..." },
"viaje": {
"id": 123, "folio": "V-2026-045",
"sitio_principal": "Cd. Juárez",
"fechas": "2026-05-20 → 2026-05-23",
"dias_servicio": 4, "tipo_viaje": "instalación",
"integrantes": 2
},
"consumos_previos_mismo_usuario_este_viaje": [
{ "id": 88, "fecha_ticket": "2026-05-20 13:45", "categoria": "comida",
"monto": 145.0, "comercio": "La Cabaña", "ciudad": "Cd. Juárez",
"estado_ia": "ok", "concepto": "comida mediodía" }
],
"totales_dia_actual_usuario": {
"fecha": "2026-05-21",
"comidas_count": 2, "comidas_total": 243.0,
"libres_total": 0.0,
"limite_dia_segun_politica_estimado": 350.0
}
}
NO incluir: histórico cross-viaje (privacy + saturación), GPS raw, tickets de otros usuarios del equipo.
Cómo pasar al modelo:
- System: SYSTEM_PROMPT actualizado + instrucciones de uso de política (cacheable con
cache_control: ephemeral). - User message: bloque
image+ bloquetextcon política inline + JSON contexto. - Header beta
anthropic-beta: prompt-caching-2024-07-31. Reduce ~50-70% el input cost del system al re-enviar.
Tool schema (dictaminar_ticket) — campos nuevos:
violaciones_politica: array strings.comentario_politica: string corto.requiere_factura_segun_politica: bool.
Cache del contexto del viaje: vale en flujo “re-auditar viaje completo” (asegurarAnalisisIA), NO en flujo normal (un job background por foto). Patrón:
$contextoBase = Cache::remember(
"ctx_viaje_{$viaje->id}_{$usuario->id}", 300,
fn() => $this->armarContextoBase($viaje, $usuario)
);
Estimación costo:
- Hoy: ~$0.008/ticket.
- Con política + contexto sin cache: ~$0.015/ticket.
- Con cache del system: ~$0.010/ticket.
- 30 consumos/viaje × $0.015 = $0.45 (vs $0.24 hoy). Asumible.
Cap diario USD: config('analisis.cap_diario_usd') debe revisarse (decisión 3).
3. Inyección en Resumidor
Trivial. ResumidorAuditoriaService::generar():
$politica = PoliticaViaticos::vigente().- Concatenar al SYSTEM_PROMPT o pasar como bloque text del user message.
- Instrucción explícita: “Evalúa flags contra política. Lista violaciones observables en flags. NO inventes violaciones sin soporte de flag.”
- Persistir
politica_hashenauditorias_viaje.politica_hash.
Costo: ~+1K tokens input = $0.003/auditoría. Insignificante.
4. Migration plan (4 PRs)
PR 1 — Schema + Nova (bajo riesgo) — ✅ DEPLOYADO 2026-05-29 (commit 7e53af6 → prod + seed)
- Migrations:
politicas_viaticos,politicas_viaticos_historial,add_politica_hash_to_auditorias_viaje. ✅ aplicadas en dev. - Modelos
PoliticaViaticos+PoliticaViaticosHistorial. ✅ — el hash se deriva vía mutator de atributo, NO boot()/saving, porqueDatabaseSeederusaWithoutModelEventsy el evento no dispararía → violación NOT NULL. El observerupdating(historial) sí va como evento (en Nova los eventos corren normal).vigente()como helper estático. PoliticasViaticosSeeder✅ idempotente, con texto BORRADOR (derivado del resumen de 6 puntos). Falta el texto oficial — se edita en Nova, ese es el punto. (sin nombres propios de personas en el seed, por regla del hub)- Nova resources ✅ —
PoliticaViaticos(editable) +PoliticaViaticosHistorial(read-only, fuera de menú). Autorización víaPoliticaViaticosPolicy(auto-discovery):superadmin+ permiso existentegestionar_viaticos(reusado, no se creó gatepoliticas-viaticos.editnuevo). - Verificado: 7 tests unit verdes + Pint limpio + migrate/seed OK en dev. 97 tests del módulo verdes (3 fallos inventario preexistentes, ajenos).
- Texto oficial refinado y sembrado en prod 2026-05-29 (id=1 vigente, hash match). Sin nombres propios (regla del hub). Decisión de negocio abierta: prorrateo del consumo libre en salidas parciales (ver bitácora de
amadeus.md). - Pendiente: que Sergio pruebe edición en /nova (usuario con
gestionar_viaticos). - Sin cambios IA todavía. ✅
PR 2 — Resumidor (riesgo bajo) — ✅ DEPLOYADO 2026-05-29 (commit 2506bb2)
ResumidorAuditoriaServicecargaPoliticaViaticos::vigente(), la inyecta en el system prompt (instrucción D5: evaluar contra política, no inventar, solo señalar) y devuelvepolitica_hashen todas las rutas. ✅AuditorViajeServicepersiste el hash enauditorias_viaje.politica_hash(?? nulldefensivo). ✅- 5 tests con
Http::fake(inyección en payload + hash + solo-vigente + persistencia). ✅ No corrí la validación pagadaanalisis:auditar-viaje(disponible a pedido, ~$0.35).
PR 3 — Analizador con contexto (riesgo alto)
- Nuevo
ContextoTicketBuilderarma JSON de contexto. AnalizadorTicketService::analizar(string $ruta, ?array $contexto = null)con backward compat.AnalizarFotoConsumoJob::handle()construye contexto antes de llamar.- Tool schema gana
violaciones_politica,comentario_politica,requiere_factura_segun_politica. - Migration ADD a
consumos_analisis. - Validación crítica: correr en staging con viaje cerrado (Gustavo elige uno). Comparar veredictos pre/post política contra juicio humano. Documentar discrepancias antes de prender en prod.
PR 4 — Persistir hash + UI mínima
AuditorViajeService::auditar()leepolitica_hashy guarda enauditorias_viaje.politica_hash+flags_agregados.politica_hash.- UI en reporte: “Política aplicada: v
editada el ” + link Nova read-only.
Tests
- Unit
PoliticaViaticos: boot() hash + observer historial. - Unit
ContextoTicketBuilder: dado viaje + 3 consumos previos, devuelve estructura esperada. - Feature
AnalizarFotoConsumoJobconHttp::fake()— verificar payload Antigravity incluye política y contexto. - Regression
AuditorViajeServicecon fixture viaje completo. - NO tests contra Antigravity real (cuesta + flaky).
Estimación total
- PR 1: 2-3h.
- PR 2: 1-1.5h.
- PR 3: 4-6h (mayoría es ContextoTicketBuilder + tool schema + tests).
- PR 4: 1.5-2h.
- Validación staging: 1-2h manual.
- Total: 9.5-14.5h, distribuidas en 3-4 sesiones con Sergio validando entre cambios.
5. Decisiones — CERRADAS 2026-05-28 ✅
- Single-row con historial (NO rangos
vigente_desde/hasta). Una sola política vigente + tabla_historial. Si en el futuro hace falta “programar” un cambio, se reevalúa. - Solo texto (NO
resumen_estructuradoJSON). La política se pasa como prosa; el modelo razona sobre ella sin doble fuente de verdad. → La columnaresumen_estructuradose omite del schema inicial (o queda nullable sin uso). - Cap diario = $20 USD. Subir
config('analisis.cap_diario_usd')de $5 a $20 (cubre el ~50-80% de aumento de costo del contexto enriquecido con margen). - Sí, prompt caching (header beta
anthropic-beta: prompt-caching) desde PR 3, para ahorrar 50-70% en re-envíos del system. - Solo bandera, NUNCA bloqueo automático por violación de política. Una violación (ej. $200 en comida con límite $150) genera bandera/observación para el auditor humano, jamás rechazo automático. Coincide con la filosofía “no definitivo / apelable” del milestone de consumos. → En PR 3, las
violaciones_politicadel tool no disparanconsumos.estatus; solo se persisten y se muestran.
Archivos que se tocan
database/migrations/2026_05_27_*_create_politicas_viaticos_table.php(nuevo)database/migrations/2026_05_27_*_add_politica_hash_to_auditorias_viaje.php(nuevo)database/migrations/2026_05_27_*_add_politica_fields_to_consumos_analisis.php(nuevo, PR 3)database/seeders/PoliticasViaticosSeeder.php(nuevo)app/Models/PoliticaViaticos.php(nuevo)app/Models/PoliticaViaticosHistorial.php(nuevo)app/Nova/PoliticaViaticos.php(nuevo)app/Nova/PoliticaViaticosHistorial.php(nuevo)app/Services/Vision/ContextoTicketBuilder.php(nuevo, PR 3)app/Services/Vision/AnalizadorTicketService.php:178(firmaanalizar(), payload messages, tool schema, cache_control)app/Services/Vision/ResumidorAuditoriaService.php:44(cargar política, inyectar)app/Services/Vision/AuditorViajeService.php:43(persistir politica_hash; PR 3 también contexto)app/Jobs/AnalizarFotoConsumoJob.php:80(construir contexto antes de llamar)tests/Unit/Services/Vision/AnalizadorTicketServiceTest.php(nuevo/ampliar)tests/Feature/AuditarViajeTest.php(nuevo/ampliar)