fix: match fuzzy de label en dynamic_list y collecting mid-flow

- handleDynamicList: usa str_contains bilateral para que "mantenimiento plateo"
  coincida con label "Plateo" (el NLU suele incluir palabras extra)
- handleCollectingInput: si el texto transcrito no es un ID válido, intenta
  match fuzzy contra __label_map guardado en el estado collecting
- handleDynamicList: guarda __label_map en el estado collecting para
  que mid-flow audio pueda auto-seleccionar sin ID exacto

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-05 22:03:57 -05:00
co-authored by Claude Sonnet 4.6
parent 3d04be14bc
commit 976f61b682
+32 -9
View File
@@ -139,16 +139,31 @@ class NormalBot
$key = $col['meta_key'] ?? ''; $key = $col['meta_key'] ?? '';
$nextNode = $col['next_node'] ?? null; $nextNode = $col['next_node'] ?? null;
$validIds = $col['__valid_ids'] ?? null; $validIds = $col['__valid_ids'] ?? null;
$labelMap = $col['__label_map'] ?? [];
if ($validIds !== null && !in_array(trim($input), $validIds, true)) { $resolved = trim($input);
return self::sendText( if ($validIds !== null && !in_array($resolved, $validIds, true)) {
'⚠️ Por favor selecciona una opción válida de la lista.', // Intento de match por label (útil cuando viene texto transcrito de audio)
$context['from'], $company $inputNorm = self::normalize($resolved);
); $matched = null;
foreach ($labelMap as $label => $id) {
$lNorm = self::normalize((string)$label);
if ($lNorm === $inputNorm || str_contains($inputNorm, $lNorm) || str_contains($lNorm, $inputNorm)) {
$matched = (string)$id;
break;
}
}
if ($matched === null) {
return self::sendText(
'⚠️ Por favor selecciona una opción válida de la lista.',
$context['from'], $company
);
}
$resolved = $matched;
} }
$formData = $meta[$group] ?? []; $formData = $meta[$group] ?? [];
$formData[$key] = trim($input); $formData[$key] = $resolved;
$meta[$group] = $formData; $meta[$group] = $formData;
unset($meta['collecting']); unset($meta['collecting']);
ConversationContext::updateMetadata($ctxId, $meta); ConversationContext::updateMetadata($ctxId, $meta);
@@ -198,6 +213,12 @@ class NormalBot
$key = $flow['meta_key'] ?? ''; $key = $flow['meta_key'] ?? '';
$nextNode = $flow['next_node'] ?? null; $nextNode = $flow['next_node'] ?? null;
// Mapa label → id para match fuzzy (usado tanto en auto-select como en collecting)
$labelMap = [];
foreach ($items as $item) {
$labelMap[(string)($item[$labelField] ?? '')] = (string)($item[$valueField] ?? '');
}
// Si el NLU extrajo entities, intentar auto-seleccionar sin mostrar lista // Si el NLU extrajo entities, intentar auto-seleccionar sin mostrar lista
$entities = $meta['__nlu_entities'] ?? []; $entities = $meta['__nlu_entities'] ?? [];
unset($meta['__nlu_entities']); unset($meta['__nlu_entities']);
@@ -205,9 +226,10 @@ class NormalBot
$matchedId = null; $matchedId = null;
foreach ($entities as $ev) { foreach ($entities as $ev) {
$evNorm = self::normalize((string)$ev); $evNorm = self::normalize((string)$ev);
foreach ($items as $item) { foreach ($labelMap as $label => $id) {
if (self::normalize((string)($item[$labelField] ?? '')) === $evNorm) { $lNorm = self::normalize($label);
$matchedId = (string)($item[$valueField] ?? ''); if ($lNorm === $evNorm || str_contains($evNorm, $lNorm) || str_contains($lNorm, $evNorm)) {
$matchedId = $id;
break 2; break 2;
} }
} }
@@ -238,6 +260,7 @@ class NormalBot
'meta_key' => $key, 'meta_key' => $key,
'next_node' => $nextNode, 'next_node' => $nextNode,
'__valid_ids' => array_column($items, $valueField), '__valid_ids' => array_column($items, $valueField),
'__label_map' => $labelMap,
]; ];
ConversationContext::updateMetadata($ctxId, $meta); ConversationContext::updateMetadata($ctxId, $meta);
ConversationContext::updateNode($ctxId, 'collecting'); ConversationContext::updateNode($ctxId, 'collecting');