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 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-19 22:55:13 -05:00
co-authored by Claude Sonnet 4.6
parent ef799a4a0a
commit 5277d47c14
3 changed files with 57 additions and 2 deletions
+51
View File
@@ -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);