fix: el NLU respeta los modulos del perfil

Los menus se filtraban por modulo pero el texto libre no: un perfil limitado a
pluviometria podia escribir "registrar ausentismo" y el NLU lo llevaba igual.
El mismo agujero que ya cerramos con las fincas.

moduloDeFlow() decide por convencion de nombre a que modulo pertenece cada
flow —'modulo' explicito manda— y se aplica en dos puntos: el catalogo que ve
el modelo llega filtrado, y el ruteo verifica igual la clave devuelta por si
el modelo alucina una que no le ofrecieron.

Lo permitido sigue directo: "subir pluviometria" cae en la fecha con un solo
mensaje, "subir labores diarias" arranca el flujo. Cinco casos nuevos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-19 22:39:45 -05:00
co-authored by Claude Sonnet 4.6
parent 9ae4b0dee3
commit 6c524a39ed
4 changed files with 95 additions and 6 deletions
+43 -2
View File
@@ -124,7 +124,8 @@ class NormalBot
// 6. NLU routing — si está habilitado, intenta mapear texto libre a un flow antes del fallback
if (!$suppressFallback && ($config['nlu_enabled'] ?? false)) {
$context['permission_type'] ??= (int)$permType;
$route = AiBot::routeOrChat($company, $context, $input);
$permitidosNlu = self::modulosPermitidos($company, $context['from'] ?? '');
$route = AiBot::routeOrChat($company, $context, $input, $permitidosNlu);
if ($route['action'] === 'route' && isset($route['key'])) {
$key = $route['key'];
// reset_finca: limpiar finca del meta y relanzar ask_finca
@@ -143,7 +144,12 @@ class NormalBot
$m['__nlu_entities'] = $route['entities'];
}
ConversationContext::updateMetadata($ctxId, $m);
if (isset($flows[$key])) {
// El catálogo ya viene filtrado, pero el modelo puede devolver
// una clave que no le ofrecieron: se verifica igual acá.
$moduloRuta = isset($flows[$key]) ? self::moduloDeFlow($key, $flows[$key]) : null;
$vetado = $permitidosNlu && $moduloRuta !== null && !in_array($moduloRuta, $permitidosNlu, true);
if (!$vetado && isset($flows[$key])) {
ConversationContext::updateNode($ctxId, $key);
return self::handleFlow($flows[$key], $context, $company, $ctxId, $menus, $key);
}
@@ -479,6 +485,41 @@ class NormalBot
];
}
/**
* A qué módulo pertenece un flow. Por convención de nombre para no tener
* que etiquetar los ~40 informes uno por uno; 'modulo' explícito manda.
* Null = sin módulo: navegación, finca, etc. — nunca se restringe.
*/
public static function moduloDeFlow(string $key, array $flow): ?string
{
if (!empty($flow['modulo'])) return $flow['modulo'];
if (str_starts_with($key, 'registrar_labor')) return 'labores';
$mapa = [
'registrar_pluviometria' => 'pluviometria',
'registrar_ausentismo' => 'ausentismos',
'registrar_ciclo' => 'ciclos',
'registrar_mantenimiento'=> 'mantenimiento',
'submenu_produccion' => 'produccion',
'submenu_ausentismos' => 'ausentismos',
];
if (isset($mapa[$key])) return $mapa[$key];
if (str_starts_with($key, 'produccion_')) return 'produccion';
if (str_starts_with($key, 'ausentismos_')) return 'ausentismos';
if (str_starts_with($key, 'ciclo_') || str_starts_with($key, 'submenu_ciclo')) return 'ciclos';
return null;
}
/** Módulos habilitados del número, aplanados. Vacío = sin restricción. */
private static function modulosPermitidos(array $company, string $waNumber): array
{
$perfil = self::perfilDelNumero($company, $waNumber);
if (!$perfil['modulos']) return [];
return array_merge(
(array)($perfil['modulos']['descarga'] ?? []),
(array)($perfil['modulos']['carga'] ?? [])
);
}
/**
* Quita del menu las filas de modulos que este numero no tiene habilitados.
* Sin modulos declarados no se filtra nada: la ausencia significa "todos",