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
+
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