diff --git a/services/AiBot.php b/services/AiBot.php index b051741..800d755 100644 --- a/services/AiBot.php +++ b/services/AiBot.php @@ -428,9 +428,15 @@ PROMPT; $type = $flow['type'] ?? 'text'; $fn = $flow['function'] ?? ''; + // nlu_dir deja que un menú de categoría declare su dirección: por tipo + // no es ni subida ni descarga, y sin eso el filtro de permisos no lo + // tocaba y se le ofrecían informes a quien solo reporta. + $dirDeclarada = $flow['nlu_dir'] ?? ''; $isUpload = in_array($type, ['collect_and_post', 'collect_for_each', 'submit_form'], true) - || ($type === 'function' && $fn === 'upload_ciclo'); - $isDownload = ($type === 'function' && $fn === 'api_report'); + || ($type === 'function' && $fn === 'upload_ciclo') + || $dirDeclarada === 'subida'; + $isDownload = ($type === 'function' && $fn === 'api_report') + || $dirDeclarada === 'descarga'; // Permission filter: // type 1 = solo reporta (upload), type 2 = solo recibe (download), type 3 = ambos @@ -479,6 +485,8 @@ PROMPT; . "Pero elegí el key así:\n" . " · Dijo QUÉ informe (\"todos los lotes\", \"más atrasados\", \"histórico\") → ruteá a ese informe.\n" . " · Solo nombró el tema y el filtro → ruteá al menú del tema, para que él elija el informe.\n" + . " · Nombró solo una categoría (\"ausentismo\", \"producción\", \"sanidad\", \"ciclos\") → ruteá al\n" + . " menú de esa categoría. NUNCA respondas por chat pidiendo que aclare: el menú ya pregunta.\n" . "Ejemplos:\n" . " \"quiero el informe de mantenimiento de plateo\" → {\"action\":\"route\",\"key\":\"ciclo_mantenimiento\",\"entities\":{\"grupo\":\"plateo\"}}\n" . " \"mantenimiento de plateo, todos los lotes\" → {\"action\":\"route\",\"key\":\"ciclo_mantenimiento_todos\",\"entities\":{\"grupo\":\"plateo\"}}\n" diff --git a/services/NormalBot.php b/services/NormalBot.php index 05f0d7d..6302b84 100644 --- a/services/NormalBot.php +++ b/services/NormalBot.php @@ -159,6 +159,17 @@ class NormalBot if (!$inMenu) ConversationContext::updateNode($ctxId, null); return self::sendText($route['text'], $context['from'], $company); } + // El NLU no supo qué hacer. Sin este aviso, abajo aparece el menú + // principal sin explicación y se siente como que ignoró el mensaje. + $noEntendi = true; + } + + if (!empty($noEntendi)) { + WhatsAppSender::sendText( + $context['from'], + 'No estoy seguro de qué necesitas. ¿Alguna de estas opciones?', + (string)($context['phone_number_id'] ?? '') + ); } // Re-show current menu when NLU didn't route anything @@ -765,34 +776,6 @@ class NormalBot return self::foreachAskNext($meta['__foreach'], $context['from'], $company); } - /** - * Sube al principio lo que este usuario elige más seguido. El orden de PHP 8 - * es estable, así que los empates conservan el alfabético del catálogo. - * - * Es por usuario y no global a propósito: quien registra incapacidades - * registra sobre todo incapacidades, y eso le sirve más que un promedio. - */ - private static function ordenarPorUso(array $items, array $usos, string $valueField): array - { - if (!$usos) return $items; - usort($items, function ($a, $b) use ($usos, $valueField) { - $ua = $usos[(string)($a[$valueField] ?? '')] ?? 0; - $ub = $usos[(string)($b[$valueField] ?? '')] ?? 0; - return $ub <=> $ua; - }); - return $items; - } - - /** Registra la elección para que la próxima vez aparezca más arriba. */ - private static function contarUso(array $meta, string $campo, string $valor): array - { - $usos = $meta['__usos'][$campo] ?? []; - $usos[$valor] = ($usos[$valor] ?? 0) + 1; - arsort($usos); - $meta['__usos'][$campo] = array_slice($usos, 0, 20, true); - return $meta; - } - /** * Reemplaza {campo} con lo ya recolectado. Permite que una clave de endpoint * o una URL dependan de lo que el usuario eligió antes: los lotes a abrir @@ -1207,9 +1190,6 @@ class NormalBot if (($field['type'] ?? 'text') === 'select') { $cap['collected_labels'][$field['key']] = $cap['__selects'][$field['key']][$value] ?? $value; - if (!empty($field['frecuentes'])) { - $meta = self::contarUso($meta, $field['key'], $value); - } } $cap['index'] = $idx + 1; @@ -1346,11 +1326,6 @@ class NormalBot $lf = $field['label_field'] ?? 'label'; $items = self::fetchDynamicList($field['source_endpoint_key'] ?? '', $company, ['__ep_vars_combined' => $cap['collected'] ?? []]); - // Los que este usuario elige más seguido, primero - if (!empty($field['frecuentes']) && $ctxId > 0) { - $m = ConversationContext::getMetadata($ctxId); - $items = self::ordenarPorUso($items, $m['__usos'][$field['key']] ?? [], $vf); - } if (!empty($items)) { if ($ctxId > 0) { @@ -1671,7 +1646,7 @@ class NormalBot // finca y grupo_mant se conservan: los submenús ofrecen varios informes sobre // la misma selección, y perderla dejaba {finca_id}/{grupo_id} sin resolver. // __saludo también, si no se re-saluda después de cada informe. - foreach (['finca', 'grupo_mant', '__saludo', '__usos'] as $group) { + foreach (['finca', 'grupo_mant', '__saludo'] as $group) { if (isset($meta[$group])) $keep[$group] = $meta[$group]; } ConversationContext::reset($ctxId); diff --git a/setup/seed_palmas.php b/setup/seed_palmas.php index afba4a2..e0f7bd3 100644 --- a/setup/seed_palmas.php +++ b/setup/seed_palmas.php @@ -304,10 +304,10 @@ $configJson = [ // Menus de sección (nlu_skip: navegación interna, no exponer al NLU) // 'back' = a dónde sube el comando "atrás" desde cada nivel - 'submenu_produccion' => ['type' => 'menu', 'menu' => 'submenu_produccion', 'nlu_skip' => true, 'back' => 'show_main_menu'], - 'submenu_ciclos' => ['type' => 'menu', 'menu' => 'submenu_ciclos', 'nlu_skip' => true, 'back' => 'show_main_menu'], - 'submenu_ciclos_sanidad' => ['type' => 'menu', 'menu' => 'submenu_ciclos_sanidad', 'nlu_skip' => true, 'back' => 'submenu_ciclos'], - 'submenu_ausentismos' => ['type' => 'menu', 'menu' => 'submenu_ausentismos', 'nlu_skip' => true, 'back' => 'show_main_menu'], + 'submenu_produccion' => ['type' => 'menu', 'menu' => 'submenu_produccion', 'back' => 'show_main_menu', 'nlu_dir' => 'descarga', 'nlu_description' => 'Producción en general: totales facturados o detalle por lotes'], + 'submenu_ciclos' => ['type' => 'menu', 'menu' => 'submenu_ciclos', 'back' => 'show_main_menu', 'nlu_dir' => 'descarga', 'nlu_description' => 'Consultar ciclos de campo en general, sin decir cuál'], + 'submenu_ciclos_sanidad' => ['type' => 'menu', 'menu' => 'submenu_ciclos_sanidad', 'back' => 'submenu_ciclos', 'nlu_dir' => 'descarga', 'nlu_description' => 'Ciclos de sanidad en general: censo, plagas, palmarum, tratamiento'], + 'submenu_ausentismos' => ['type' => 'menu', 'menu' => 'submenu_ausentismos', 'back' => 'show_main_menu', 'nlu_dir' => 'descarga', 'nlu_description' => 'Consultar ausentismos: quién faltó hoy, esta semana o el mes'], // Ciclos — cada uno abre un sub-menú de 3 opciones 'ciclo_cosecha' => ['type' => 'menu', 'menu' => 'submenu_ciclo_cosecha', 'back' => 'submenu_ciclos', 'nlu_description' => 'Ver ciclos de cosecha (lotes más largos o todos)'], @@ -431,8 +431,6 @@ $configJson = [ 'source_endpoint_key' => 'novedades_ausentismo_dn', 'value_field' => 'id', 'label_field' => 'nombre', - // Los motivos que este usuario registra seguido, arriba - 'frecuentes' => true, ], [ 'key' => 'fecha_inicial', @@ -616,8 +614,22 @@ $configJson = [ 'greeting' => '', 'fallback' => null, 'commands' => ['menu' => 'show_main_menu'], - 'menus' => [], + 'menus' => [ + // Solo cat 3 puede consultar Y registrar, asi que "ausentismo" a + // secas es ambiguo unicamente para el. Cat 1 y cat 2 lo resuelve + // el filtro de permisos: cada uno ve una sola opcion. + 'menu_ausentismo_que' => ['type' => 'button', 'body' => '¿Qué necesitas con los ausentismos?', 'buttons' => [ + ['id' => 'submenu_ausentismos', 'title' => '📊 Consultar'], + ['id' => 'registrar_ausentismo', 'title' => '📝 Registrar'], + ]], + ], 'flows' => [ + 'ausentismo' => [ + 'type' => 'menu', + 'menu' => 'menu_ausentismo_que', + 'back' => 'show_main_menu', + 'nlu_description' => 'Ausentismos cuando no dice si quiere consultarlos o registrar uno', + ], 'show_main_menu' => ['type' => 'menu', 'menu' => 'menu_modo_trabajo'], 'descargar_informes' => ['type' => 'menu', 'menu' => 'show_menu_cat2', 'back' => 'show_main_menu'], 'enviar_informacion' => ['type' => 'menu', 'menu' => 'submenu_subir_ciclos', 'back' => 'show_main_menu'], diff --git a/setup/test_navegacion.php b/setup/test_navegacion.php index 60b1914..607754c 100644 --- a/setup/test_navegacion.php +++ b/setup/test_navegacion.php @@ -462,33 +462,52 @@ check('el POST manda un array de ids', array_map('strval', array_keys($alternar( check('accesible desde enviar información', $enMenu($config, '3', 'registrar_ciclo'), true); -echo "\nMotivos más usados primero\n"; +echo "\nCategorías ruteables por el NLU\n"; -check('el motivo aprende del uso', $campos['novedad_id']['frecuentes'] ?? false, true); - -/** Espejo de ordenarPorUso() + contarUso() */ -$contar = function (array $usos, string $v): array { - $usos[$v] = ($usos[$v] ?? 0) + 1; - arsort($usos); - return array_slice($usos, 0, 20, true); -}; -$ordenar = function (array $items, array $usos): array { - usort($items, fn($a, $b) => ($usos[$b['id']] ?? 0) <=> ($usos[$a['id']] ?? 0)); - return array_column($items, 'id'); +/** Espejo de buildFlowCatalog(): qué ve la IA según la categoría del usuario */ +$catalogo = function (array $config, int $perm): array { + $out = []; + foreach (flowsDe($config, (string)$perm) as $k => $f) { + $tipo = $f['type'] ?? 'text'; + $dir = $f['nlu_dir'] ?? ''; + $sube = in_array($tipo, ['collect_and_post', 'collect_for_each', 'submit_form'], true) || $dir === 'subida'; + $baja = ($tipo === 'function' && ($f['function'] ?? '') === 'api_report') || $dir === 'descarga'; + if ($sube && !in_array($perm, [1, 3], true)) continue; + if ($baja && !in_array($perm, [2, 3], true)) continue; + if (!empty($f['nlu_skip'])) continue; + if (($f['nlu_description'] ?? '') === '') continue; + $out[] = $k; + } + return $out; }; -$catalogo = [['id' => '25'], ['id' => '34'], ['id' => '35'], ['id' => '41']]; -check('sin historial respeta el catálogo', $ordenar($catalogo, []), ['25', '34', '35', '41']); +// Los tres casos que reporto la clienta: decir la categoria a secas +foreach (['submenu_ausentismos', 'submenu_produccion', 'submenu_ciclos_sanidad', 'submenu_ciclos'] as $k) { + check("la IA puede rutear a {$k}", in_array($k, $catalogo($config, 2), true), true); +} -$usos = []; -foreach (['35', '35', '35', '41'] as $v) $usos = $contar($usos, $v); -check('cuenta cada elección', $usos, ['35' => 3, '41' => 1]); -check('sube los más usados y desempata alfabético', - $ordenar($catalogo, $usos), ['35', '41', '25', '34']); +// El filtro de permisos resuelve la ambiguedad en cat 1 y cat 2 +$c1 = $catalogo($config, 1); +$c2 = $catalogo($config, 2); +check('cat 1 solo ve registrar ausentismo', + [in_array('registrar_ausentismo', $c1, true), in_array('submenu_ausentismos', $c1, true)], [true, false]); +check('cat 2 solo ve consultar ausentismos', + [in_array('registrar_ausentismo', $c2, true), in_array('submenu_ausentismos', $c2, true)], [true === false, true]); -// El contador vive en el metadata, que se limpia tras cada registro -check('los usos sobreviven al reset', - in_array('__usos', ['finca', 'grupo_mant', '__saludo', '__usos'], true), true); +// Cat 3 ve las dos, por eso necesita desambiguar +$c3 = $catalogo($config, 3); +check('cat 3 ve consultar y registrar', + [in_array('registrar_ausentismo', $c3, true), in_array('submenu_ausentismos', $c3, true)], [true, true]); +check('y tiene el menu que pregunta cual', in_array('ausentismo', $c3, true), true); + +// La navegacion pura sigue oculta: rutear ahi no aporta nada +foreach (['show_main_menu', 'descargar_informes', 'enviar_informacion'] as $k) { + check("{$k} sigue oculto al NLU", in_array($k, $catalogo($config, 3), true), false); +} + +// El orden por uso lo resuelve Palmas360, no el bot +check('el bot ya no reordena por su cuenta', + isset(flowsDe($config, '3')['registrar_ausentismo']['fields'][1]['frecuentes']), false); echo "\nask_finca\n"; $af = $flows['ask_finca'];