feat: Initial commit for Saqel Platform architecture, backend, Docker, and documentation
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user