الجودة والتطوير
دعم الذكاء الاصطناعي
التطبيقات التي تستخدم الذكاء الاصطناعي يجب أن تُعلن ذلك صراحةً في الـ manifest عبر حقلين:
aiSupport(boolean): هل يستخدم التطبيق الذكاء الاصطناعي؟aiMode(enum): إمّاown(أدوات الشريك الذاتية) أوplatform(محرّك أوكتا الموحَّد).
واجهة الإعلان داخل المنصة
- عند إنشاء التطبيق: بطاقة "دعم الذكاء الاصطناعي" تظهر بين "البيانات الأساسية" و"التسعير" في صفحة
/partner/modules/create. - عند تحديث الإصدارات: نفس البطاقة تظهر في تبويب "نظرة عامة" داخل محرّر الإصدار، فيمكن إضافة دعم AI لإصدار محدّد دون تعديل التطبيق الأصلي. الإعلان على مستوى الإصدار يتفوّق على إعلان مستوى التطبيق.
الخيارات
own— التطبيق يدير مزوّداته (OpenAI / Anthropic / Gemini / ...) ومفاتيحه بنفسه. لا يحتاج اعتماداً إضافياً من فريق المنصة.platform— التطبيق يستهلكAiManagerالمركزي في okta-web. التصريح هو الاعتماد، ولا خطوة انتظار بعده.
كيف يُعتمد platform فعلاً
بوّابة التشغيل (EnsureAiPlatformApproved) تسأل عمودَين على جدول modules في okta-web: ai_support وai_mode = platform. لا تقرأ بيانك الخام، ولا تنتظر طابوراً. وهذان العمودان يُملآن من بيانك في مسارَين:
| المسار | متى |
|---|---|
| النشر | مراجعة النشر التي وافقت على بيانك هي موافقة المنصّة على aiMode فيه — لا طلب اعتماد ثانٍ |
| الدفع إلى بيئة التجربة | فور دفع التطبيق، بلا نشر إنتاجي |
في بيئة التجربة: أعلِن وادفع، ويعمل — بلا أي موافقة من إدارة المنصّة.
بيئة التجربة تُسقِط سؤال الاعتماد كلّه: التطبيق غير مثبَّت في أي جهة حقيقية، والبيانات ليست بيانات أحد، والغاية من البيئة أن تعرف إن كانت الميزة تستحق التقديم أصلاً — فقرارُ اعتمادٍ يمنع التجربة قرارٌ في سؤال لم يُطرَح بعد. حتى السحب لا يصل إلى بيئة التجربة: إن سحبت الإدارة الاعتماد فالإنتاج يتوقّف وتبقى تجربتك تعمل.
والعكس ليس صحيحاً: الدفع إلى بيئة التجربة لا يُلغي سحباً على الإنتاج. في تركيب مشترك تقرأ جهةُ تجربتك والإنتاجُ صفَّ التطبيق نفسه، فلو كان الدفع يمسح العلم لاستعاد أي شريك وصولاً سُحب منه لسبب — بمجرّد الدفع إلى بيئته.
(وهذا كان معطوباً: مسار تثبيت التجربة لم يكن يستخرج العمودَين إطلاقاً، فتطبيق يعلن aiMode: platform يُدفَع إلى التجربة ويحصل على 403 دائماً — والمخرج الوحيد نشرٌ إنتاجي كامل.)
يبقى لفريق أوكتا سحب الاعتماد عند إساءة الاستخدام. السحب يوقف النداءات فوراً، ويعود الاعتماد عند نشر إصدار جديد يمرّ بمراجعة بشرية.
قراءة الـ 403 حين يأتي
الرفض يحمل الآن reason يقول أيّ الحالتين هي — وهما تحتاجان علاجَين متعاكسين:
{
"error": "ai_platform_not_approved",
"reason": "not_declared", // أو "revoked"
"message": "..."
}
not_declared— بيانك لم يُسجَّل: راجعaiSupport/aiModeفيه، وتأكّد أن التطبيق نُشِر أو دُفِع إلى التجربة. لا تنتظر أحداً.revoked— فريق أوكتا سحب الاعتماد: راجعهم.
الترويسات: X-Ai-Approval-Required: 1 وX-Ai-Denied-Reason: <reason>.
مرجع تفصيلي
راجع ai-support.md للحصول على قواعد التحقّق الكاملة، أمثلة manifest، وأسئلة شائعة عن التكلفة والترقية بين own وplatform.
أمثلة كود استخدام AiManager
هذه الأمثلة تنطبق على
aiMode=platformفقط. تطبيقاتaiMode=ownتستخدم مزوّداتها مباشرة (انظر القسم الأخير).
حقن AiManager
use App\AI\AiManager;
use App\AI\AiException;
class StudentReportsController extends Controller
{
public function __construct(private AiManager $ai) {}
public function summary(Request $request)
{
try {
$summary = $this->ai->summarize($request->long_text);
return response()->json(['summary' => $summary]);
} catch (AiException $e) {
return response()->json(['error' => $e->getMessage()], 502);
}
}
}
Laravel يحلّ AiManager تلقائياً عبر AiServiceProvider (يُسجَّل كـ singleton). يمكن أيضاً استخدام app(AiManager::class) أو app('ai').
chat() — محادثة بسيطة
$reply = app(AiManager::class)->chat(
prompt: 'لخّص لي تقرير الطالب التالي في 3 نقاط',
context: [
['role' => 'user', 'content' => 'تقرير الطالب: ...'],
['role' => 'assistant', 'content' => 'حسناً، سأقرأ التقرير.'],
],
opts: [
'model' => 'gpt-4o', // اختياري — افتراضي يحدّده محرّك أوكتا
'temperature' => 0.3,
'max_tokens' => 500,
'system_prompt' => 'أنت مساعد تعليمي محترف. أجب بالعربية.',
],
);
stream() — استجابة متدفّقة (للواجهات التفاعلية)
use App\AI\AiManager;
return response()->stream(function () {
$full = app(AiManager::class)->stream(
prompt: 'اشرح لي مفهوم التحليل العاملي',
context: [],
opts: ['temperature' => 0.5],
onChunk: function (string $chunk) {
echo "data: " . json_encode(['text' => $chunk]) . "\n\n";
ob_flush();
flush();
},
);
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
]);
في Livewire، استخدم wire:stream أو حدّث خاصية عامة من داخل onChunk callback.
complete() — إكمال نصّ (بدون محادثة)
$completion = app(AiManager::class)->complete(
text: "كتب الطالب أحمد في مقاله: 'التعليم هو السبيل ل",
opts: ['max_tokens' => 50],
);
// نتيجة: "...النهضة والتقدّم في المجتمعات الحديثة، ولذلك يجب..."
summarize() — تلخيص نصّ طويل
$summary = app(AiManager::class)->summarize(
longText: $student->report_full_text,
opts: [
'max_tokens' => 300,
'system_prompt' => 'لخّص في فقرة واحدة، مع التركيز على نقاط القوة والضعف.',
],
);
translate() — ترجمة
$en = app(AiManager::class)->translate(
text: 'الطالب متفوّق في الرياضيات والعلوم',
toLocale: 'en',
);
// نتيجة: "The student excels in mathematics and sciences"
معالجة الأخطاء
use App\AI\AiException;
use Illuminate\Http\Client\ConnectionException;
try {
$result = app(AiManager::class)->chat($prompt);
} catch (AiException $e) {
// فشل المزوّد، تجاوز الحدّ، نموذج غير متاح، ...
Log::warning('AI request failed', ['error' => $e->getMessage()]);
return back()->with('error', 'تعذّر معالجة طلبك حالياً. حاول لاحقاً.');
} catch (ConnectionException $e) {
// عدم وصول للخدمة (نادر)
return back()->with('error', 'خدمة الذكاء الاصطناعي غير متاحة.');
}
لا تلتقط
PlatformAiNotApprovedException(HTTP 403). دعها تنتشر — middlewareEnsureAiPlatformApprovedسيُرجع للمستخدم رسالة "بانتظار اعتماد المنصة" مع headerX-Ai-Approval-Required: true، وهذه رسالة معيارية على المنصة.
أمثلة aiMode=own — أدوات الشريك الذاتية
إذا اخترت aiMode=own، فإن أوكتا لا تشارك في إدارة الطلبات. تستخدم مزوّدك مباشرةً مع SDK الخاص به. مثال OpenAI:
use OpenAI\Laravel\Facades\OpenAI;
$response = OpenAI::chat()->create([
'model' => 'gpt-4o',
'messages' => [
['role' => 'system', 'content' => 'أنت مساعد تعليمي'],
['role' => 'user', 'content' => $userInput],
],
]);
$reply = $response->choices[0]->message->content;
أو Anthropic Claude:
$response = Http::withHeaders([
'x-api-key' => config('services.anthropic.key'),
'anthropic-version' => '2023-06-01',
])->post('https://api.anthropic.com/v1/messages', [
'model' => 'claude-opus-4-7',
'max_tokens' => 1024,
'messages' => [
['role' => 'user', 'content' => $userInput],
],
])->json();
$reply = $response['content'][0]['text'];
مسؤوليّاتك في وضع own:
- تخزين مفاتيح API بأمان (متغيرات بيئة، KMS، ...)
- إدارة حدود الاستخدام وفواتير المزوّد
- الإفصاح للمستأجر عن أي بيانات تُرسَل خارج المنصة
- التزام سياسات الخصوصية وحماية البيانات
الوكيل الذكي (AI Agent) مع استخدام الأدوات
التطبيقات التي تريد ذكاءً اصطناعياً يُنفِّذ عمليات (لا يُجيب فقط) تستخدم الوكيل (Agent) مع مفهوم "الأدوات" (Tools). أنت تُعرِّف الأدوات كصفّ PHP، والذكاء الاصطناعي يقرّر متى يستدعيها — مثلاً عند طلب "أضف ٥ لجان بتوزيع ٣٠ طالباً لكل لجنة" يقوم الذكاء بنفسه باستدعاء أداة exams.committees.add وتمرير المعاملات الصحيحة.
هذا الجزء يتطلّب
aiMode=platformمُعلَناً في بيانك ومسجَّلاً على المنصّة (بالنشر أو بالدفع إلى بيئة التجربة) — انظر «كيف يُعتمدplatformفعلاً» أعلاه.
بنية الأداة
كل أداة في تطبيقك هي صفّ PHP يُنفّذ الواجهة App\AI\Contracts\AiTool ويعيش في المجلد Modules/<اسم-التطبيق>/AiTools/. المنصة تكتشفها تلقائياً عند تحميل التطبيق — لا حاجة لتسجيل يدوي.
مثال كامل: AddCommitteesTool في تطبيق الاختبارات
ملف: Modules/Exams/AiTools/AddCommitteesTool.php
<?php
namespace Modules\Exams\AiTools;
use App\AI\Contracts\AiTool;
use App\AI\Exceptions\AiToolException;
use Modules\Exams\Models\ExamCommittee;
use Modules\Exams\Services\CommitteeDistributor;
class AddCommitteesTool implements AiTool
{
public function __construct(
private readonly CommitteeDistributor $distributor,
) {}
public function name(): string
{
return 'exams.committees.add';
}
public function description(): string
{
return 'يُنشئ لجان اختبارات جديدة ويوزّع الطلاب عليها حسب الإعدادات المُعطاة. '
.'استخدم هذه الأداة عندما يطلب المستخدم إضافة لجان أو توزيع طلاب على لجان.';
}
public function parametersSchema(): array
{
return [
'type' => 'object',
'properties' => [
'committee_count' => [
'type' => 'integer',
'minimum' => 1,
'maximum' => 100,
'description' => 'عدد اللجان المطلوب إنشاؤها',
],
'students_per_committee' => [
'type' => 'integer',
'minimum' => 1,
'maximum' => 60,
'description' => 'عدد الطلاب في كل لجنة',
],
'exam_period' => [
'type' => 'string',
'enum' => ['first', 'second', 'final'],
'description' => 'الفترة الامتحانية (first/second/final)',
],
'distribution_strategy' => [
'type' => 'string',
'enum' => ['alphabetical', 'random', 'by_grade'],
'default' => 'alphabetical',
'description' => 'استراتيجية التوزيع',
],
],
'required' => ['committee_count', 'students_per_committee', 'exam_period'],
];
}
public function requiredScopes(): array
{
return ['exams.committees.write', 'education.students.read'];
}
public function handle(array $params): mixed
{
$count = (int) $params['committee_count'];
$perCommittee = (int) $params['students_per_committee'];
$period = (string) $params['exam_period'];
$strategy = $params['distribution_strategy'] ?? 'alphabetical';
$available = $this->distributor->countAvailableStudents($period);
$needed = $count * $perCommittee;
if ($available < $needed) {
throw new AiToolException(
"عدد الطلاب المتاحين ({$available}) أقل من المطلوب ({$needed}). "
."يمكنك تقليل عدد اللجان أو عدد الطلاب لكل لجنة."
);
}
$committees = $this->distributor->createAndDistribute(
count: $count,
studentsPerCommittee: $perCommittee,
period: $period,
strategy: $strategy,
);
return [
'created' => $committees->count(),
'total_students_assigned' => $committees->sum('student_count'),
'period' => $period,
'committee_ids' => $committees->pluck('id')->all(),
'message' => "تم إنشاء {$count} لجنة وتوزيع {$needed} طالب بنجاح",
];
}
}
إضافة واجهة الدردشة في صفحة التطبيق
<x-ai-agent
:title="'مساعد اللجان'"
:placeholder="'مثال: أضف ٥ لجان للدور الأول بتوزيع ٣٠ طالباً لكل لجنة'"
:system-prompt="'أنت مساعد ذكي لإدارة لجان الاختبارات في تطبيق okta-exams. ساعد المستخدم في إنشاء وتوزيع وإدارة لجان الاختبارات. تكلّم بالعربية دائماً.'"
height="600px"
/>
كيف يعمل التدفّق
١. المستخدم يكتب: "أضف ٥ لجان للدور الأول بتوزيع ٣٠ طالباً"
٢. الواجهة ترسل الطلب إلى POST /api/apps/ai/agent/stream
٣. المنصة تستخرج التطبيق الحالي من AppContextManager، تكتشف أدوات Modules/Exams/AiTools/ تلقائياً، تُرشّحها حسب requiredScopes()، وتُرسلها للنموذج
٤. النموذج يردّ بـ {"tool": "exams.committees.add", "args": {...}}
٥. المنصة تنفّذ AddCommitteesTool::handle() وتُرجع النتيجة
٦. الواجهة تعرض كل خطوة لحظياً (running → done)
معالجة الأخطاء
AiToolException: خطأ متعافٍ — يُرسَل للنموذج فيقرّر. استخدمه للتحقّقات.- أي خطأ آخر: يُلغى دور الوكيل بأكمله ويُسجَّل في
partner_installchannel.
الصلاحيات
كل أداة تُعلن requiredScopes(). حلقة الوكيل تُسقط الأدوات التي لا يملك التطبيق صلاحياتها قبل عرضها على النموذج. هذا يعني أن أداة الكتابة تبقى آمنة حتى لو منح المستأجِر صلاحيات قراءة فقط.
حدّ التكرار
كل دور للوكيل محدود بـ ٥ تكرارات. بعد الحدّ، يُطلب من النموذج كتابة ملخّص نهائي.
نصائح للتأليف
١. اسم الأداة: استخدم نمط module.resource.action.
٢. الوصف: اكتبه كتوثيق API.
٣. JSON Schema: كن صارماً (enums, min/max).
٤. القيمة المُرجعة: أعِد بنية بسيطة (array/scalar).
٥. التحقّق المُسبق: ارمِ AiToolException للأخطاء المتوقّعة.
اختبار الأداة محلياً
// tests/Feature/AddCommitteesToolTest.php
$tool = app(\Modules\Exams\AiTools\AddCommitteesTool::class);
$result = $tool->handle([
'committee_count' => 3,
'students_per_committee' => 25,
'exam_period' => 'first',
]);
$this->assertEquals(3, $result['created']);