feat: one-shot audio entity extraction y pre-fill de formularios
- AiBot: NLU ahora extrae entities (finca, valor, etc.) del audio - BotRouter: guarda __nlu_entities en metadata antes de enrutar - NormalBot: handleCollectAndPost pre-rellena campos desde entities y capAskNext salta los ya completados - NormalBot: handleCollectForEach registra ítem directo si entities trae finca+valor coincidente, sin iterar todo el listado - BotRouter: mid-flow audio va a NormalBot directamente (no NLU) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
d0c1b1d3be
commit
54a93adea9
+106
-2
@@ -317,6 +317,69 @@ class NormalBot
|
||||
);
|
||||
}
|
||||
|
||||
// Si el NLU extrajo finca+valor, registrar solo ese ítem directamente
|
||||
$entities = $meta['__nlu_entities'] ?? [];
|
||||
unset($meta['__nlu_entities']);
|
||||
if (!empty($entities)) {
|
||||
$valueField = $flow['value_field'] ?? 'id';
|
||||
$labelField = $flow['label_field'] ?? 'name';
|
||||
$entityVal = null;
|
||||
$matchedId = null;
|
||||
$matchedLabel = null;
|
||||
|
||||
// Buscar valor numérico en entities
|
||||
foreach ($entities as $ek => $ev) {
|
||||
if (in_array(self::normalize($ek), ['valor','value','mm','cantidad','qty'], true) && is_numeric(str_replace(',', '.', (string)$ev))) {
|
||||
$entityVal = str_replace(',', '.', (string)$ev);
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar finca que coincida en entities
|
||||
foreach ($entities as $ek => $ev) {
|
||||
if (in_array(self::normalize($ek), ['valor','value','mm','cantidad','qty'], true)) continue;
|
||||
$evNorm = self::normalize((string)$ev);
|
||||
foreach ($items as $item) {
|
||||
if (self::normalize((string)($item[$labelField] ?? '')) === $evNorm) {
|
||||
$matchedId = (string)($item[$valueField] ?? '');
|
||||
$matchedLabel = (string)($item[$labelField] ?? '');
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchedId !== null && $entityVal !== null) {
|
||||
ConversationContext::updateMetadata($ctxId, $meta);
|
||||
ConversationContext::reset($ctxId);
|
||||
// POST directo para ese ítem
|
||||
$epKey = $flow['endpoint_key'] ?? '';
|
||||
$stmt = db()->prepare("SELECT url, method FROM company_endpoints WHERE company_id=? AND endpoint_key=? AND is_active=1 LIMIT 1");
|
||||
$stmt->execute([(int)$company['id'], $epKey]);
|
||||
$ep = $stmt->fetch();
|
||||
if ($ep && !empty($ep['url'])) {
|
||||
$url = self::buildUrl($ep['url'], $company);
|
||||
$apiKey = $company['api_key'] ?? '';
|
||||
$body = json_encode([$valueField => $matchedId, 'valor' => $entityVal]);
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $body,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json', "Authorization: Bearer {$apiKey}", "X-API-Key: {$apiKey}"],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
]);
|
||||
$resp = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($code >= 200 && $code < 300) {
|
||||
$successText = $flow['success_text'] ?? '✅ Datos registrados correctamente.';
|
||||
return self::sendText("✅ *{$matchedLabel}*: {$entityVal} registrado.\n{$successText}", $context['from'], $company);
|
||||
}
|
||||
}
|
||||
return self::sendText('⚠️ No se pudo registrar. Intenta de nuevo o escribe *menu*.', $context['from'], $company);
|
||||
}
|
||||
ConversationContext::updateMetadata($ctxId, $meta);
|
||||
}
|
||||
|
||||
$meta['__foreach'] = [
|
||||
'items' => $items,
|
||||
'index' => 0,
|
||||
@@ -474,10 +537,34 @@ class NormalBot
|
||||
}
|
||||
|
||||
$meta = ConversationContext::getMetadata($ctxId);
|
||||
$entities = $meta['__nlu_entities'] ?? [];
|
||||
unset($meta['__nlu_entities']);
|
||||
|
||||
$collected = [];
|
||||
$startIdx = 0;
|
||||
if (!empty($entities)) {
|
||||
foreach ($fields as $i => $field) {
|
||||
$fKey = $field['key'] ?? '';
|
||||
$fLabel = $field['label'] ?? '';
|
||||
foreach ($entities as $ek => $ev) {
|
||||
if (self::normalize($ek) === self::normalize($fKey) ||
|
||||
self::normalize($ek) === self::normalize($fLabel)) {
|
||||
$collected[$fKey] = (string)$ev;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Advance index past consecutive pre-filled leading fields
|
||||
while ($startIdx < count($fields) && isset($collected[$fields[$startIdx]['key'] ?? ''])) {
|
||||
$startIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
$meta['__cap'] = [
|
||||
'fields' => $fields,
|
||||
'index' => 0,
|
||||
'collected' => [],
|
||||
'index' => $startIdx,
|
||||
'collected' => $collected,
|
||||
'prefilled' => $collected,
|
||||
'endpoint_key' => $flow['endpoint_key'] ?? '',
|
||||
'success_text' => $flow['success_text'] ?? '✅ Datos registrados correctamente.',
|
||||
'confirm' => (bool)($flow['confirm'] ?? true),
|
||||
@@ -629,6 +716,23 @@ class NormalBot
|
||||
|
||||
private static function capAskNext(array $cap, string $to, array $company, int $ctxId = 0): ?array
|
||||
{
|
||||
// Saltar campos ya pre-llenados (no contiguos)
|
||||
$prefilled = $cap['prefilled'] ?? [];
|
||||
while ((int)$cap['index'] < count($cap['fields'])) {
|
||||
$f = $cap['fields'][(int)$cap['index']];
|
||||
if (isset($prefilled[$f['key'] ?? '']) && !isset($cap['collected'][$f['key'] ?? ''])) {
|
||||
$cap['collected'][$f['key']] = $prefilled[$f['key']];
|
||||
$cap['index']++;
|
||||
if ($ctxId > 0) {
|
||||
$m = ConversationContext::getMetadata($ctxId);
|
||||
$m['__cap'] = $cap;
|
||||
ConversationContext::updateMetadata($ctxId, $m);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$idx = (int)$cap['index'];
|
||||
$total = count($cap['fields']);
|
||||
$field = $cap['fields'][$idx];
|
||||
|
||||
Reference in New Issue
Block a user