استخدام بوت الذكاء الاصطناعي عبر واجهة برمجة تطبيقات Discourse

نظرة عامة

يكشف 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)

  1. مقطع السياق (Context Chunk): يوفر البيانات الوصفية (معرف الموضوع، معرف مستخدم البوت، معرف الوكيل).
  2. المقاطع الجزئية (Partial Chunks): تحتوي على مقاطع النص المُبثوطة.

مثال على البث:

{"topic_id":42,"bot_user_id":7,"agent_id":123}

{"partial":"Hello"}

{"partial":" there"}

يجب على العميل دمج حقول partial لبناء الإجابة النهائية.


سير العمل والآثار الجانبية

تنفذ نقطة النهاية الإجراءات التالية أثناء التنفيذ:

  1. ينشئ منشوراً للمستخدم يحتوي على query الخام.
  2. يبث إجابة الذكاء الاصطناعي عبر الاستجابة المقسمة.
  3. ينشئ منشور الرد النهائي للذكاء الاصطناعي مع الإجابة المتراكمة.
  4. للمحادثات الجديدة، قد يقوم بتعيين عنوان تلقائي للموضوع.

الأدوات المخصصة المنفذة من قبل العميل

يمكنك توفير تعريفات الأدوات للسماح للنموذج باستدعاء أدوات خارجية. يتوقف الخادم البث عند استدعاء أداة، مما يسمح للعميل بتنفيذ الأداة ثم استئناف البث.

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.
إعجابَين (2)