value('value'); $model = \App\Models\Setting::where('key', 'ai_default_model')->value('value'); $provider = $provider ?: config('services.ai.default_provider', 'openrouter'); $model = $model ?: config('services.ai.providers.'.$provider.'.model', 'inclusionai/ling-3.0-tiny:free'); return [ 'provider' => $provider, 'model' => $model, ]; } /** * Create a new AIService instance. * * @param string|null $provider Optional provider name. If null, uses default from config. * @param string|null $model Optional model name. If null, uses provider default. */ public function __construct(?string $provider = null, ?string $model = null) { $defaultProvider = config('services.ai.default_provider', 'openrouter'); $defaults = self::getTranslationDefaults(); $this->loadProvider($provider ?? $defaults['provider'] ?? $defaultProvider); if ($model) { $this->model = $model; } elseif (!empty($defaults['model'])) { $this->model = $defaults['model']; } } /** * Load provider configuration. */ protected function loadProvider(string $provider): void { $this->provider = $provider; $config = config("services.ai.providers.{$provider}", []); $this->baseUrl = rtrim($config['base_url'] ?? '', '/') . '/'; $this->apiKey = $config['api_key'] ?? ''; $this->model = $config['model'] ?? 'gpt-3.5-turbo'; // Set default headers $this->headers = [ 'Content-Type' => 'application/json', ]; if (!empty($this->apiKey)) { // Some providers use 'Authorization: Bearer', others use 'x-api-key' $this->headers['Authorization'] = 'Bearer ' . $this->apiKey; } // Provider-specific headers if ($provider === 'openrouter') { $this->headers['HTTP-Referer'] = config('app.url'); $this->headers['X-Title'] = config('app.name'); } } /** * Generate a message based on the configured message type. * * @param string $subject The subject/topic for the message * @param string $userMessage Additional user instructions * @param array $context Optional context (language, introduction, etc.) * @param \App\Enums\MessageType|null $messageType The type of message to generate */ public function generateInsight(string $subject, string $userMessage, array $context = [], ?MessageType $messageType = null): array { if (empty($this->apiKey)) { return [ 'success' => false, 'error' => __('API key not configured for provider: :provider', ['provider' => $this->provider]), ]; } // Get language preference from context $language = $context['language'] ?? 'en'; unset($context['language']); $languageNames = [ 'pt_BR' => 'Brazilian Portuguese', 'en' => 'English', 'es' => 'Spanish', ]; $languageName = $languageNames[$language] ?? 'English'; $messageType = $messageType ?? MessageType::Insight; $baseSystemPrompt = "You are an expert learning assistant for the subject: {$subject}. "; $languageInstruction = "CRITICAL LANGUAGE INSTRUCTION - You MUST follow this exactly:\n" . "Your entire response MUST be written ONLY in {$languageName} ({$language}).\n" . "Do NOT use any other language. Do NOT include English translations or explanations.\n" . "If the subject is a language course (like English), you may use that language as examples,\n" . "but all instructions, explanations, and tips MUST be in {$languageName}.\n" . "This is mandatory. Start your response directly with the content in {$languageName}."; $systemPrompt = match ($messageType) { MessageType::LanguageLesson => $baseSystemPrompt . "You are acting as a specialized Language Teacher. " . "CRITICAL LANGUAGE RULE FOR LESSONS:\n" . "- Explanations, instructions, and tips MUST be written in {$languageName} ({$language}).\n" . "- BUT all target-language content (examples, vocabulary, practice sentences) MUST be in the language being taught: English.\n" . "- Do NOT translate English examples into {$languageName}. Keep English content pure.\n" . "- For Practical Examples and Practice Sentences: the ENGLISH sentence comes FIRST, then its Portuguese translation AFTER a hyphen.\n" . "- Example: 'The experiment shows that light travels faster than sound. - O experimento mostra que a luz viaja mais rápido que o som.'\n\n" . "ABSOLUTE RULES FOR PRACTICAL EXAMPLES AND PRACTICE SENTENCES:\n" . "1. The English sentence MUST come FIRST.\n" . "2. After the English sentence, add a hyphen and the Portuguese translation.\n" . "3. Do NOT write the Portuguese sentence first.\n" . "4. Do NOT repeat the same sentence twice.\n" . "5. Do NOT mix languages in the same sentence.\n\n" . "Create a complete language lesson structured exactly as follows:\n\n" . "## Insight\n" . "A brief, motivating introduction/insight about the language concept being taught. Write this in {$languageName}.\n\n" . "## Grammar Explanation\n" . "Explain ONE specific grammar point clearly and concisely. " . "Include rules, usage, and common mistakes. Write this in {$languageName}.\n\n" . "## Vocabulary Building\n" . "List 5-8 essential words/phrases IN ENGLISH. For each, provide:\n" . "- The word/phrase IN ENGLISH wrapped in a [English word/phrase] tag\n" . "- Its meaning in {$languageName}\n" . "- An example sentence IN ENGLISH wrapped in a [English example sentence] tag\n\n" . "## Practical Examples\n" . "Provide 3-5 practical example sentences IN ENGLISH showing real-world usage. " . "Format each line EXACTLY as:\n" . "[English sentence] - [Portuguese translation]\n" . "Do NOT put Portuguese first. English MUST be first.\n\n" . "## Practice Sentences\n" . "Provide 5-7 complete sentences IN ENGLISH for the user to practice speaking. " . "Format each line EXACTLY as:\n" . "/[Complete IPA for the full English sentence]/ [English sentence] - [Portuguese translation]\n" . "The IPA must be COMPLETE for the FULL English sentence. Do NOT truncate IPA.\n\n" . "WRONG FORMAT (DO NOT USE):\n" . "- [Portuguese sentence] - [English sentence]\n" . "CORRECT FORMAT (MANDATORY):\n" . "- [English sentence] - [Portuguese sentence]\n\n" . "Make it engaging, structured, and suitable for self-study.\n\n" . $languageInstruction, MessageType::GameSimulator => $baseSystemPrompt . "You are acting as a Game and Simulator Strategy Expert. " . "Create content that includes:\n" . "- An insight or strategic tip related to the subject\n" . "- Practical strategies and tactics\n" . "- Commands, shortcuts, or step-by-step instructions where applicable\n" . "- Scenarios or simulation tips\n" . "Make it actionable and engaging.\n\n" . $languageInstruction, MessageType::TechnicalTeaching => $baseSystemPrompt . "You are acting as a Specialized Technical Teacher. " . "Create content that includes:\n" . "- An insight about the technical subject\n" . "- Description and practical use of components/tools\n" . "- Types of components/tools and their differences\n" . "- Service techniques and best practices\n" . "- Repair techniques and troubleshooting\n" . "- How to read technical diagrams/schematics\n" . "- Practical examples and applied exercises when relevant\n" . "Be direct, technical, and educational. Use clear step-by-step explanations.\n\n" . $languageInstruction, default => $baseSystemPrompt . "Provide insightful, educational content. Be concise and practical.\n\n" . $languageInstruction, }; $userMessageWithLanguage = "[IMPORTANTE: Responda EXCLUSIVAMENTE em {$languageName}. " . "NÃO use inglês. NÃO traduza. NÃO misture idiomas. " . "Toda a resposta deve ser APENAS em {$languageName}.]\n\n" . $userMessage; if ($messageType === MessageType::TechnicalTeaching && str_contains(strtolower($subject), 'eletrônica')) { $userMessageWithLanguage .= "\n\nSPECIAL INSTRUCTION: For each lesson, include explanation, technical description, and practical applications of specific electronic components. Examples: LM555, AT89C2051, BC557.\n"; $userMessageWithLanguage .= "CRITICAL: Write ALL content in Brazilian Portuguese. Even technical terms must be explained in Portuguese. Do NOT write in English."; } $messages = [ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userMessageWithLanguage], ]; // Add context if provided if (!empty($context)) { $contextStr = "Context information:\n"; foreach ($context as $key => $value) { $contextStr .= "- {$key}: {$value}\n"; } array_unshift($messages, ['role' => 'system', 'content' => $contextStr]); } try { $response = Http::withHeaders($this->headers) ->timeout(120) ->post($this->baseUrl . 'chat/completions', [ 'model' => $this->model, 'messages' => $messages, ]); $data = $response->json(); if (!$data || !isset($data['choices'][0]['message'])) { $errorMsg = $data['error']['message'] ?? ($data['error'] ?? __('Unknown API error')); Log::error("AI Service ({$this->provider}) error response", [ 'response' => $data, 'subject' => $subject, 'message_type' => $messageType->value, ]); return [ 'success' => false, 'error' => $errorMsg, ]; } $message = $data['choices'][0]['message']; $content = $message['content'] ?? $message['reasoning'] ?? null; if ($content) { return [ 'success' => true, 'content' => $content, 'input_tokens' => $data['usage']['prompt_tokens'] ?? 0, 'output_tokens' => $data['usage']['completion_tokens'] ?? 0, ]; } Log::error("AI Service ({$this->provider}): Empty response content", [ 'response' => $data, 'subject' => $subject, 'message_type' => $messageType->value, ]); return [ 'success' => false, 'error' => __('The AI model returned an empty response. Please try again.'), ]; } catch (\Exception $e) { Log::error("AI Service ({$this->provider}) error: " . $e->getMessage(), [ 'subject' => $subject, 'message_type' => $messageType->value, ]); return [ 'success' => false, 'error' => $e->getMessage(), ]; } } /** * Get the current provider name. */ public function getProvider(): string { return $this->provider; } /** * Get the current model name. */ public function getModel(): string { return $this->model; } /** * Translate text to the specified language using AI. * * @param string $text The text to translate * @param string $targetLanguage The target language code (pt_BR, en, es) * @param string|null $subject Optional subject context for better translation */ public function translate(string $text, string $targetLanguage, ?string $subject = null): array { if (empty($this->apiKey)) { return [ 'success' => false, 'error' => __('API key not configured for provider: :provider', ['provider' => $this->provider]), ]; } $languageNames = [ 'pt_BR' => 'Brazilian Portuguese', 'en' => 'English', 'es' => 'Spanish', ]; $languageName = $languageNames[$targetLanguage] ?? 'English'; $subjectContext = $subject ? "This is about: {$subject}.\n" : ''; $systemPrompt = "You are a professional translator. Translate the following text to {$languageName} ({$targetLanguage}).\n\n" . "RULES:\n" . "1. Translate ONLY the content, preserving the original meaning, tone, and structure.\n" . "2. Keep all markdown formatting, HTML tags, and special characters exactly as they are.\n" . "3. Do NOT add any explanations, notes, or translations of your own.\n" . "4. Do NOT translate code blocks, URLs, or technical terms that are commonly kept in English.\n" . "5. Return ONLY the translated text, nothing else.\n" . "6. The entire response must be in {$languageName}."; $userMessage = "{$subjectContext}Translate this text to {$languageName}:\n\n{$text}"; $messages = [ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userMessage], ]; try { $response = Http::withHeaders($this->headers) ->timeout(120) ->post($this->baseUrl . 'chat/completions', [ 'model' => $this->model, 'messages' => $messages, ]); $data = $response->json(); if (!$data || !isset($data['choices'][0]['message'])) { $errorMsg = $data['error']['message'] ?? ($data['error'] ?? __('Unknown API error')); Log::error("AI Service ({$this->provider}) translation error", [ 'response' => $data, 'target_language' => $targetLanguage, ]); return [ 'success' => false, 'error' => $errorMsg, ]; } $message = $data['choices'][0]['message']; $content = $message['content'] ?? $message['reasoning'] ?? null; if ($content) { return [ 'success' => true, 'content' => $content, 'input_tokens' => $data['usage']['prompt_tokens'] ?? 0, 'output_tokens' => $data['usage']['completion_tokens'] ?? 0, ]; } Log::error("AI Service ({$this->provider}): Empty translation response", [ 'response' => $data, 'target_language' => $targetLanguage, ]); return [ 'success' => false, 'error' => __('The AI model returned an empty response. Please try again.'), ]; } catch (\Exception $e) { Log::error("AI Service ({$this->provider}) translation error: " . $e->getMessage(), [ 'target_language' => $targetLanguage, ]); return [ 'success' => false, 'error' => $e->getMessage(), ]; } } /** * Send a chat message to the AI and get a response. * * @param array $messages */ public function chat(array $messages): array { if (empty($this->apiKey)) { return [ 'success' => false, 'error' => __('API key not configured for provider: :provider', ['provider' => $this->provider]), ]; } try { $response = Http::withHeaders($this->headers) ->timeout(120) ->post($this->baseUrl . 'chat/completions', [ 'model' => $this->model, 'messages' => $messages, ]); $data = $response->json(); if (!$data || !isset($data['choices'][0]['message'])) { $errorMsg = $data['error']['message'] ?? ($data['error'] ?? __('Unknown API error')); Log::error("AI Service ({$this->provider}) chat error", [ 'response' => $data, ]); return [ 'success' => false, 'error' => $errorMsg, ]; } $message = $data['choices'][0]['message']; $content = $message['content'] ?? $message['reasoning'] ?? null; if ($content) { return [ 'success' => true, 'content' => $content, 'input_tokens' => $data['usage']['prompt_tokens'] ?? 0, 'output_tokens' => $data['usage']['completion_tokens'] ?? 0, ]; } Log::error("AI Service ({$this->provider}): Empty chat response", [ 'response' => $data, ]); return [ 'success' => false, 'error' => __('The AI model returned an empty response. Please try again.'), ]; } catch (\Exception $e) { Log::error("AI Service ({$this->provider}) chat error: " . $e->getMessage(), []); return [ 'success' => false, 'error' => $e->getMessage(), ]; } } }