From 6f799c38299358ec3781a4d482b7370f9438cb28 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Sun, 28 Jun 2026 13:28:56 -0500 Subject: [PATCH] feat: media via AI, text via NormalBot only, ai_for_media toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Text/interactive/button messages → NormalBot only, never escalates to AI - Image/audio/video/document → AiBot.processMedia() if ai_for_media enabled, else falls back to category greeting menu - AiBot.processMedia() uses permission_type context so AI acknowledges uploads for perm 3 and redirects others to their menu - WpWebhook.handleMedia now calls BotRouter so the bot responds to media - Admin UI: checkbox toggle "Procesar imágenes y archivos con IA" in AI tab - categoryMenuFallback() extracted in BotRouter as shared helper Co-Authored-By: Claude Sonnet 4.6 --- admin/DashboardController.php | 11 +++++++ admin/v1/WpWebhook.php | 3 ++ public/index.php | 1 + services/AiBot.php | 55 +++++++++++++++++++++++++++++++++++ services/BotRouter.php | 35 +++++++++++++++------- 5 files changed, 95 insertions(+), 10 deletions(-) diff --git a/admin/DashboardController.php b/admin/DashboardController.php index be801a2..fdda3ad 100644 --- a/admin/DashboardController.php +++ b/admin/DashboardController.php @@ -2362,6 +2362,7 @@ HTML; $greetingVal = self::h($config['greeting'] ?? ''); $fallbackVal = self::h($config['fallback'] ?? ''); $aiPromptVal = self::h($config['ai_prompt'] ?? ''); + $aiForMediaChecked = ($config['ai_for_media'] ?? true) ? 'checked' : ''; $aiModelVal = self::h($config['ai_model'] ?? 'gpt-4o-mini'); $aiTempVal = self::h((string)($config['ai_temperature'] ?? 0.7)); $aiProviderVal = self::h($config['ai_provider'] ?? ''); @@ -2618,6 +2619,16 @@ HTML;
Instrucciones que define el comportamiento de la IA
+
+ +
+ Cuando el usuario envía imagen, audio o documento, la IA analiza el contenido según los permisos del usuario. + Si está desactivado, el bot muestra el menú de categoría directamente. +
+
diff --git a/admin/v1/WpWebhook.php b/admin/v1/WpWebhook.php index 4b3d9ba..bf6eb49 100644 --- a/admin/v1/WpWebhook.php +++ b/admin/v1/WpWebhook.php @@ -351,6 +351,9 @@ class WpWebhook self::saveWebhookLog('messages', $ctx['from'], $ctx['name'], $type, $preview); self::saveConversation($ctx, $preview, $mediaId); self::forwardToCompany($ctx, $preview, $mediaId); + if (self::$currentCompany !== null) { + BotRouter::route(self::$currentCompany, $ctx, $caption, $type); + } } private static function handleLocation(array $msg, array $ctx): void diff --git a/public/index.php b/public/index.php index 02d7246..2e9e46e 100644 --- a/public/index.php +++ b/public/index.php @@ -409,6 +409,7 @@ $routes = [ if ($aiTemp !== '') $config['ai_temperature'] = (float)$aiTemp; $aiProvider = trim($_POST['ai_provider'] ?? ''); if ($aiProvider !== '') $config['ai_provider'] = $aiProvider; + $config['ai_for_media'] = isset($_POST['ai_for_media']); // Per-category menus $perTypeMenus = $_POST['per_type_menu'] ?? []; diff --git a/services/AiBot.php b/services/AiBot.php index f4dd86c..a4e4712 100644 --- a/services/AiBot.php +++ b/services/AiBot.php @@ -3,6 +3,54 @@ declare(strict_types=1); class AiBot { + public static function processMedia(array $company, array $context, string $mediaType, string $caption): ?array + { + $permType = (int)($context['permission_type'] ?? 1); + $config = self::getConfig($company); + + $mediaLabel = match ($mediaType) { + 'image' => 'una imagen', + 'audio' => 'un mensaje de audio', + 'video' => 'un video', + 'document' => 'un documento', + 'sticker' => 'un sticker', + default => 'un archivo', + }; + + $canUpload = $permType === 3; + $permDesc = $canUpload + ? 'El usuario tiene permisos para subir información y reportes.' + : 'El usuario solo puede recibir información o descargar informes, NO puede subir archivos.'; + + $captionNote = $caption !== '' && !str_starts_with($caption, '[') ? " con el texto: \"{$caption}\"" : ''; + + $basePrompt = $config['ai_prompt'] ?? self::defaultMediaPrompt($company); + $systemPrompt = $basePrompt . "\n\n{$permDesc}\n\nReglas:\n" + . "- Si el usuario puede subir: confirma recepción de {$mediaLabel} e indica que será procesado.\n" + . "- Si NO puede subir: explica brevemente y dile que use el menú de opciones.\n" + . "- Respuesta máx 2 oraciones. Sin saludo largo."; + + $userMessage = "El usuario envió {$mediaLabel}{$captionNote}."; + + $botCtx = ConversationContext::getOrCreate((int)$company['id'], $context['from'], 'ai'); + $ctxId = (int)$botCtx['id']; + + ConversationContext::addAiMessage($ctxId, ['role' => 'user', 'content' => $userMessage]); + $result = self::callLlm($systemPrompt, $ctxId, $company); + if ($result === null) { + return null; + } + + ConversationContext::addAiMessage($ctxId, ['role' => 'assistant', 'content' => $result['content'] ?? '']); + + return [ + 'action' => 'send', + 'type' => 'text', + 'to' => $context['from'], + 'payload' => json_encode(['text' => $result['content'] ?? '']), + ]; + } + public static function process(array $company, array $context, string $input): ?array { $botCtx = ConversationContext::getOrCreate((int)$company['id'], $context['from'], 'ai'); @@ -189,6 +237,13 @@ Mantén las respuestas concisas (máximo 3 párrafos). PROMPT; } + private static function defaultMediaPrompt(array $company): string + { + $name = $company['display_name'] ?? $company['name'] ?? 'la empresa'; + return "Eres el asistente virtual de {$name}. El usuario te acaba de enviar un archivo multimedia. " + . "Responde de forma breve y apropiada según los permisos del usuario. Sin saludos largos."; + } + private static function getConfig(array $company): array { $json = $company['config_json'] ?? ''; diff --git a/services/BotRouter.php b/services/BotRouter.php index f2cde89..b7f8bab 100644 --- a/services/BotRouter.php +++ b/services/BotRouter.php @@ -100,31 +100,46 @@ class BotRouter return AiBot::process($company, $context, $input); } + private static function isMediaType(string $inputType): bool + { + return in_array($inputType, ['image', 'audio', 'video', 'document', 'sticker'], true); + } + private static function runHybridBot(array $company, array $context, string $input, string $inputType): ?array { - $response = self::runNormalBot($company, $context, $input, $inputType); + $config = self::getConfig($company); + // Media (image/audio/video/document) → AI handles it if enabled + if (self::isMediaType($inputType)) { + $aiEnabled = (bool)($config['ai_for_media'] ?? true); + if (!$aiEnabled) { + return self::categoryMenuFallback($company, $context, $config); + } + return AiBot::processMedia($company, $context, $inputType, $input); + } + + // Text / interactive / button → NormalBot only, never AI + $response = self::runNormalBot($company, $context, $input, $inputType); if ($response !== null) { return $response; } - // Only escalate to AI if bot type truly needs it; avoid AI for menu-driven bots - $config = self::getConfig($company); - $permType = (string)($context['permission_type'] ?? 1); - $perType = $config['per_type'][$permType] ?? []; + // NormalBot returned null → show the category greeting menu as fallback + return self::categoryMenuFallback($company, $context, $config); + } - // If a greeting_menu is configured for this category, use it as fallback instead of AI + private static function categoryMenuFallback(array $company, array $context, array $config): ?array + { + $permType = (string)($context['permission_type'] ?? 1); + $perType = $config['per_type'][$permType] ?? []; $greetingMenuKey = $perType['greeting_menu'] ?? null; if ($greetingMenuKey !== null) { $menus = array_merge($config['menus'] ?? [], $perType['menus'] ?? []); if (isset($menus[$greetingMenuKey])) { - self::log("NormalBot no manejó '{$input}' — mostrando menú de categoría en lugar de IA"); return NormalBot::buildGreetingMenu($menus[$greetingMenuKey], $context['from'], $company); } } - - self::log("NormalBot no manejó '{$input}', escalando a IA"); - return self::runAiBot($company, $context, $input); + return null; } private static function shouldSkipBot(array $company, array $context): bool