'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'); $ctxId = (int)$botCtx['id']; $config = self::getConfig($company); $systemPrompt = $config['ai_prompt'] ?? self::defaultPrompt($company); ConversationContext::addAiMessage($ctxId, ['role' => 'user', 'content' => $input]); $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'] ?? '']), ]; } private static function callLlm(string $systemPrompt, int $ctxId, array $company): ?array { $provider = env('AI_PROVIDER', 'openai'); $history = ConversationContext::getAiHistory($ctxId); return match ($provider) { 'openai' => self::callOpenAI($systemPrompt, $history, $company), 'gemini' => self::callGemini($systemPrompt, $history), 'mock' => self::mockResponse($history), default => self::callOpenAI($systemPrompt, $history, $company), }; } private static function callOpenAI(string $systemPrompt, array $history, array $company): ?array { $apiKey = env('OPENAI_API_KEY', ''); if ($apiKey === '') { return null; } $model = env('OPENAI_MODEL', 'gpt-4o-mini'); $messages = [ ['role' => 'system', 'content' => $systemPrompt], ]; foreach ($history as $msg) { $messages[] = [ 'role' => $msg['role'] ?? 'user', 'content' => $msg['content'] ?? '', ]; } $payload = json_encode([ 'model' => $model, 'messages' => $messages, 'max_tokens' => (int)env('AI_MAX_TOKENS', '500'), 'temperature' => 0.7, ]); $ch = curl_init('https://api.openai.com/v1/chat/completions'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $apiKey, ], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($httpCode !== 200 || $response === false) { WpWebhook::log('ERROR', "OpenAI API error: {$error} HTTP:{$httpCode}"); return null; } $data = json_decode($response, true); $text = $data['choices'][0]['message']['content'] ?? null; return $text !== null ? ['content' => $text] : null; } private static function callGemini(string $systemPrompt, array $history): ?array { $apiKey = env('GEMINI_API_KEY', ''); if ($apiKey === '') return null; $model = env('GEMINI_MODEL', 'gemini-2.0-flash'); $maxTokens = (int)env('AI_MAX_TOKENS', '500'); // Gemini usa "contents" con roles "user"/"model" $contents = []; foreach ($history as $msg) { $role = ($msg['role'] ?? 'user') === 'assistant' ? 'model' : 'user'; $contents[] = ['role' => $role, 'parts' => [['text' => $msg['content'] ?? '']]]; } // Si el historial está vacío o el último es del model, Gemini requiere que el último sea user if (empty($contents)) { $contents[] = ['role' => 'user', 'parts' => [['text' => '']]]; } $payload = json_encode([ 'system_instruction' => ['parts' => [['text' => $systemPrompt]]], 'contents' => $contents, 'generationConfig' => ['maxOutputTokens' => $maxTokens, 'temperature' => 0.7], ]); $url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}"; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($httpCode !== 200 || $response === false) { WpWebhook::log('ERROR', "Gemini API error: {$error} HTTP:{$httpCode}"); return null; } $data = json_decode($response, true); $text = $data['candidates'][0]['content']['parts'][0]['text'] ?? null; return $text !== null ? ['content' => $text] : null; } private static function mockResponse(array $history): array { $last = end($history); $input = $last['content'] ?? ''; $responses = [ 'hola' => '¡Hola! ¿En qué puedo ayudarte hoy?', 'gracias' => '¡De nada! Si necesitas algo más, estoy aquí.', 'adios' => '¡Hasta luego! Que tengas un excelente día.', 'default' => "Gracias por tu mensaje. He recibido: \"{$input}\". Un asesor se pondrá en contacto contigo pronto.", ]; $normalized = mb_strtolower(trim($input)); $found = $responses['default']; foreach ($responses as $keyword => $response) { if (str_contains($normalized, $keyword)) { $found = $response; break; } } return ['content' => $found]; } private static function defaultPrompt(array $company): string { $name = $company['display_name'] ?? $company['name'] ?? 'la empresa'; return <<