Files
saqel/backend/app/Services/GeminiService.php
T

108 lines
3.8 KiB
PHP

<?php
namespace App\Services;
use App\Contracts\AiService;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class GeminiService implements AiService
{
private const ANALYSIS_SCHEMA_HINT = <<<'PROMPT'
أنت معلم خبير خاص بمنهج الثانوية العامة الأردني (التوجيهي). حلل بيانات أداء الطالب التالية
وأعد استجابة JSON فقط بالحقول التالية بالضبط:
{
"weak_concepts": [{"name": "...", "reason": "...", "severity": "high|medium|low"}],
"remedial_lesson_ids": [أرقام صحيحة],
"recommended_actions": ["..."],
"motivation_note": "رسالة تحفيزية قصيرة بالعربية",
"summary": "ملخص من سطرين بالعربية"
}
لا تكتب أي شيء خارج JSON.
PROMPT;
/**
* @param array<string, mixed> $performance
* @return array<string, mixed>
*
* @throws ConnectionException
*/
public function analyzeStudentPerformance(array $performance): array
{
$text = $this->generate(self::ANALYSIS_SCHEMA_HINT."\n\nبيانات الطالب:\n".json_encode($performance, JSON_UNESCAPED_UNICODE));
return $this->decodeJson($text);
}
/**
* @param array<string, mixed> $activity
* @return array<string, mixed>
*
* @throws ConnectionException
*/
public function generateParentWeeklyReport(array $activity): array
{
$prompt = <<<'PROMPT'
أنت مستشار تعليمي. اكتب تقريراً أسبوعياً لولي أمر طالب توجيهي بناءً على البيانات التالية.
أعد JSON فقط بهذا الشكل:
{"headline": "...", "progress_note": "...", "weak_points": ["..."], "home_advice": ["..."]}
استخدم العربية الواضحة دون مصطلحات تقنية.
PROMPT;
$text = $this->generate($prompt."\n\nبيانات النشاط:\n".json_encode($activity, JSON_UNESCAPED_UNICODE));
return $this->decodeJson($text);
}
/**
* @throws ConnectionException
*/
private function generate(string $prompt): string
{
$config = config('services.gemini');
$model = is_array($config) ? ($config['model'] ?? 'gemini-2.5-flash-lite') : 'gemini-2.5-flash-lite';
$response = Http::withHeaders([
'x-goog-api-key' => is_array($config) ? (string) $config['key'] : '',
])
->timeout(is_array($config) ? (int) ($config['timeout'] ?? 30) : 30)
->retry(2, 500)
->post(rtrim((string) (is_array($config) ? $config['base_url'] : ''), '/')."/models/{$model}:generateContent", [
'contents' => [
['parts' => [['text' => $prompt]]],
],
'generationConfig' => [
'temperature' => 0.4,
'responseMimeType' => 'application/json',
],
]);
if ($response->failed()) {
Log::warning('Gemini request failed', ['status' => $response->status(), 'body' => mb_substr($response->body(), 0, 500)]);
throw new ConnectionException('Gemini API request failed.');
}
/** @var array{candidates?: list<array{content?: array{parts?: list<array{text?: string}}>}}> $payload */
$payload = $response->json();
return $payload['candidates'][0]['content']['parts'][0]['text'] ?? '{}';
}
/**
* @return array<string, mixed>
*/
private function decodeJson(string $text): array
{
/** @var array<string, mixed> $decoded */
$decoded = json_decode(trim($text), true);
if (! is_array($decoded)) {
throw new \RuntimeException('Gemini returned invalid JSON payload.');
}
return $decoded;
}
}