feat: NLU routing + audio/image transcription via AI
- NluRouter in AiBot: builds permission-filtered flow catalog, asks AI to route user intent to exact flow key or return chat response - MediaTranscriber: downloads WA media, transcribes audio (Whisper/Gemini), extracts image intent (OpenAI/Gemini/Claude vision) - BotRouter hybrid: text → NormalBot → NLU fallback if no match; audio/image → transcribe → NLU route; sends transcript preview first - Admin IA tab: NLU checkbox with description - Per-company nlu flag saved in config_json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
2b859e3dc0
commit
9e36a965c9
+56
-5
@@ -107,24 +107,75 @@ class BotRouter
|
||||
|
||||
private static function runHybridBot(array $company, array $context, string $input, string $inputType): ?array
|
||||
{
|
||||
$config = self::getConfig($company);
|
||||
$config = self::getConfig($company);
|
||||
$nluEnabled = (bool)($config['nlu'] ?? false);
|
||||
$aiEnabled = (bool)($config['ai_for_media'] ?? true);
|
||||
|
||||
// Media (image/audio/video/document) → AI handles it if enabled
|
||||
// ── Media (audio/imagen) ──────────────────────────────────────────────
|
||||
if (self::isMediaType($inputType)) {
|
||||
$aiEnabled = (bool)($config['ai_for_media'] ?? true);
|
||||
if (!$aiEnabled) {
|
||||
return self::categoryMenuFallback($company, $context, $config);
|
||||
}
|
||||
|
||||
if ($nluEnabled && in_array($inputType, ['audio', 'image'], true)) {
|
||||
$text = $inputType === 'audio'
|
||||
? MediaTranscriber::transcribeAudio($input, $company)
|
||||
: MediaTranscriber::extractFromImage($input, $company);
|
||||
|
||||
if ($text !== null && trim($text) !== '') {
|
||||
self::log("NLU media [{$inputType}]: texto extraído → \"{$text}\"");
|
||||
// Confirmar al usuario lo que entendió
|
||||
$prefix = $inputType === 'audio' ? '🎤 Transcripción' : '🖼️ Imagen analizada';
|
||||
WhatsAppSender::sendText(
|
||||
$context['from'],
|
||||
"{$prefix}: _{$text}_",
|
||||
$context['phone_number_id'] ?? ''
|
||||
);
|
||||
return self::runNluOnText($company, $context, $text, $config);
|
||||
}
|
||||
}
|
||||
|
||||
return AiBot::processMedia($company, $context, $inputType, $input);
|
||||
}
|
||||
|
||||
// Text / interactive / button → NormalBot only, never AI
|
||||
// ── Texto / interactivo: NormalBot primero ────────────────────────────
|
||||
$response = self::runNormalBot($company, $context, $input, $inputType);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// NormalBot returned null → show the category greeting menu as fallback
|
||||
// NormalBot no reconoció → NLU si está activo, si no menú categoría
|
||||
if ($nluEnabled && $inputType === 'text') {
|
||||
return self::runNluOnText($company, $context, $input, $config);
|
||||
}
|
||||
|
||||
return self::categoryMenuFallback($company, $context, $config);
|
||||
}
|
||||
|
||||
private static function runNluOnText(array $company, array $context, string $input, array $config): ?array
|
||||
{
|
||||
$result = AiBot::routeOrChat($company, $context, $input);
|
||||
|
||||
if ($result['action'] === 'route') {
|
||||
$key = $result['key'];
|
||||
$botCtx = ConversationContext::getOrCreate((int)$company['id'], $context['from'], 'normal');
|
||||
ConversationContext::updateNode((int)$botCtx['id'], $key);
|
||||
self::log("NLU: {$context['from']} → flow [{$key}]");
|
||||
// Ejecutar con input vacío — NormalBot retoma current_node
|
||||
return NormalBot::process($company, $context, '');
|
||||
}
|
||||
|
||||
$text = trim($result['text'] ?? '');
|
||||
if ($text !== '') {
|
||||
self::log("NLU: {$context['from']} → chat libre");
|
||||
return [
|
||||
'action' => 'send',
|
||||
'type' => 'text',
|
||||
'to' => $context['from'],
|
||||
'payload' => json_encode(['text' => $text]),
|
||||
];
|
||||
}
|
||||
|
||||
return self::categoryMenuFallback($company, $context, $config);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user