Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
26 KiB
JSON
1 line
26 KiB
JSON
{"repo": "alumnisteman/steman-alumni", "files": {"app/Services/AIService.php": {"1e51b8e3c3cbee0bafcab24a98baf5c09ef96de8": {"t": 1786068385.153905, "n": 26023, "h": "144a38fa7426cc5d", "c": "<?php\n\nnamespace App\\Services;\n\nuse Illuminate\\Support\\Facades\\Http;\nuse Illuminate\\Support\\Facades\\Log;\nuse Illuminate\\Support\\Facades\\RateLimiter;\n\nclass AIService\n{\n protected ?string $apiKey;\n protected ?string $openRouterKey;\n protected ?string $openRouterModel;\n protected ?string $openRouterApiBase;\n protected ?string $deepSeekKey;\n protected string $baseUrl = 'https://generativelanguage.googleapis.com';\n protected string $deepSeekUrl = 'https://api.deepseek.com';\n protected string $gatewayUrl = 'http://ai-gateway:3000';\n public ?string $activeProvider = null;\n\n public function __construct(?string $apiKey = null)\n {\n // Prioritize: Passed key > config/.env > Setting from Database\n $this->apiKey = $apiKey ?: (\\config('services.gemini.api_key') ?: \\setting('gemini_api_key'));\n $this->openRouterKey = \\config('services.openrouter.api_key');\n $this->openRouterModel = \\config('services.openrouter.model', 'google/gemini-2.0-flash-exp:free');\n $this->openRouterApiBase = \\config('services.openrouter.api_base', 'https://openrouter.ai/api/v1');\n $this->deepSeekKey = \\config('services.deepseek.api_key');\n $this->gatewayUrl = \\config('services.ai_gateway.url', 'http://ai-gateway:3000');\n }\n\n private function tryDeepSeek(string $prompt, float $temperature): ?string\n {\n try {\n $response = Http::withoutVerifying()->withHeaders([\n 'Authorization' => 'Bearer ' . $this->deepSeekKey,\n 'Content-Type' => 'application/json',\n ])->timeout(30)->post(\"{$this->deepSeekUrl}/chat/completions\", [\n 'model' => 'deepseek-chat',\n 'messages' => [\n ['role' => 'user', 'content' => $prompt]\n ],\n 'temperature' => $temperature,\n ]);\n\n if ($response->successful()) {\n return $response->json('choices.0.message.content');\n }\n } catch (\\Exception $e) {\n Log::error('AIService: DeepSeek Exception: ' . $e->getMessage());\n }\n return null;\n }\n\n /**\n * General method to prompt Gemini with Auto-Switching capabilities\n */\n public function ask(string $prompt, float $temperature = 0.7, ?string $model = null): ?string\n {\n $this->activeProvider = null;\n $primary = config('services.ai_gateway.primary_provider', 'gemini');\n\n // Cek apakah gateway tersedia sebelum coba\n $gatewayEnabled = !empty($this->gatewayUrl) && !str_contains($this->gatewayUrl, 'ai-gateway');\n $providers = array_filter(['gateway', 'gemini', 'deepseek', 'openrouter'], function($p) use ($gatewayEnabled) {\n return $p !== 'gateway' || $gatewayEnabled;\n });\n\n foreach ($providers as $provider) {\n // Skip provider if it failed recently (cooldown)\n if (\\Illuminate\\Support\\Facades\\Cache::has(\"ai_provider_fail_{$provider}\")) {\n continue;\n }\n\n if ($provider === 'gateway') {\n if (empty($this->gatewayUrl)) {\n Log::debug('AIService: Gateway URL not configured, skipping.');\n continue;\n }\n Log::info('AIService: Attempting AI Gateway...');\n $result = $this->tryGateway($prompt);\n if ($result) {\n $this->activeProvider = 'AI Gateway (Redundant)';\n return $result;\n }\n $this->markProviderFailed('gateway');\n } elseif ($provider === 'gemini') {\n $models = $model ? [$model] : ['gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.0-flash', 'gemini-2.0-flash-lite'];\n foreach ($models as $currentModel) {\n for ($attempt = 1; $attempt <= 2; $attempt++) {\n $result = $this->tryRequest($prompt, $temperature, $currentModel);\n if ($result) {\n $this->activeProvider = 'Gemini (' . $currentModel . ')';\n return $result;\n }\n if ($attempt < 2) usleep(500000); // 0.5s\n }\n }\n // If all Gemini models failed, mark Gemini as failed for a while\n $this->markProviderFailed('gemini');\n\n } elseif ($provider === 'deepseek' && !empty($this->deepSeekKey)) {\n Log::info('AIService: Attempting DeepSeek Direct...');\n $result = $this->tryDeepSeek($prompt, $temperature);\n if ($result) {\n $this->activeProvider = 'DeepSeek Direct';\n return $result;\n }\n $this->markProviderFailed('deepseek');\n\n } elseif ($provider === 'openrouter' && !empty($this->openRouterKey)) {\n Log::info('AIService: Attempting OpenRouter...', ['model' => $this->openRouterModel]);\n $result = $this->tryOpenRouter($prompt, $temperature);\n if ($result) {\n $this->activeProvider = 'OpenRouter (' . $this->openRouterModel . ')';\n return $result;\n }\n $this->markProviderFailed('openrouter');\n }\n }\n\n Log::error('AIService: All AI attempts failed across all configured providers.');\n return null;\n }\n\n /**\n * Strategic data analysis for admins\n */\n public function analyzeStats(array $stats): ?string\n {\n $prompt = \"You are a senior data analyst for an alumni network portal. \n Analyze the following statistics and provide 3-4 bullet points of strategic insights or recommendations for the admin. \n Use a professional, encouraging, and slightly futuristic tone. Output in Indonesian.\n \n STATISTICS:\n - Total Alumni: {$stats['totalAlumni']}\n - Employment Rate: {$stats['employmentRate']}%\n - International Alumni: {$stats['internationalCount']}\n - Total Programs: {$stats['totalPrograms']}\n - Total Jobs: {$stats['totalJobs']}\n \n Keep it concise but impactful.\";\n\n return $this->ask($prompt, 0.8, 'gemini-2.5-flash');\n }\n\n /**\n * Internal request handler with dynamic versioning\n */\n private function tryRequest(string $prompt, float $temperature, string $model): ?string\n {\n if (empty($this->apiKey)) {\n Log::warning('AIService: GEMINI_API_KEY is not configured.');\n return null;\n }\n\n // v1beta first karena gemini-2.5-flash hanya tersedia di v1beta\n $apiVersions = ['v1beta', 'v1'];\n\n foreach ($apiVersions as $apiVersion) {\n try {\n $url = \"{$this->baseUrl}/{$apiVersion}/models/{$model}:generateContent?key={$this->apiKey}\";\n\n $response = Http::withHeaders([\n 'Content-Type' => 'application/json',\n ])->timeout(30)->post($url, [\n 'contents' => [['parts' => [['text' => $prompt]]]],\n 'generationConfig' => [\n 'temperature' => $temperature,\n 'maxOutputTokens' => 1024,\n ]\n ]);\n\n if ($response->successful()) {\n $text = $response->json('candidates.0.content.parts.0.text');\n return !empty($text) ? $text : null;\n }\n\n $status = $response->status();\n $body = $response->body();\n\n // Jika 404, model tidak ada di versi ini, coba versi berikutnya\n if ($status === 404) {\n continue;\n }\n\n // Differentiate between quota (429) and real errors\n if ($status === 429) {\n // Quota exceeded, try next provider\n } else {\n Log::warning(\"AIService: Gemini API Error ($status) for [$model] in [$apiVersion]\", [\n 'url' => \"{$this->baseUrl}/{$apiVersion}/models/{$model}\",\n 'body' => $body\n ]);\n }\n\n // If we get a 403 or 400, no point in trying other versions for this model\n if ($status === 403 || $status === 400) break;\n\n } catch (\\Exception $e) {\n Log::error(\"AIService Exception for [$model] in [$apiVersion]: \" . $e->getMessage());\n break;\n }\n }\n\n return null;\n }\n\n /**\n * Fallback to OpenRouter (The \"Open Claw\" integration)\n */\n private function tryOpenRouter(string $prompt, float $temperature): ?string\n {\n try {\n $endpoint = rtrim($this->openRouterApiBase, '/') . '/chat/completions';\n $response = Http::withHeaders([\n 'Authorization' => 'Bearer ' . $this->openRouterKey,\n 'Content-Type' => 'application/json',\n 'HTTP-Referer' => \\config('app.url'),\n 'X-Title' => 'Smart Market OS',\n ])->timeout(30)->post($endpoint, [\n 'model' => $this->openRouterModel,\n 'messages' => [\n ['role' => 'user', 'content' => $prompt]\n ],\n 'temperature' => $temperature,\n ]);\n\n if ($response->successful()) {\n return $response->json('choices.0.message.content');\n }\n\n Log::warning('AIService: OpenRouter API Error', [\n 'status' => $response->status(),\n 'body' => $response->body()\n ]);\n } catch (\\Exception $e) {\n Log::error('AIService: OpenRouter Exception: ' . $e->getMessage());\n }\n\n return null;\n }\n\n /**\n * Verify if the API key is healthy\n */\n public function checkHealth(): array\n {\n if (empty($this->apiKey) && empty($this->openRouterKey) && empty($this->deepSeekKey)) {\n return ['status' => 'DEGRADED', 'message' => 'AI Service Degraded: No API Keys configured. AI features disabled.', 'provider' => 'None'];\n }\n\n // Try a very simple, safe ping\n $result = $this->ask(\"ping\", 0.1);\n \n if ($result) {\n return [\n 'status' => 'HEALTHY', \n 'message' => 'AI Resilience Engine Operational (Active: ' . ($this->activeProvider ?? 'Primary') . ')',\n 'provider' => $this->activeProvider ?? 'Unknown'\n ];\n }\n\n // If general ask failed, maybe all failed or just primary. \n // We report ERROR only if we truly can't get ANY response.\n return ['status' => 'DEGRADED', 'message' => 'AI Service Degraded: All providers unreachable. AI features disabled.', 'provider' => 'None'];\n }\n\n private function markProviderFailed(string $provider): void\n {\n // Don't mark as failed if it's the only one left, always keep trying something\n Log::warning(\"AIService: Marking provider [{$provider}] as failed temporarily.\");\n \\Illuminate\\Support\\Facades\\Cache::put(\"ai_provider_fail_{$provider}\", true, 600); // 10 min cooldown\n }\n\n /**\n * Specific method for content moderation\n */\n public function moderate(string $content): array\n {\n $prompt = \"Identify if the following text contains spam, scam, hate speech, or inappropriate content for an alumni portal. Return JSON format only: {\\\"is_safe\\\": boolean, \\\"reason\\\": \\\"string\\\", \\\"score\\\": 0.0-1.0}.\\n\\nText: \\\"$content\\\"\";\n \n $result = $this->ask($prompt, 0.1);\n \n if (!$result) {\n return ['is_safe' => true, 'reason' => 'AI unavailable', 'score' => 0];\n }\n\n // Strip markdown code blocks if AI returns them\n $json = preg_replace('/^```json\\s*|\\s*```$/i', '', trim($result));\n \n return json_decode($json, true) ?? ['is_safe' => true, 'reason' => 'Parse error', 'score' => 0];\n }\n\n /**\n * AI Recommendation: Match alumni based on interests/major\n */\n public function recommendAlumni(array $userProfile, array $candidates): array\n {\n $candidateData = \\collect($candidates)->map(fn($c) => \"[ID: {$c['id']}, Name: {$c['name']}, Major: {$c['major']}, Job: {$c['current_job']}]\")->join(\"\\n\");\n \n $prompt = \"You are a professional networking assistant. Based on this user profile:\n Name: {$userProfile['name']}\n Major: {$userProfile['major']}\n Bio: {$userProfile['bio']}\n \n Recommend the top 3 best matching alumni from this list for professional networking:\n $candidateData\n \n Criteria: Same major, complementary skills, or interesting career paths.\n Return ONLY valid JSON: [{\\\"id\\\": integer, \\\"reason\\\": \\\"string\\\"}].\n Language: Indonesian.\";\n\n $result = $this->ask($prompt, 0.4);\n\n if (!$result) return [];\n\n $json = preg_replace('/^```json\\s*|\\s*```$/i', '', trim($result));\n return json_decode($json, true) ?? [];\n }\n\n /**\n * AI Assistant: Conversational bot with portal knowledge\n */\n /**\n * AI Career Mentor: Personalized matchmaking\n */\n public function matchMentor(array $userProfile, array $candidates): array\n {\n $candidateData = \\collect($candidates)->map(fn($c) => \"[ID: {$c['id']}, Name: {$c['name']}, Major: {$c['major']}, Job: {$c['current_job']}]\")->join(\"\\n\");\n \n $prompt = \"You are an expert career counselor. Based on this user's profile:\n Name: {$userProfile['name']}\n Current Major: {$userProfile['major']}\n Career Goal: {$userProfile['goal']}\n \n Find matching mentors from this list:\n $candidateData\n \n Provide 2 potential mentors and explain why in 1 short sentence Indonesian for each.\n Return ONLY valid JSON: [{\\\"id\\\": integer, \\\"reason\\\": \\\"string\\\"}].\";\n\n $result = $this->ask($prompt, 0.5);\n if (!$result) return [];\n\n $json = preg_replace('/^```json\\s*|\\s*```$/i', '', trim($result));\n return json_decode($json, true) ?? [];\n }\n\n public function getAssistantResponse(string $userMessage, array $history = []): string\n {\n $schoolName = setting('school_name', 'SMKN 2 Ternate');\n $chairmanName = setting('chairman_name', 'Nama Ketua Umum');\n $siteName = setting('site_name', 'Forum Silaturahmi Alumni Steman Ternate');\n\n $systemPrompt = \"You are 'Steman-AI', the official assistant for the $siteName portal. \n Context:\n - Organization: Forum Silaturahmi Alumni Steman Ternate ($schoolName).\n - Chairman: $chairmanName.\n - Purpose: Connecting alumni, sharing jobs, and nostalgia.\n - Features: 3D Network Map, Jobs Portal, Mentoring, and Digital Cards.\n \n Guidelines:\n 1. Be friendly, professional, and helpful. \n 2. Use Indonesian language.\n 3. Keep answers concise (max 3 sentences).\n 4. If asked about technical issues, tell them to contact admin.\n 5. Use emojis occasionally for a friendly feel. 🚀\";\n\n $fullPrompt = \"$systemPrompt\\n\\nUser: $userMessage\\nSteman-AI:\";\n\n return $this->ask($fullPrompt, 0.7) ?? \"Maaf, saya sedang tidak bisa memproses permintaan Anda. Coba lagi nanti ya! 🙏\";\n }\n\n /**\n * AI Geocoding: Convert address to lat/lng\n */\n public function geocode(string $address): ?array\n {\n $cacheKey = 'geocode_fail_' . md5($address);\n if (\\Illuminate\\Support\\Facades\\Cache::has($cacheKey)) {\n Log::warning(\"AIService: Skipping geocode for known unsearchable address: $address\");\n return null;\n }\n\n // Variation 1: Exact Address\n $prompt = \"Convert the following Indonesian address into geographic coordinates (latitude and longitude). \n Address: \\\"$address\\\"\n Return ONLY valid JSON format: {\\\"lat\\\": float, \\\"lng\\\": float}. \n If address is invalid or not found, return {\\\"lat\\\": null, \\\"lng\\\": null}.\";\n\n $result = $this->ask($prompt, 0.1);\n if ($result) {\n $json = preg_replace('/^```json\\s*|\\s*```$/i', '', trim($result));\n $data = json_decode($json, true);\n if (isset($data['lat']) && isset($data['lng']) && $data['lat'] !== null) {\n return $data;\n }\n }\n\n // Broad Search Fallback: Try Kabupaten/City\n $searchTerms = ['Kabupaten', 'Kota', 'Kecamatan'];\n foreach ($searchTerms as $term) {\n if (str_contains($address, $term)) {\n $parts = explode($term, $address);\n $broaderAddress = $term . end($parts);\n Log::warning(\"AIService Geocode: Specific failed, trying broader region: $broaderAddress\");\n \n $prompt = \"Find center coordinates for this area in Indonesia: \\\"$broaderAddress\\\". Return ONLY JSON: {\\\"lat\\\": float, \\\"lng\\\": float}.\";\n $result = $this->ask($prompt, 0.1);\n if ($result) {\n $json = preg_replace('/^```json\\s*|\\s*```$/i', '', trim($result));\n $data = json_decode($json, true);\n if (isset($data['lat']) && isset($data['lng']) && $data['lat'] !== null) {\n return $data;\n }\n }\n }\n }\n\n // Final Fallback: OpenStreetMap Nominatim with cleaned query\n Log::warning(\"AIService Geocode: Gemini failed, attempting Nominatim fallback for: $address\");\n try {\n // Clean common terms that sometimes confuse Nominatim for villages\n $cleanAddress = str_replace(['Desa ', 'Kelurahan '], '', $address);\n $query = $cleanAddress;\n if (!str_contains(strtolower($cleanAddress), 'indonesia')) {\n $query .= ', Indonesia';\n }\n \n $response = Http::withHeaders([\n 'User-Agent' => 'StemanAlumniPortal/1.0'\n ])->timeout(10)->get('https://nominatim.openstreetmap.org/search', [\n 'q' => $query,\n 'format' => 'json',\n 'limit' => 1\n ]);\n\n if ($response->successful() && !empty($response->json())) {\n $data = $response->json()[0];\n Log::warning(\"AIService Geocode Success (Nominatim): \" . $data['lat'] . \", \" . $data['lon']);\n return ['lat' => (float) $data['lat'], 'lng' => (float) $data['lon']];\n }\n } catch (\\Exception $e) {\n Log::warning(\"AIService Geocode: Nominatim fallback failed: \" . $e->getMessage());\n }\n\n // Mark as failed in cache for 24h to avoid redundant expensive API calls\n \\Illuminate\\Support\\Facades\\Cache::put($cacheKey, true, 86400);\n return null;\n }\n\n /**\n * AI Profile Optimizer: Suggest missing info for better engagement\n */\n public function suggestProfileOptimizations(array $userData): ?string\n {\n $prompt = \"You are a profile growth assistant. Based on this user data:\n - Major: {$userData['major']}\n - Job: {$userData['current_job']}\n - Social Links Count: {$userData['social_count']}\n - Has LinkedIn: \" . ($userData['has_linkedin'] ? 'Yes' : 'No') . \"\n - Bio Length: \" . strlen($userData['bio'] ?? '') . \" characters\n \n Provide 1 short, catchy, and motivational suggestion (Indonesian) to improve their profile.\n Example: 'Tambahkan LinkedIn Anda agar perusahaan lebih mudah melirik potensi profesional Anda!'\n Be friendly and encouraging.\";\n\n return $this->ask($prompt, 0.7);\n }\n /**\n * AI Content Helper: Generate or improve text for news/jobs\n */\n public function generateContent(string $type, string $input): ?string\n {\n $prompt = \"You are a creative content writer for a high-end alumni portal. \n Help the user $type based on this input: \\\"$input\\\".\n - Tone: Professional, engaging, and modern.\n - Language: Indonesian.\n - Output ONLY the result text, no chat or meta comments.\n - If generating news, make it a compelling news article.\n - If generating a job description, make it professional and structured.\";\n\n return $this->ask($prompt, 0.7);\n }\n\n /**\n * Generate a professional bio for an alumni.\n * \n * @param string $name\n * @param string $major\n * @param string|int $gradYear\n * @param string $skills\n * @param string $experience\n * @return string|null\n */\n public function generateProfessionalBio($name, $major, $gradYear, $skills = '', $experience = '')\n {\n $prompt = \"Generate a short, professional, and inspiring bio for an alumni named {$name}. \n Major: {$major}. \n Graduation Year: {$gradYear}. \n Skills: {$skills}. \n Experience: {$experience}.\n The bio should be in Indonesian, maximum 3 sentences, and sound confident yet approachable. \n Return ONLY the bio text.\";\n\n return $this->ask($prompt);\n }\n\n /**\n * AI Gacha Matchmaker: Calculate compatibility score and comments\n */\n public function getCompatibilityScore(array $me, array $target): ?array\n {\n $prompt = \"You are a friendly, witty, and optimistic AI Matchmaker for an alumni portal.\n Analyze these two alumni profiles and calculate their compatibility for networking/friendship:\n \n Profile 1 (Me):\n - Name: {$me['name']}\n - Major: {$me['major']}\n - Graduation Year: {$me['graduation_year']}\n - City: {$me['city']}\n - Interests: {$me['interests']}\n - Bio: {$me['bio']}\n \n Profile 2 (Target):\n - Name: {$target['name']}\n - Major: {$target['major']}\n - Graduation Year: {$target['graduation_year']}\n - City: {$target['city']}\n - Interests: {$target['interests']}\n - Bio: {$target['bio']}\n \n Return ONLY valid JSON format:\n {\n \\\"score\\\": integer (between 50 and 99),\n \\\"comment\\\": \\\"string (1-2 encouraging sentences in Indonesian explaining their compatibility, highlighting shared fields, majors, city, or interests)\\\"\n }\n Do not output any markdown code fences or conversational text.\";\n\n $result = $this->ask($prompt, 0.6);\n if (!$result) return ['score' => 75, 'comment' => 'Kalian berdua alumni SMKN 2 Ternate, pasti punya banyak kesamaan untuk dibicarakan!'];\n\n $json = preg_replace('/^```json\\s*|\\s*```$/i', '', trim($result));\n $data = json_decode($json, true);\n return is_array($data) ? $data : ['score' => 75, 'comment' => 'Kalian berdua alumni SMKN 2 Ternate, pasti punya banyak kesamaan untuk dibicarakan!'];\n }\n\n /**\n * AI Gacha Icebreakers: Generate 3 interesting chat starters\n */\n public function generateIcebreakers(array $me, array $target): ?array\n {\n $prompt = \"You are a social icebreaker expert. Create 3 distinct conversation starter options (in Indonesian) that User 1 (Me) can send to User 2 (Target).\n Make the options vary: Option 1 should be professional/networking, Option 2 friendly/nostalgic (mentioning SMKN 2 Ternate), Option 3 funny/creative.\n \n User 1 (Me):\n - Name: {$me['name']}\n - Major: {$me['major']}\n - Graduation Year: {$me['graduation_year']}\n - Interests: {$me['interests']}\n \n User 2 (Target):\n - Name: {$target['name']}\n - Major: {$target['major']}\n - Graduation Year: {$target['graduation_year']}\n - Interests: {$target['interests']}\n \n Return ONLY valid JSON format:\n [\n \\\"string option 1\\\",\n \\\"string option 2\\\",\n \\\"string option 3\\\"\n ]\n Do not output any markdown code fences or conversational text.\";\n\n $result = $this->ask($prompt, 0.7);\n if (!$result) return [\n \"Halo {$target['name']}, senang bisa terhubung! Salam kenal ya.\",\n \"Halo {$target['name']}, sesama alumni SMKN 2 Ternate nih. Apa kabar?\",\n \"Halo kak {$target['name']}, salam hangat dari angkatan {$me['graduation_year']}!\"\n ];\n\n $json = preg_replace('/^```json\\s*|\\s*```$/i', '', trim($result));\n $data = json_decode($json, true);\n return is_array($data) ? $data : [\n \"Halo {$target['name']}, senang bisa terhubung! Salam kenal ya.\",\n \"Halo {$target['name']}, sesama alumni SMKN 2 Ternate nih. Apa kabar?\",\n \"Halo kak {$target['name']}, salam hangat dari angkatan {$me['graduation_year']}!\"\n ];\n }\n\n /**\n * AI Birthday Greeting: Generate a personalized birthday wish\n */\n public function generateBirthdayGreeting(array $target): ?string\n {\n $prompt = \"Write a warm, creative, and personalized birthday wish (in Indonesian) for this alumni:\n - Name: {$target['name']}\n - Major: {$target['major']}\n - Graduation Year: {$target['graduation_year']}\n \n Use a friendly, enthusiastic, and slightly nostalgic tone (referencing SMKN 2 Ternate/Steman).\n Keep it concise (1-2 sentences). Return ONLY the greeting text, no comments or quotes.\";\n\n return $this->ask($prompt, 0.8);\n }\n\n\n private function tryGateway(string $prompt): ?string\n {\n try {\n $response = Http::timeout(30)->post(\"{$this->gatewayUrl}/chat\", [\n 'prompt' => $prompt\n ]);\n\n if ($response->successful()) {\n return $response->json('response');\n }\n } catch (\\Exception $e) {\n Log::error('AIService: Gateway Error: ' . $e->getMessage());\n }\n return null;\n }\n}\n"}}}} |