First Commit
This commit is contained in:
Executable
+464
@@ -0,0 +1,464 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\MessageType;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Multi-provider AI service supporting OpenAI-compatible APIs.
|
||||
* Providers: OpenRouter, OpenAI, StepFun, llama.cpp, QwenCloud, etc.
|
||||
*/
|
||||
class AIService
|
||||
{
|
||||
/** @var string The API base URL */
|
||||
protected string $baseUrl;
|
||||
|
||||
/** @var string The API key */
|
||||
protected string $apiKey;
|
||||
|
||||
/** @var string The model name */
|
||||
protected string $model;
|
||||
|
||||
/** @var string The provider name for logging */
|
||||
protected string $provider;
|
||||
|
||||
/** @var array Default headers */
|
||||
protected array $headers = [];
|
||||
|
||||
/**
|
||||
* Get the configured default AI provider/model for translations.
|
||||
*
|
||||
* @return array{provider: string, model: string}
|
||||
*/
|
||||
public static function getTranslationDefaults(): array
|
||||
{
|
||||
$provider = \App\Models\Setting::where('key', 'ai_default_provider')->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 <span class=\"tts\" data-text=\"[English word/phrase]\">[English word/phrase]</span> tag\n"
|
||||
. "- Its meaning in {$languageName}\n"
|
||||
. "- An example sentence IN ENGLISH wrapped in a <span class=\"tts\" data-text=\"[English example sentence]\">[English example sentence]</span> tag\n\n"
|
||||
. "## Practical Examples\n"
|
||||
. "Provide 3-5 practical example sentences IN ENGLISH showing real-world usage. "
|
||||
. "Format each line EXACTLY as:\n"
|
||||
. "<span class=\"tts\" data-text=\"[English sentence]\">[English sentence]</span> - [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]/ <span class=\"tts\" data-text=\"[English sentence]\">[English sentence]</span> - [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<int, array{role: string, content: string}> $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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user