c786e471606f941e0b2acac47ff6a8bb423531bb
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
448b3d7c44 |
last_delivery: reusar dirección/zona del último pedido + retitular header
Persistencia: cuando confirm_order encola create_order y la orden tiene
shipping/zona/window, runTurn snapshot a context.last_delivery
{is_delivery, shipping_address, matched_zone, pending_location, delivery_window}.
pipeline preserva ese campo cuando resetea por stale (>24h), igual que
external_customer_id.
Agente lo ve via working_memory.last_delivery en cada turno.
Nueva tool reuse_last_delivery() que copia shipping_address + matched_zone
(+ pending_location) al order actual. Pickup-only sólo setea is_delivery=false.
systemPrompt: instrucciones para que el bot proactivamente ofrezca "te lo
mandamos al mismo lugar que la última vez (dirección, zona, $)" cuando
last_delivery existe y todavía no se eligió método de envío. Cliente puede
aceptar (reuse_last_delivery) o pedir otra dirección/retiro. delivery_window
NO se asume — siempre se vuelve a preguntar día/hora.
Smoke E2E: cliente recurrente con conversación stale 25h+
- 1ra orden: 1kg vacío → location → mar 12h → confirma.
- DB: context.last_delivery con zona Centro Test + dirección + ventana.
- 2da orden: "hola, 500g bondiola" → bot: "¿al mismo lugar (Av. Corrientes
1234, Centro, $1.500)?" → "sí" → "¿qué día? La última fue mar 12h, puede
ser otro" → "jueves 11hs" → orden cerrada sin re-pedir pin.
Header: "Bot Ops Console" → "Piaf Console" (index.html + ops-shell).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
aed79078de |
Zonas de delivery por polígono + horarios + location share por WhatsApp
Schema delivery_zones JSONB pasa a { zones: [{ id, name, polygon (GeoJSON),
delivery_cost, delivery_days, delivery_hours, min_order_amount, enabled }] }.
Tirado el modelo legacy de 48 barrios CABA hardcoded.
Backend:
- lib/geo.js: pointInPolygon + findZoneForPoint (ray casting, sin deps) + 9 tests.
- storeContext.checkAddressInZone ahora valida con lat/lng (necesita ubicación
del cliente; no geocodifica texto). buildZonesForLLM expone zonas resumidas
para el agente. summarizeDeliveryZones genera prosa con costo+días+horas.
- settingsRepo expone delivery_zones (bug pre-existente: nunca se devolvía).
- pipeline: inboundLocation ⇒ persistir order.pending_location; orderModel
acepta pending_location, matched_zone, delivery_window.
Intake:
- evolutionParser detecta locationMessage/liveLocationMessage (Baileys).
- evolution + sim handlers propagan inboundLocation al pipeline.
Agent (DeepSeek tool-calling):
- workingMemory inyecta store.delivery.zones[], store.pickup.schedule,
order.pending_location/matched_zone/delivery_window.
- setAddress: matchea zona con la ubicación pendiente; sin location devuelve
need_location y el LLM le pide el pin al cliente.
- setShipping: para delivery, indica requires_location si faltan coords.
- confirmOrder: valida día+hora contra zone.delivery_days/hours o pickup.schedule.
- nueva tool set_delivery_window(day, time?) para registrar el slot pedido.
- systemPrompt agrega instrucciones de envío/zonas + flujo location share.
Frontend:
- zone-map-editor: web component (light DOM) que carga Leaflet 1.9 +
leaflet-geoman lazy desde CDN y permite dibujar/editar polígonos sobre OSM.
API zones get/set, eventos change/select, paleta tomada de --chart-*.
- settings-crud: borrada lista CABA_BARRIOS, nueva UI con mapa al lado y
formulario por zona seleccionada (nombre, costo, días, horario start/end,
mínimo, habilitada). Save serializa al schema nuevo.
Smoke E2E manual:
- "1kg vacío + envío" → bot pide pin → location en Centro → matched_zone
$1.500, lun-sab 10-20h → "martes 12hs" → confirma orden con total + envío.
- Location en Palermo Test → mar/jue 11-19h respetado.
- Location fuera de zonas → "no llegamos a esa zona" + lista de zonas válidas.
- Domingo en Centro → rechazado con días disponibles.
157/157 tests verde.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
675a449ce8 |
D9 cleanup: borrar NLU/handlers/machine/replyTemplates legacy + activar agente + prompt caching
Después de validar el agente E2E con DeepSeek, el legacy queda muerto. 51 archivos cambiados (la mayoría borrados), el motor único es ahora el agente tool-calling. Borrados (~3500 LOC): - src/modules/3-turn-engine/nlu/ (router + 4 specialists + promptLoader + schemas + humanFallback + 6 default prompts) — reemplazado por systemPrompt.js - src/modules/3-turn-engine/stateHandlers/ (cart.js, cartHelpers.js, idle.js, shipping.js, utils.js, index.js) — reemplazado por tools del agente - src/modules/3-turn-engine/stateHandlers.js (re-export shim) - src/modules/3-turn-engine/openai.js (NLU clásico v3 + jsonCompletion + llmRecommendWriter + llmPlanningRecommend) — el agente crea su propio cliente OpenAI con tools nativos - src/modules/3-turn-engine/replyRewriter.js (rewriting LLM) — el agente escribe say directo, no necesita reescribir - src/modules/3-turn-engine/replyTemplates.js + test (rotación de variantes) — el agente varía naturalmente con tool_choice=required + temperature - src/modules/3-turn-engine/recommendations.js (cross-sell + planning) — el agente decide cuándo recomendar via tool calls - src/modules/3-turn-engine/machine/ (XState v5 completo + 19 tests) — reemplazado por la FSM podada en fsm.js + agent/runTurn.js - src/modules/3-turn-engine/turnEngineV3.helpers.js, .units.js, .pendingSelection.js (helpers del legacy) - src/modules/0-ui/controllers/prompts.js, handlers/prompts.js, db/promptsRepo.js — admin de prompts NLU (ya no hay prompts editables) - public/components/prompts-crud.js + nav entry en ops-shell turnEngineV3.js se reduce a un thin wrapper que exporta runTurnV3 (alias de runTurnAgent) + safeNextState (re-export de fsm.js). Mantiene la firma pública para no tocar pipeline.js. Activado: - AGENT_MAX_TOOL_CALLS=10 y AGENT_TURN_TIMEOUT_MS=25000 son los únicos flags. Borradas: USE_MODULAR_NLU, USE_XSTATE, XSTATE_SHADOW, XSTATE_SETTLE_MS, REPLY_REWRITER, REPLY_REWRITER_TIMEOUT_MS, TURN_ENGINE, AGENT_TURN_ENGINE, AGENT_TURN_ENGINE_SHADOW (el agente es default). Prompt caching DeepSeek: - systemPrompt.js: era función con storeName interpolado → ahora export const SYSTEM_PROMPT (100% estático). storeName se pasa por user message via working_memory.store.name. Cualquier cambio al system invalida cache, por eso es estático estricto. - runTurn.js: captura usage.prompt_cache_hit_tokens (DeepSeek) o prompt_tokens_details.cached_tokens (OpenAI compat) y suma a métricas. - /api/metrics/agent ahora reporta prompt_tokens_total, completion_tokens_total, prompt_cache_hit_tokens, cache_hit_ratio. - Smoke test 3 turnos: cache_hit_ratio = 0.72 (17664 cached / 24546 total prompt tokens). Saving directo en costo: ~$0.02/M cached vs $0.27/M no cached en DeepSeek. Tests: 148/148 (perdimos 90 tests del legacy XState/replyTemplates que ya no aplican). Sim flow E2E confirmado: hola → agent responde, multi-turn con cache caliente. Si más adelante hace falta volver al legacy: git revert este commit (c c9c69cf8 es el último estado verde con doble motor). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
03621f16f4 |
Redesign: agente tool-calling con DeepSeek (D2-D10 del plan)
Reemplaza el NLU rígido (intent+entities) por un agente LLM con tool-calling
que decide y muta estado en cada turno. Opt-in vía AGENT_TURN_ENGINE=1.
DeepSeek V4 (deepseek-chat) configurado como modelo (OpenAI-compatible).
Arquitectura nueva en src/modules/3-turn-engine/agent/:
- workingMemory.js: arma el JSON contextual que recibe el LLM cada turno
(cart, pending, last_shown_options, store, customer_profile, history,
preparsed quantity).
- systemPrompt.js: prompt estático ~70 líneas. Define rol + reglas duras +
cómo procesar mensajes + cómo escribir el say. Sin enumeración de intents.
- runTurn.js: loop de tool-calling con tool_choice="required". Cap 10 tool
calls / 20s timeout. Métricas in-memory.
- customerProfile.js: lookup de frequent_items en woo_orders_cache por
teléfono (chat_id → phone), top 5 últimos 6 meses. Cache 10 min.
- tools/schemas.js: 11 tools (search_catalog, add_to_cart, set_quantity,
select_candidate, remove_from_cart, set_shipping, set_address,
confirm_order, pause, escalate_to_human, say).
- tools/executor.js: validación Ajv + dispatch + observación al LLM.
woo_id se valida contra snapshot — si no existe el agente vuelve a
search_catalog (anti-halucinación).
- tools/searchCatalog.js: wrappea retrieveCandidates + fallback por
categoría usando jsonb_array_elements_text del snapshot. Persiste
last_shown_options automáticamente.
- tools/{addToCart, setQuantity, selectCandidate, removeFromCart,
setShipping, setAddress, confirmOrder, pause, escalateToHuman}.js:
side effects atómicos sobre el order.
- quantityParser.js (D1): determinístico, parsea fracciones, frases
compuestas (media docena, cuarto kilo), numéricos. 46 tests.
FSM extendida (fsm.js): nuevo estado PAUSED (TTL 7d, cart preservado,
"después te digo" → pause tool).
pipeline.js: TTL stale ahora 24h general, 7d si PAUSED, infinito si
AWAITING_HUMAN.
turnEngineV3.js: nuevas flags AGENT_TURN_ENGINE y AGENT_TURN_ENGINE_SHADOW.
Branch a runTurnAgent cuando full o corre en paralelo escribiendo diffs
estructurales en audit_log (entity_type='agent_shadow') para validar
paridad antes de flippar.
Endpoint nuevo: GET /api/metrics/agent → turns, avg_tool_calls, fallback
rate, escalations, pauses, orders_confirmed.
Smoke test E2E con DeepSeek real:
- "hola" → say (2.3s, 1 tool)
- "2kg de vacio" → search → add_to_cart → say (8.8s, 3 tools)
- "media docena de chorizos" → search → say con clarificación (10.3s, 4 tools)
- "listo" → say (3.3s, 1 tool)
- "retiro" → set_shipping → confirm → say (5.1s, 3 tools)
Cart final correcto: 2kg de Vacío. Estado: CART → SHIPPING.
Tests: 238/238 pasando.
D9 (cleanup legacy ~1200 LOC NLU/handlers/replyRewriter) DEFERRED:
se hace después de paridad shadow validada con tráfico real. Hoy
agente coexiste con legacy; default sigue siendo el motor V3.
Plan completo en ~/.claude/plans/ok-creo-que-tiene-humming-sutton.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|