انتقل إلى المحتوى الرئيسي

الوحدة 4 — الواجهات الحوارية

مع انتشار النماذج اللغويّة الكبيرة، صارت واجهة المحادثة النمط الأكثر طلبًا. Gradio يُقدّم gr.ChatInterface كصنف مخصّص يوفّر خانة كتابة، وسجلّ رسائل مُتراكم، وأزرارًا لإرسال وإعادة إرسال وحذف، دون أن نبنيها من Blocks مكوّنًا مكوّنًا. هذه الوحدة تفتح المساعد الحواريّ الذي سيرافقنا حتّى الوحدة العاشرة.

الحدّ الأدنى في تسعة أسطر

gr.ChatInterface يحتاج فقط دالّة تأخذ رسالة المستخدم والتاريخ وتُرجع الجواب:

import gradio as gr

def repondre(message, historique):
return f"استلمت رسالتك: {message}. عدد الرسائل السابقة: {len(historique)}."

demo = gr.ChatInterface(
fn=repondre,
title="مساعد بسيط",
description="أوّل واجهة حوار في تسعة أسطر.",
)
demo.launch()

يفتح المستخدم الرابط فيرى واجهة محادثة كاملة الشكل: حقل كتابة، زرّ إرسال، سجلّ رسائل، وأزرار «كرّر» و«امسح». historique هو قائمة من أزواج (user, bot) (أو قواميس بنمط رسائل حسب type="messages")، ينمو تلقائيًّا مع كلّ تبادل.

صيغة التاريخ: messages أم tuples

الافتراضيّ في الإصدارات الحديثة (Gradio 5) صار type="messages"، وهو ما يجعل التاريخ قاموسًا بنمط OpenAI:

[
{"role": "user", "content": "مرحبًا"},
{"role": "assistant", "content": "أهلًا بك، كيف أساعدك؟"},
{"role": "user", "content": "لخّص لي هذه المقالة"},
]

هذه الصيغة تُبسّط التمرير المباشر إلى واجهة نموذج لغويّ. الصيغة القديمة type="tuples" (قائمة من (user, bot)) لا تزال مدعومة لكنّها تُهمَل تدريجيًّا. اعتماد messages منذ اليوم يوفّر إعادة كتابة لاحقًا.

رسالة النظام: تحديد شخصيّة المساعد

رسالة النظام تُحدّد شخصيّة المساعد ونبرته وحدوده. ChatInterface يوفّر additional_inputs لعرضها كحقل يعدّله المستخدم أو المطوّر:

def repondre(message, historique, systeme):
messages = [{"role": "system", "content": systeme}] + historique + [
{"role": "user", "content": message},
]
return f"[سيتصل بالنموذج مع {len(messages)} رسالة]"

demo = gr.ChatInterface(
fn=repondre,
additional_inputs=[
gr.Textbox(
value="أنت مساعد قانونيّ يجيب بإيجاز ودون افتراضات، ويُشير إلى مصادره.",
label="رسالة النظام",
lines=3,
rtl=True,
),
],
title="مساعد قابل للتخصيص",
)
demo.launch()

القيمة الافتراضيّة لـvalue هي ما يُشحن عند فتح الواجهة. المستخدم يستطيع تعديلها لاختبار سلوكات مختلفة، ثمّ يستعيد الإصدار الذي يرضيه.

الاتّصال بنموذج محلّيّ: Ollama من الدورة 29

الوحدة السابقة تركت المساعد بمحادثة وهميّة. لنبنِ واجهة حقيقيّة تتّصل بنموذج Ollama يعمل محلّيًّا:

import gradio as gr
from openai import OpenAI

client = OpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama", # Ollama يقبل أيّ قيمة
)

def repondre(message, historique, systeme):
messages = [{"role": "system", "content": systeme}] + historique + [
{"role": "user", "content": message},
]
reponse = client.chat.completions.create(
model="qwen2.5:7b-instruct-q4_K_M",
messages=messages,
temperature=0.2,
max_tokens=400,
)
return reponse.choices[0].message.content

gr.ChatInterface(
fn=repondre,
additional_inputs=[
gr.Textbox(
value="أنت مساعد يجيب بإيجاز باللغة العربيّة الفصحى.",
label="رسالة النظام",
lines=2,
rtl=True,
),
],
title="مساعد محلّيّ عبر Ollama",
description="يعمل بالكامل على جهازك — لا يخرج شيء إلى السحابة.",
theme="soft",
).launch()

نستعمل واجهة OpenAI المتوافقة التي يعرضها Ollama على /v1 (كما رأيناها في الدورة 29)، فيصير التبديل بين نموذج محلّيّ ونموذج سحابيّ تغييرًا في base_url وapi_key فقط. هذه المرونة قيمة كبيرة عند العمل لعملاء يريدون نسخة محلّيّة ونسخة سحابيّة من العرض نفسه.

الاتّصال بواجهة سحابيّة

للنموذج السحابيّ، يبقى الكود نفسه تقريبًا:

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

def repondre(message, historique, systeme):
messages = [{"role": "system", "content": systeme}] + historique + [
{"role": "user", "content": message},
]
reponse = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
temperature=0.2,
)
return reponse.choices[0].message.content

قاعدة أمان: المفتاح لا يُكتب في الشيفرة، بل يُقرأ من متغيّر بيئة. هذا مهمّ خصوصًا في الوحدة 9 حين ننشر على مساحة Hugging Face، حيث نستعمل «الأسرار» لتخزينه.

دعم اليمين إلى اليسار

للجمهور العربيّ، اتّجاه الكتابة من اليمين إلى اليسار جزء من الاحترافيّة. Gradio يدعمه على مكوّنات النصّ بخاصيّة rtl=True، وعلى المستوى العامّ عبر السمة direction. الأبسط أن نُطبّق rtl على الحقول ذات النصّ العربيّ، ونترك الباقي على حاله:

gr.Textbox(rtl=True, label="اكتب هنا")

عند تخصيص المظهر عبر CSS مباشر، يمكن إجبار direction: rtl على كامل الواجهة، لكنّ هذا يقلب أيضًا مواضع الأزرار وقد يُنتج شعورًا غريبًا. الأنسب البقاء على تخطيط قياسيّ مع نصوص من اليمين إلى اليسار داخل الحقول.

قصر التاريخ لتفادي الانفجار

مع طول الحوار، يصير التاريخ ضخمًا فيتضخّم عدد الرموز في كلّ نداء ويرتفع الكلفة والزمن. حلّ عمليّ هو الحفاظ على آخر k تبادلات فقط:

K = 6

def repondre(message, historique, systeme):
hist_court = historique[-K:] # آخر ستّ رسائل فقط
messages = [{"role": "system", "content": systeme}] + hist_court + [
{"role": "user", "content": message},
]
...

هذا كافٍ للمساعدين العامّين. للمهامّ التي تتطلّب ذاكرة طويلة (متابعة قضيّة قانونيّة عبر جلسات) نستعمل تلخيصًا دوريًّا أو تخزينًا خارجيًّا، وهذا موضوع الدورة 30 عن الوكلاء لا هذه الدورة.

تعامل صريح مع الأخطاء

إذا سقط النموذج (خادم Ollama متوقّف، أو مفتاح API منتهٍ) وأعادت الواجهة استثناءً غير معالج، يرى المستخدم رسالة خطأ عامّة تُخيفه. أحسن معاملة: try/except حول نداء النموذج، وإرجاع رسالة عربيّة واضحة كـ«الخدمة غير متاحة حاليًّا، جرّب بعد قليل».

الخلاصة

  • gr.ChatInterface يبني واجهة محادثة كاملة من دالّة واحدة تأخذ الرسالة والتاريخ.
  • صيغة messages (قائمة أدوار) تُطابق واجهة OpenAI مباشرة وتصير الافتراضيّة في Gradio 5.
  • رسالة النظام تُحدّد شخصيّة المساعد، وتُعرَض عبر additional_inputs قابلة للتحرير.
  • التبديل بين محلّيّ وسحابيّ يقتصر على تغيير base_url وapi_key بفضل واجهة Ollama المتوافقة مع OpenAI.
  • rtl=True يضمن اتّجاهًا صحيحًا للنصّ العربيّ داخل حقول الإدخال والعرض.

الوحدة التالية: البثّ التدريجي للردود بواسطة مولّدات Python، فلا ينتظر المستخدم عشر ثوانٍ صامتة قبل أن يرى الحرف الأوّل.