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(),
];
}
}
}