Entities:
- Se conserva el mapa {campo: valor} en vez de aplanarlo. El pre-llenado de
formularios y el auto-registro de valores comparan la clave contra el nombre
del campo, asi que con la lista plana nunca se disparaban.
- entity_key acota cada dynamic_list a su propio campo: antes una finca llamada
"reposo" se comparaba contra grupos de mantenimiento y se descartaba ahi.
- Cada lista consume solo lo suyo, asi "finca reposo, grupo plateo" resuelve
las dos cosas en una frase.
Finca visible:
- Se guarda la etiqueta ademas del id al elegir una opcion.
- El footer de cada menu muestra la finca activa; sin finca queda el nombre de
la empresa, asi cat 1 no cambia. Los menus de botones ahora emiten footer.
- {finca} disponible en header, body y footer.
Aviso de comandos: los textos tras cada informe nombran menu, atras y salir.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
config_json guarda tanto la estructura del bot (menus, flujos, comandos) como
los ajustes que se editan desde el panel (ai_model, gemini_model, claude_model,
temperatura, prompt, whisper). El seed reescribia el JSON entero, asi que cada
corrida borraba lo segundo.
Ahora conserva toda clave de primer nivel que el seed no declara, e imprime
cuales conservo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
"informe de mantenimiento de plateo" no dice si quiere los mas atrasados o
todos los lotes, asi que entregar un PDF era adivinar. Ahora resuelve el grupo
por entity y muestra el submenu; solo va directo al informe cuando el usuario
lo nombra ("todos los lotes", "mas atrasados").
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
preservingReset dejaba current_node en NULL, que process() no distingue de una
sesion nueva: se redisparaba el flujo de bienvenida (ask_finca), skip_if_set lo
saltaba al menu principal y el paso de NLU nunca corria. Por eso "elegir otra
finca" o "informe de mantenimiento de plateo" devolvian el menu. Ahora queda el
centinela __greeted, que marca sesion iniciada.
api_base_url guardaba el Graph API de WhatsApp, pero WhatsAppSender lo tiene
hardcodeado y todos los demas consumidores esperan el ERP.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- "atras"/"volver"/"regresar" suben un nivel via mapa 'back' declarativo en el
seed, en vez de saltar siempre al menu principal.
- "finca"/"cambiar finca" como comandos directos a reset_finca.
- requires/resolver: si el NLU rutea a un informe sin el dato que necesita, el
bot lo pide y vuelve al informe original, no al submenu. Asi "informe de
mantenimiento de plateo" entrega el PDF sin pasos intermedios.
- ciclo_mantenimiento consulta el catalogo completo cuando hay entities: los
grupos fuera del top 3 tambien matchean.
- ask_finca con skip_if_set: deja de repreguntar la finca despues de cada
informe; para cambiarla esta reset_finca.
- Boton "Otro grupo" en el submenu de mantenimiento.
- Fix: el auto-select por entities no mergeaba per_type y mandaba a cat 3 al
menu equivocado al nombrar una finca.
validate_config.php verifica el grafo (back/resolver/botones/endpoints) y
test_navegacion.php recorre en seco los escenarios de ruteo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Cat 2 y cat 3: greeting '' (no null) para que greeting_flow: ask_finca
se dispare en el primer mensaje y el usuario seleccione su finca
- nlu_enabled: true en config root activa el routing inteligente
- NormalBot: step 6 llama AiBot::routeOrChat() antes del fallback_flow;
si retorna 'route' ejecuta el flow; si 'chat' responde en texto libre
- reset_finca: nuevo flow+función que limpia el meta de finca y relanza
ask_finca; el NLU puede enrutar "quiero cambiar de finca" aquí
- nlu_description en todos los flows consultables por el NLU; nlu_skip
en menus de navegación interna para no exponerlos al modelo
- AiBot::buildFlowCatalog: agrega upload_ciclo a isUpload para que cat 2
no vea flujos de subida en el catálogo del NLU
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
api_key contiene el token ERP de PALMAS360 configurado manualmente.
Se retira del ON DUPLICATE KEY UPDATE para que cada seed run no lo pise.
Solo aplica al INSERT inicial de una empresa nueva.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
La fecha que pasan estos nodos al endpoint es un punto de corte (hoy),
no un rango mensual. current_month enviaba fecha_desde=primer-día-mes
que el controller pasaba al modelo como referencia de días.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Eliminada entrada duplicada 'ciclo_mantenimiento' tipo menu (reemplazada
por el dynamic_list de abajo en el mismo array)
- Eliminado endpoint 'mantenimiento_top_texto_bot_dn' (nunca referenciado
por ningún nodo de flujo)
- Eliminado endpoint duplicado 'mantenimiento_todos_dn' (URL idéntica a
mantenimiento_todos_bot_dn, sin usos)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ciclo_mantenimiento → dynamic_list top 3 grupos más usados + "Otro grupo"
- "Otro grupo" → lista grupos 4-13 (append_options redirect)
- Tras seleccionar grupo → menú 3 botones: más largos, todos, histórico 30d
- Endpoints con {grupo_id} dinámico en URL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cada ciclo (cosecha, polinización, plagas, censo, palmarum,
tratamientos, mantenimiento) ahora presenta un menú de botones con:
1. Más largos — texto top 10 con fecha actual
2. Todos los lotes — PDF filtrado por finca
3. Histórico 30d — PDF matriz (excepto mantenimiento)
Se agregan endpoints *_todos_dn apuntando a ciclos_*_dn (PDF).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ask_finca como dynamic_list con meta_group=finca
- ciclos usan api_report con next_endpoint_key para texto+PDF
- produccion_total encadena texto → PDF
- ausentismos usan date_mode en vez de funcion inexistente
- URLs de endpoints sin {fecha_desde}/{fecha_hasta} (date_mode los agrega)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AiLogger.php: new service, inserts into ai_logs on every AI call
- ai_logs table: added to migrate.php (provider, model, call_type, tokens, duration_ms)
- AiBot: logs chat, media_response, nlu calls with timing and token counts
- MediaTranscriber: logs whisper, geminiAudio, openaiVision, geminiVision, claudeVision
- /admin/ai-logs: paginated table with provider/company filter
- Layout.php: added IA Logs nav link
- index.php: require AiLogger before AiBot; added GET /admin/ai-logs route
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Global settings: custom AI panel with OpenAI/Gemini/Claude each with
API key, model selector, and live test-connection button (no save needed)
- Per-company IA tab: same three providers + fallback to global when empty
- AiBot: reads per-company config_json first, falls back to env() global
- Added callClaude() for Anthropic Messages API
- More OpenAI models: gpt-4.1, gpt-4.1-mini
- test-ai endpoint: accepts provider/api_key/model from POST for live test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added gemini-2.5-flash to the admin panel selector (first/default option)
and updated the fallback default in AiBot and migrate seed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cuando un número pertenece a más de una empresa, el bot muestra un
menú interactivo "¿Con cuál empresa deseas comunicarte?" antes de
cualquier flujo normal. La selección se cachea 8h en la tabla
multi_company_sessions. Keywords de reinicio (salir, inicio, menú...)
limpian la sesión y vuelven a mostrar el pre-menú.
Números de una sola empresa: flujo intacto.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Settings: agrega Gemini al selector de proveedor, campos gemini_api_key y gemini_model
- Modelos: gemini-2.0-flash, gemini-1.5-flash, gemini-1.5-pro
- AiBot::callGemini(): llama generateContent API con system_instruction y historial
- migrate.php: seed defaults gemini_api_key y gemini_model
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- El número WA Business es global (configuración general), no por empresa
- OutboundWorker usa fallback a WHATSAPP_DEFAULT_PHONE_NUMBER_ID si la empresa no tiene
- CompanyRepository::save() ya no requiere phone_number_id
- phone_number_id permite DEFAULT '' en schema
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
El routing ahora es por número remitente (company_phones), no por phone_number_id.
phone_number_id solo se usa para enviar mensajes salientes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>