108 lines
3.8 KiB
PHP
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;
|
|
}
|
|
}
|