نظرة عامة
يكشف Discourse AI عن نقطة نهاية (Endpoint) للإدارة/الواجهة البرمجية (API) لبث ردّ وكيل الذكاء الاصطناعي (AI Agent) عبر استجابة HTTP خام مقسّمة (chunked).
- البروتوكول: ترميز نقل HTTP الخام المقسّم (وليس أحداث الخادم المرسلة Server-Sent Events).
- التنفيذ: يستولي على جسر Rack (Hijacks the Rack socket) لبث كائنات JSON مفصولة بعلامات سطر جديد.
- الآثار الجانبية: هذا ليس مجرد واجهة برمجة لإكمال النصوص؛ بل يقوم بإنشاء منشورات حقيقية لرسائل Discourse الخاصة (PM).
تفاصيل نقطة النهاية
- الموقع:
plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_agents_controller.rb:175-272 - المسار:
POST /admin/plugins/discourse-ai/ai-agents/stream-reply.json - نطاق مفتاح API:
ai:stream_completion(مسجّل فيplugins/discourse-ai/lib/ai_bot/entry_point.rb:283-286)
ترويسات الطلب (Request Headers)
POST /admin/plugins/discourse-ai/ai-agents/stream-reply.json
Api-Key: <your_api_key>
Api-Username: <your_username>
Content-Type: application/json
معاملات جسم الطلب (Request Body Parameters)
| المعامل | النوع | مطلوب | الوصف |
|---|---|---|---|
agent_id |
عدد صحيح (Integer) | اختياري* | المعرّف الخاص بالوكيل. |
agent_name |
نص (String) | اختياري* | معرّف بديل للوكيل. |
query |
نص (String) | نعم | استعلام/سؤال المستخدم. |
username |
نص (String) | مطلوب إذا تم حذف user_unique_id |
يُستخدم في حال احتاجت رسالة المحادثة الخاصة (PM) إلى الارتباط بمستخدم موجود. |
user_unique_id |
نص (String) | مطلوب إذا تم حذف username |
يحدد المستخدم النهائي. ينشئ/يعيد استخدام مستخدم مؤقت (staged user) بمفتاح حقل مخصص ai-stream-conversation-unique-id. |
preferred_username |
نص (String) | اختياري* | اسم المستخدم (إذا لم يُستخدم user_unique_id). |
topic_id |
عدد صحيح (Integer) | اختياري | مواصلة محادثة PM موجودة مسبقاً. |
custom_instructions |
نص (String) | اختياري | يُضاف إلى سياق موجه الوكيل (agent prompt context). |
ملاحظة: يجب تحديد المستخدم النهائي إما عن طريق
username(مستخدم Discourse موجود) أوuser_unique_id.
تنسيق الاستجابة
يعيد الخادم استجابة 200 OK مع Transfer-Encoding: chunked. يتكون البث من كائنات JSON مفصولة بعلامات سطر جديد.
ترويسات HTTP
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Cache-Control: no-cache, no-store, must-revalidate
Connection: close
X-Accel-Buffering: no
X-Content-Type-Options: nosniff
بنية حمولة البث (Stream Payload Structure)
- مقطع السياق (Context Chunk): يوفر البيانات الوصفية (معرف الموضوع، معرف مستخدم البوت، معرف الوكيل).
- المقاطع الجزئية (Partial Chunks): تحتوي على مقاطع النص المُبثوطة.
مثال على البث:
{"topic_id":42,"bot_user_id":7,"agent_id":123}
{"partial":"Hello"}
{"partial":" there"}
يجب على العميل دمج حقول partial لبناء الإجابة النهائية.
سير العمل والآثار الجانبية
تنفذ نقطة النهاية الإجراءات التالية أثناء التنفيذ:
- ينشئ منشوراً للمستخدم يحتوي على
queryالخام. - يبث إجابة الذكاء الاصطناعي عبر الاستجابة المقسمة.
- ينشئ منشور الرد النهائي للذكاء الاصطناعي مع الإجابة المتراكمة.
- للمحادثات الجديدة، قد يقوم بتعيين عنوان تلقائي للموضوع.
الأدوات المخصصة المنفذة من قبل العميل
يمكنك توفير تعريفات الأدوات للسماح للنموذج باستدعاء أدوات خارجية. يتوقف الخادم البث عند استدعاء أداة، مما يسمح للعميل بتنفيذ الأداة ثم استئناف البث.
1. الطلب الأولي مع الأدوات
تضمّن custom_tools في جسم الطلب:
{
"agent_id": 123,
"query": "What's the weather?",
"user_unique_id": "external-user-42",
"custom_tools": [
{
"name": "client_weather",
"description": "Gets weather from the client runtime",
"parameters": [
{
"name": "city",
"description": "City to fetch weather for",
"type": "string",
"required": true
}
]
}
]
}
2. حدث استدعاء الأداة (Tool Call Event)
إذا استدعى النموذج أداة، يبث البث حدث tool_calls ويتوقف:
{
"event": "tool_calls",
"tool_calls": [
{
"id": "tool_1",
"name": "client_weather",
"parameters": { "city": "Austin" }
}
],
"resume_token": "..."
}
يخزن الخادم حالة المحادثة في Redis في هذه النقطة.
3. الاستئناف مع نتائج الأدوات
ينفذ العميل الأداة ويستأنف البث:
{
"resume_token": "...",
"tool_results": [
{
"tool_call_id": "tool_1",
"content": { "temperature_c": 23 }
}
]
}
يعيد الخادم تحميل حالة الموجه المحفوظة، ويدخل نتيجة الأداة، ويستمر في التوليد، ويبث المزيد من المقاطع partial.
حدود الأدوات
- الحد الأقصى للأدوات المخصصة: 20
- الحد الأقصى لنتائج الأدوات: 20
- الحد الأقصى لحجم تعريف الأداة المخصصة: 10,000 بايت
- الحد الأقصى لحجم محتوى نتيجة الأداة: 100 ك.ب
- مدة صلاحية الاستئناف (Resume TTL): 15 دقيقة
- الحد الأقصى لجولات الاستئناف: 10
مراجع التنفيذ
- التحكم (Controller):
plugins/discourse-ai/app/controllers/discourse_ai/admin/ai_agents_controller.rb - المُبث (Streamer):
plugins/discourse-ai/lib/ai_bot/response_http_streamer.rb - جلسة الأدوات المخصصة:
plugins/discourse-ai/lib/ai_bot/stream_reply_custom_tools_session.rb
أمثلة الاختبار
انظر إلى plugins/discourse-ai/spec/requests/admin/ai_agents_controller_spec.rb للحصول على أمثلة شاملة:
- محادثة مُبثوطة جديدة: الأسطر 1248-1356
- أدوات مخصصة + رمز استئناف: الأسطر 1358-1448
- استدعاءات أدوات متوازية: الأسطر 1467-1590
مثال على التنفيذ
سكريبت Ruby
require 'net/http'
require 'json'
require 'uri'
# Configuration
DISCOURSE_URL = '<your site URL>'
API_KEY = '<your API key>'
USERNAME = '<your username>'
AGENT_ID = -1 # Or use agent_name
QUERY = "Hello, how are you today?"
USER_UNIQUE_ID ='<leave empty if want the PM to be sent to the USERNAME user>'
# Helper to create the URI
uri = URI("#{DISCOURSE_URL}/admin/plugins/discourse-ai/ai-agents/stream-reply.json")
# Create the HTTP request
request = Net::HTTP::Post.new(uri)
request['Api-Key'] = API_KEY
request['Api-Username'] = USERNAME
request['Content-Type'] = 'application/json'
# Prepare the request body
body = {
agent_id: AGENT_ID,
query: QUERY,
## uncomment the below line if you want to use an existing user for the conversation. Also, `username` takes precedence over `user_unique_id` if passed.
# username: USERNAME,
## use the below field along with `user_unique_id` in order to create a new staged user. When using this, skip passing the `username`.
# preferred_username: USER_UNIQUE_ID
}
body[:user_unique_id] = USER_UNIQUE_ID unless USER_UNIQUE_ID.empty?
body = body.to_json
request.body = body
http = Net::HTTP.new(uri.hostname, uri.port)
http.use_ssl = (uri.scheme == 'https')
http.request(request) do |response|
# Check if the response is successful
if response.code == '200'
puts "Stream started successfully."
puts "Response headers: #{response.to_hash}"
puts "Streaming content:"
# Read the chunked response
response.read_body do |chunk|
# The response is newline-separated JSON objects
chunk.each_line do |line|
line = line.strip
next if line.empty?
begin
json = JSON.parse(line)
if json['topic_id']
puts "\n--- Context Received ---"
puts "Topic ID: #{json['topic_id']}"
puts "Bot User ID: #{json['bot_user_id']}"
puts "Agent ID: #{json['agent_id']}"
elsif json['partial']
# Stream the partial content
print json['partial']
elsif json['event'] == 'tool_calls'
puts "\n--- Tool Call Received ---"
puts JSON.pretty_generate(json)
# Here you would handle tool execution and resume
# For this example, we just print it
end
# puts "<new word>"
rescue JSON::ParserError => e
puts "\nError parsing JSON: #{e.message}"
puts "Raw line: #{line}"
end
end
end
puts "\n--- Stream Finished ---"
else
puts "Error: #{response.code} #{response.message}"
puts "Response body: #{response.body}"
end
end
ملاحظات
- إذا كنت تريد أن تكون المحادثة باسم مستخدم مؤقت جديد، قم بتمرير
unique_user_idوpreferred_usernameكاسم المستخدم الجديد المطلوب وتخطي حقلusername. - إذا كنت تريد إجراء المحادثة باستخدام مستخدم موجود، قم بتمرير
usernameلذلك المستخدم وتخطي حقولunique_user_idوpreferred_username.