تخطي إلى المحتوى

المساعد المضمن

هذه الصفحة مترجمة من الإنجليزية. اقرأ الصفحة الإنجليزية

الحالة: منفذ خلف ميزة Cargo المسماة assistant في zeyn.

يمكن أن يحمل تطبيق Zeyn مساعده الذكي الخاص: محادثة في كل صفحة تجيب ببيانات التطبيق وتعمل على شاشاته وواجهاته البرمجية (APIs) بصفة المستخدم المسجل الدخول. تستطيع التطبيقات المبنية بـ Zeyn تفعيله لعملائها، وربط النموذج الذي يريده هؤلاء العملاء، وحفظ سجل المحادثات حيث يحتاجون.

zeyn = { version = "0.0.2", features = ["assistant"] }

بدون الميزة لا يُصرَّف شيء منه: لا عميل HTTP ولا مسارات إضافية ولا ذاكرة إضافية.

1. الإعداد

use zeyn::assistant::{Assistant, OpenAiCompatible};

let model = OpenAiCompatible::new(config.require("assistant.base_url")?, config.require("assistant.model")?)
    .api_key(config.secret("assistant.api_key")?);

App::new("Lifts")
    .assistant(
        Assistant::new(model)
            .permission("lifts.assistant.use")
            .instructions("You help service engineers plan elevator maintenance.")
            .suggest(work_orders, "Which lifts are overdue for service?")
            .suggest_anywhere("What can you do?"),
    )

هذا كل شيء. تحصل كل صفحة على زر عائم يفتح المحادثة في لوحة جانبية. ويمكن لشاشة أن تعرض المحادثة بنفسها:

#[screen(route = "/assistant", permission = "lifts.assistant.use", ai = false)]
pub fn assistant_page() -> Screen {
    Screen::new("Assistant").body(AssistantChat::page())
}

AssistantChat::panel() يناسب الوضع بجوار محتوى آخر. وAssistant::launcher(false) يُسقط الزر العائم، فتظهر المحادثة حيث تضعها الشاشة فقط.

الخيارالافتراضي
permission("...")كل مستخدم مسجل الدخول
instructions("...")لا شيء؛ يكتبها مطورو التطبيق، ولا يكتبها المستخدمون أبداً
suggest(screen, prompt), suggest_anywhere(prompt)لا رقائق اقتراح ابتدائية
speech_to_text(p), text_to_speech(p)واجهة Web Speech API في المتصفح
no_voice()الميكروفون والقراءة بصوت عالٍ مفعّلان
store(s)MemoryChatStore::default()
read_only()يستطيع المساعد أن يحفظ أيضاً، بعد التأكيد
rate_limit(RateLimit)20 طلباً في الدقيقة لكل مستخدم
token_limit(Some(RateLimit))200,000 رمز (token) في الساعة لكل مستخدم
limits(Limits { .. })انظر القسم 6

2. النماذج

يعمل أي نوع ينفّذ LlmProvider: دالة واحدة تبدأ جولة متدفقة وتعيد قناة محدودة من ChatEvent (قطع نصية واستدعاءات أدوات واستهلاك). المدمج منها:

المزوّدما يغطيه
OpenAiCompatible::new(base_url, model)OpenAI (https://api.openai.com/v1) وواجهة Azure OpenAI v1 (https://NAME.openai.azure.com/openai/v1، مع النشر (deployment) بوصفه النموذج، و.azure() لترويسة api-key) وOllama (http://localhost:11434/v1) وvLLM وLiteLLM وغيرها من الخوادم المتوافقة. .legacy_max_tokens() للخوادم التي لا تعرف max_completion_tokens
Anthropic::new(model)واجهة Anthropic Messages (.base_url(...) لبوابة). تُرسل كتل الاستدلال (reasoning) كما هي ضمن الدور الواحد
ScriptedProviderللاختبارات والعروض والتطوير دون اتصال: قواعد بدلاً من استدعاء شبكي، وتدفق كلمة كلمة، واستدعاءات أدوات حقيقية

تأتي المفاتيح من قيم Secret (Config::secret)، وتبقى على الخادم ولا تظهر أبداً في رسائل الخطأ. تُسجَّل أخطاء المزوّد على الخادم؛ ويرى المستخدم رسالة عامة.

توجيه showcase إلى نموذج حقيقي لا يتطلب إلا الإعداد:

NORTHWIND_ASSISTANT__PROVIDER=openai
NORTHWIND_ASSISTANT__BASE_URL=https://api.openai.com/v1
NORTHWIND_ASSISTANT__MODEL=gpt-4.1-mini
NORTHWIND_ASSISTANT__API_KEY=...

أو NORTHWIND_ASSISTANT__PROVIDER=anthropic مع NORTHWIND_ASSISTANT__MODEL=claude-opus-5-5، أو عنوان Ollama دون مفتاح. وNORTHWIND_ASSISTANT__PROVIDER=off يعطّله.

3. الكلام

افتراضياً يستخدم الميكروفون التعرف على الكلام في المتصفح، وتستخدم "القراءة بصوت عالٍ" تركيب الكلام في المتصفح: بلا كلفة على الخادم، ولا يغادر أي صوت المتصفح نحو التطبيق. أما للكلام عبر الخادم:

Assistant::new(model)
    .speech_to_text(OpenAiTranscription::new(base, "whisper-1").api_key(key.clone()))
    .text_to_speech(OpenAiSpeech::new(base, "tts-1", "alloy").api_key(key))

كلاهما يتحدث نقطتي النهاية /audio/transcriptions و/audio/speech المتوافقتين مع OpenAI، وهما ما تقدمه أيضاً خوادم Whisper والكلام المحلية. التسجيلات محدودة (4 ميغابايت و60 ثانية في المتصفح)؛ وصوت الكلام يتدفق إلى المتصفح فور وصوله.

4. السجل

يحفظ ChatStore المحادثات بمفتاح المستأجر والمستخدم (ChatKey)، فلا يتشارك مستأجران محادثة واحدة أبداً:

pub trait ChatStore: Send + Sync + 'static {
    fn load(&self, key: &ChatKey, limit: usize) -> BoxFuture<Result<Vec<ChatMessage>>>;
    fn append(&self, key: &ChatKey, messages: Vec<ChatMessage>) -> BoxFuture<Result<()>>;
    fn clear(&self, key: &ChatKey) -> BoxFuture<Result<()>>;
}

MemoryChatStore::new(max_users, max_messages) هو الافتراضي (1,000 محادثة من 60 رسالة، يخرج الأقل استخداماً مؤخراً أولاً، ويضيع عند إعادة التشغيل). لقاعدة بيانات، نفّذ الدوال الثلاث على جدول فيه المستأجر والمستخدم والموضع والرسالة بصيغة JSON. الرسائل للإضافة فقط؛ والمخزن هو من يقرر مدة الاحتفاظ. وتعيش التأكيدات المعلقة في السجل أيضاً، فلا يحتفظ الخادم بأي حالة أخرى لكل مستخدم.

5. الأدوات والإجراءات

يرى النموذج الأدوات نفسها التي يراها MCP وWebMCP (انظر docs/runtime-protocol.md القسم 9)، وتُبنى للمستخدم المسجل الدخول في كل مرة: عمليات بحث الشبكات وحفظ النماذج وإجراءات API التي يحق للمستخدم استخدامها، مطروحاً منها كل ما وُسم بـ ai = false، مع أوصاف من تعليقات /// وإرشادات ai = "...". وتُضاف أداتان مدمجتان:

  • navigate: يفتح شاشة ذات مسار يحق للمستخدم فتحها، دون تحميل صفحة (zeyn:navigate)، ويستطيع ترشيح شبكتها (zeyn:filter). ثم تُحمَّل الشبكة عبر POST /_zeyn/grid، حيث يطبق الخادم فحوصه المعتادة.
  • suggest: مطالبات متابعة تُعرض رقائق.

يمر كل استدعاء عبر مسار الإجراء العادي مع سياق موسوم بأنه وكيل (agent): وصول الشاشة، وحراس الميزة (Feature)، وصلاحية الإجراء، والمعترضات، وحدود المعدل، وحد الوقت، والتدقيق (يُسجَّل كل استدعاء من المساعد مع via_agent: true)، والتقنيع.

التأكيد. تعمل القراءات فوراً. أما أي شيء يكتب (حفظ النماذج، وإجراءات API التي مدخلها ليس DataQuery) وكل إجراء موسوم بـ #[action(confirm)] فيوقف الجولة ويعرض بطاقة تأكيد فيها ما سيُشغَّل ووسائطه. لا يشغّله إلا نقر المستخدم (POST /_zeyn/assistant/confirm، مع CSRF، لاستدعاء معلق في سجل ذلك المستخدم نفسه)، ومرة واحدة. رسالة مثل "I confirm" مجرد نص: الرسالة الجديدة تغلق الاستدعاء المعلق على أنه لم يُشغَّل.

6. نموذج الأمان

الخطرما تفعله المجموعة
تسرب مفاتيح APIالمفاتيح قيم Secret على الخادم؛ والمتصفح لا يتحدث إلا مع التطبيق
نموذج يتصرف فوق صلاحيات المستخدمتُبنى الأدوات من صلاحيات المستخدم؛ ويُفحص كل استدعاء من جديد على الخادم
كتابات لم يردها المستخدمالكتابات وإجراءات confirm تحتاج إلى نقر المستخدم؛ ولا يستطيع النموذج أن يؤكد
وصول بيانات مقنَّعة إلى النموذجالحقول المقنَّعة تبقى مقنَّعة دائماً في استدعاءات الوكيل، حتى للمستخدمين الذين يحق لهم كشفها
حقن المطالبات عبر البياناتنتائج الأدوات نص عادي داخل محددات <tool_result> مع تهريب < و>؛ وتنص مطالبة النظام على أنها بيانات. يعيد الخادم بناء مطالبة النظام في كل جولة من قواعد ثابتة وتعليمات التطبيق واسم الشاشة، ولا يبنيها أبداً من بيانات المستخدم أو الصفحة أو الأدوات
تسريب البيانات عبر الروابطتُعرض الإجابات بمجموعة فرعية صغيرة من Markdown تُبنى بـ textContent: بلا HTML ولا روابط ولا صور
كلفة خارجة عن السيطرةحد معدل الطلبات لكل مستخدم، وحد الرموز لكل مستخدم، ورموز خرج محدودة، و6 جولات على الأكثر و4 استدعاءات أدوات في الجولة
مدخلات كبيرةرسائل حتى 2,000 حرف، و30 رسالة سجل في الجولة، ونتائج أدوات تُقصّ عند 6,000 حرف، وتسجيلات حتى 4 ميغابايت
الطلبات عبر المواقعكل POST يحتاج إلى ترويسة CSRF ويُحتسب ضمن حد الكتابة في التطبيق
إجراءات بلا مساءلةيُدقَّق كل استدعاء أداة من المساعد مع via_agent

ما لا تفعله: الحكم بصحة إجابة النموذج. أخبر المستخدمين بذلك (واللوحة تفعل)، وأبقِ الإجراءات المهمة خلف confirm.

7. التضمين للعملاء

التطبيق الذي يبيع المساعد لعملائه يريد عادة:

  • permission("app.assistant.use") وفحص وحدة الترخيص في حارس، حتى لا يراه إلا المستأجرون الدافعون؛
  • نموذجاً لكل مستأجر: نفّذ LlmProvider موجّهاً صغيراً يختار مزوّد العميل من الإعداد بحسب مستأجر ctx، أو شغّل نموذجاً واحداً للجميع؛
  • ChatStore على قاعدة بيانات التطبيق، بقواعد الاحتفاظ الخاصة بالعميل؛
  • instructions(...) تصف المجال (شركة مصاعد: وحدات ومواقع وفواصل صيانة)، وإرشادات ai = "..." على الإجراءات التي تحتاجها؛
  • token_limit مضبوطاً على الخطة التي يدفع العميل ثمنها.

8. نقاط النهاية

نقطة النهايةما تفعله
GET /_zeyn/assistant/start?screen={ messages, pending, suggestions, stt, tts, max }
POST /_zeyn/assistant/chat { message, screen }تبث أحداث application/x-ndjson
POST /_zeyn/assistant/confirm { id, approve }تشغّل الاستدعاء المعلق أو ترفضه، ثم تبث كما في chat
POST /_zeyn/assistant/clearتحذف المحادثة؛ 204
POST /_zeyn/assistant/stt (جسم صوتي){ text }؛ و404 عند غياب تحويل الكلام إلى نص على الخادم
POST /_zeyn/assistant/tts { text }صوت متدفق؛ و404 عند غياب تحويل النص إلى كلام على الخادم

الأحداث، كائن JSON واحد في كل سطر: {t:"text",v} و{t:"tool",name,id} و{t:"navigate",url,screen,filters} و{t:"suggest",v} و{t:"confirm",id,tool,title,question,details} و{t:"resolved",id,approved} و{t:"error",message} و{t:"done"}.

يُحمَّل assistant.js وassistant.css فقط للمستخدمين الذين يحق لهم استخدام المساعد. سجل المحادثة منطقة حية role="log" (مع aria-busy أثناء تدفق الإجابة)، وEnter يرسل، وShift+Enter يبدأ سطراً جديداً، وEscape يغلق اللوحة الجانبية ويعيد التركيز إلى الزر، وبطاقة التأكيد تضع التركيز على إلغاء (Cancel) أولاً.

9. الكلفة

لا يحتفظ المساعد بأي حالة لكل مستخدم في الذاكرة سوى مخزن السجل المحدود والتدفقات الجارية. تتدفق الإجابات من طرف إلى طرف عبر قنوات من 32 عنصراً. القياس على showcase (بناء debug، Windows، في حالة خمول بعد خمس صفحات محملة): مجموعة عمل 14.4 ميغابايت وذاكرة خاصة 3.13 ميغابايت والمساعد معطل، و14.5 ميغابايت و3.19 ميغابايت وهو مفعّل (نحو 100 كيلوبايت زيادة).