fix: decir una categoria a secas devolvia el menu principal
Los submenus estaban marcados nlu_skip para que la IA no ruteara a navegacion, pero eso dejo solo las hojas en el catalogo: "ausentismo" tenia cuatro candidatos y ninguno se llamaba asi, "sanidad" ninguno, "produccion" dos. Como no podia elegir, caia al fallback y aparecia el menu principal, que se siente como que ignoro el mensaje. Las cuatro categorias vuelven a ser ruteables. Para que el filtro de permisos las alcance —por tipo un menu no es ni subida ni descarga— se agrega nlu_dir, con lo que cat 1 solo ve registrar y cat 2 solo consultar, y la ambiguedad se resuelve sola. Cat 3 ve las dos, asi que tiene un menu que pregunta cual. Cuando aun asi no rutea, ahora avisa antes de mostrar el menu. Se quita el aprendizaje por usuario: el orden por uso lo resuelve Palmas360 sobre los registros reales, y dos criterios compitiendo sobraba. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
2d92867431
commit
6a6f2ef44a
+10
-2
@@ -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"
|
||||
|
||||
+12
-37
@@ -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);
|
||||
|
||||
+19
-7
@@ -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'],
|
||||
|
||||
+41
-22
@@ -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'];
|
||||
|
||||
Reference in New Issue
Block a user