From 5277d47c140673382bdfeb23225d8226986e5900 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:55:13 -0500 Subject: [PATCH] feat: soporte Google Gemini como proveedor de IA - Settings: agrega Gemini al selector de proveedor, campos gemini_api_key y gemini_model - Modelos: gemini-2.0-flash, gemini-1.5-flash, gemini-1.5-pro - AiBot::callGemini(): llama generateContent API con system_instruction y historial - migrate.php: seed defaults gemini_api_key y gemini_model Co-Authored-By: Claude Sonnet 4.6 --- admin/DashboardController.php | 6 +++-- services/AiBot.php | 51 +++++++++++++++++++++++++++++++++++ setup/migrate.php | 2 ++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/admin/DashboardController.php b/admin/DashboardController.php index 911cb0c..13a9455 100644 --- a/admin/DashboardController.php +++ b/admin/DashboardController.php @@ -1675,9 +1675,11 @@ HTML; ['key' => 'verify_hmac_signature', 'label' => 'Validar firma HMAC de Meta', 'type' => 'checkbox', 'placeholder' => ''], ], 'Inteligencia Artificial' => [ - ['key' => 'ai_provider', 'label' => 'Proveedor', 'type' => 'select', 'options' => ['mock' => 'Mock (simulado)', 'openai' => 'OpenAI']], + ['key' => 'ai_provider', 'label' => 'Proveedor', 'type' => 'select', 'options' => ['mock' => 'Mock (simulado)', 'openai' => 'OpenAI', 'gemini' => 'Google Gemini']], ['key' => 'openai_api_key', 'label' => 'OpenAI API Key', 'type' => 'password', 'placeholder' => 'sk-...'], - ['key' => 'openai_model', 'label' => 'Modelo', 'type' => 'select', 'options' => ['gpt-4o-mini' => 'GPT-4o Mini', 'gpt-4o' => 'GPT-4o', 'gpt-3.5-turbo' => 'GPT-3.5 Turbo']], + ['key' => 'openai_model', 'label' => 'Modelo OpenAI', 'type' => 'select', 'options' => ['gpt-4o-mini' => 'GPT-4o Mini', 'gpt-4o' => 'GPT-4o', 'gpt-3.5-turbo' => 'GPT-3.5 Turbo']], + ['key' => 'gemini_api_key', 'label' => 'Google Gemini API Key', 'type' => 'password', 'placeholder' => 'AIza...'], + ['key' => 'gemini_model', 'label' => 'Modelo Gemini', 'type' => 'select', 'options' => ['gemini-2.0-flash' => 'Gemini 2.0 Flash', 'gemini-1.5-flash' => 'Gemini 1.5 Flash', 'gemini-1.5-pro' => 'Gemini 1.5 Pro']], ['key' => 'ai_max_tokens', 'label' => 'Máximo de tokens', 'type' => 'number', 'placeholder' => '500'], ['key' => 'ai_default_prompt', 'label' => 'System Prompt por defecto', 'type' => 'textarea', 'placeholder' => 'Eres un asistente...'], ], diff --git a/services/AiBot.php b/services/AiBot.php index 34739c6..2f09771 100644 --- a/services/AiBot.php +++ b/services/AiBot.php @@ -36,6 +36,7 @@ class AiBot return match ($provider) { 'openai' => self::callOpenAI($systemPrompt, $history, $company), + 'gemini' => self::callGemini($systemPrompt, $history), 'mock' => self::mockResponse($history), default => self::callOpenAI($systemPrompt, $history, $company), }; @@ -104,6 +105,56 @@ class AiBot return ['content' => $text]; } + 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) { + return ['content' => 'Lo siento, tengo problemas para procesar tu mensaje. Por favor intenta de nuevo.']; + } + + $data = json_decode($response, true); + $text = $data['candidates'][0]['content']['parts'][0]['text'] ?? null; + + return $text !== null ? ['content' => $text] : ['content' => 'No pude generar una respuesta. ¿Puedes reformular tu pregunta?']; + } + private static function mockResponse(array $history): array { $last = end($history); diff --git a/setup/migrate.php b/setup/migrate.php index c72ae30..3a89a82 100644 --- a/setup/migrate.php +++ b/setup/migrate.php @@ -199,6 +199,8 @@ $seedSettings = [ 'ai_provider' => 'mock', 'openai_api_key' => '', 'openai_model' => 'gpt-4o-mini', + 'gemini_api_key' => '', + 'gemini_model' => 'gemini-2.0-flash', 'ai_max_tokens' => '500', 'ai_default_prompt' => 'Eres un asistente virtual de Palmas360. Responde de forma amable y profesional.', ];