apiKey = ChatConfig::get('gemini_api_key', ''); $this->model = ChatConfig::get('gemini_model', 'gemini-2.0-flash'); } public function responder( string $texto, array $contexto, ?int $usuarioId = null, string $canal = 'telegram' ): array { $vacio = ['respuesta' => null, 'accion' => null, 'accion_data' => []]; if (! $this->apiKey) { return $vacio; } $body = [ 'contents' => [[ 'parts' => [['text' => $this->buildPrompt($texto, $contexto)]], ]], 'generationConfig' => [ 'temperature' => 0.5, 'maxOutputTokens' => 512, 'thinkingConfig' => ['thinkingBudget' => 0], ], ]; $inicio = microtime(true); $response = null; $lastError = ''; foreach (['v1', 'v1beta'] as $ver) { $url = "https://generativelanguage.googleapis.com/{$ver}/models/{$this->model}:generateContent"; try { $resp = Http::withHeaders(['Content-Type' => 'application/json']) ->timeout(12) ->post("{$url}?key={$this->apiKey}", $body); if ($resp->successful()) { $response = $resp; break; } $lastError = "[{$ver}] " . ($resp->json('error.message') ?? substr($resp->body(), 0, 200)); } catch (\Throwable $e) { $lastError = "[{$ver}] " . $e->getMessage(); } } $tiempoMs = (int) ((microtime(true) - $inicio) * 1000); if (! $response) { Log::warning('[GeminiAgent] API failed: ' . $lastError); return $vacio; } $parts = $response->json('candidates.0.content.parts', []); $text = implode("\n", array_column( array_filter($parts, fn ($p) => isset($p['text']) && empty($p['thought'])), 'text' )); $result = $this->parseRespuesta($text); AiUsageLog::registrar([ 'servicio' => 'gemini_agent', 'canal' => $canal, 'usuario_id' => $usuarioId, 'input_tokens' => $response->json('usageMetadata.promptTokenCount', 0), 'output_tokens' => $response->json('usageMetadata.candidatesTokenCount', 0), 'tiempo_ms' => $tiempoMs, 'resultado' => $result['respuesta'] ? 'ok' : 'error', 'detalle' => ['accion' => $result['accion'] ?? null, 'raw' => substr($text, 0, 200)], ]); Log::info('[GeminiAgent] ' . json_encode($result)); return $result; } // ─── Context builder ──────────────────────────────────────────────────── public static function buildContexto(array $state, ?int $userId): array { // Balance $balance = 0; if ($userId) { $user = \App\Models\User::with('saldo')->find($userId); $balance = $user?->saldo?->valor ?? 0; } // Catálogo resumido (cacheado 5 min por rol) $rolId = $state['rol_id'] ?? null; $catalogo = Cache::remember("agent_catalogo_{$rolId}", 300, fn () => self::buildCatalogo($rolId, $userId)); // Últimos mensajes del chat (contexto conversacional) $historial = ''; if ($convId = $state['conv_id'] ?? null) { $msgs = ChatMessage::where('conversation_id', $convId) ->orderBy('id', 'desc') ->limit(8) ->get() ->reverse(); if ($msgs->count() > 1) { $lines = $msgs->map(fn ($m) => ($m->tipo === 'usuario' ? 'Usuario' : 'Bot') . ': ' . $m->contenido); $historial = "CONVERSACIÓN RECIENTE:\n" . $lines->implode("\n"); } } return [ 'nombre' => $state['nombre'] ?? ($state['data']['nombre'] ?? 'Cliente'), 'balance' => number_format($balance), 'catalogo' => $catalogo, 'historial' => $historial, ]; } private static function buildCatalogo(?int $rolId, ?int $userId): string { $servicios = Servicio::with(['tarifas' => fn ($q) => $q->where('estado', 'activo') ->when($rolId, fn ($q2) => $q2->where('rol_id', $rolId)) ])->where('estado', 'activo')->limit(12)->get(); $lines = []; foreach ($servicios as $srv) { if ($srv->tarifas->isEmpty()) { continue; } $tarifa = $srv->tarifas->first(); $precio = CuentasHelper::precio($tarifa->id, $userId, $tarifa->valor); $disp = CuentasHelper::contar($tarifa) > 0 ? '✅' : '❌'; $lines[] = " {$disp} {$srv->nombre} — \$" . number_format($precio) . " / {$tarifa->dias} días"; } return $lines ? implode("\n", $lines) : 'Sin servicios disponibles'; } // ─── Prompt ───────────────────────────────────────────────────────────── private function buildPrompt(string $texto, array $contexto): string { $nombre = $contexto['nombre'] ?? 'Cliente'; $balance = $contexto['balance'] ?? '0'; $catalogo = $contexto['catalogo'] ?? ''; $hist = $contexto['historial'] ? "\n" . $contexto['historial'] . "\n" : ''; return << null, 'accion' => null, 'accion_data' => []]; $text = preg_replace('/```json\s*|\s*```/', '', $text); if (preg_match('/\{.*\}/s', $text, $m)) { $text = $m[0]; } $json = json_decode(trim($text), true); if (! is_array($json) || empty($json['respuesta'])) { return $vacio; } $accionesValidas = ['recarga.iniciar', 'servicios.listar', 'promo.listar', 'credenciales.listar', 'historial.ver', 'perfil.ver']; $accion = in_array($json['accion'] ?? '', $accionesValidas) ? $json['accion'] : null; $accionData = is_array($json['accion_data'] ?? null) ? $json['accion_data'] : []; return [ 'respuesta' => trim($json['respuesta']), 'accion' => $accion, 'accion_data' => $accionData, ]; } }