بناء روبوت أسئلة وأجوبة
يبني هذا الدليل جوهر روبوت يجيب عن الأسئلة من قاعدة معرفتك: في أداة محادثة، أو في مركز المساعدة لديك، أو داخل منتجك. يجد المساعد المعرفة المطلوبة ويجيب، مستشهدًا بالبطاقات التي استخدمها؛ وشيفرتك ترسل السؤال وتعرض النتيجة.
تحتاج إلى مفتاح له نطاقا الصلاحيات search وask. لا تستخدم الإجابة إلا البطاقات التي
يُسمح للمفتاح بقراءتها، فدور المفتاح وملف تعريف البيانات والمجموعات تقرر ما يستطيع الروبوت الحديث عنه. وأعطِ روبوت
العملاء مفتاحًا لا يرى إلا المجموعات العامة.
ثلاث طرق للحصول على إجابة
| نقطة النهاية | ما تعيده | متى تستخدمها |
|---|---|---|
POST /v1/search | المقاطع المطابقة، الأفضل أولًا. بلا إجابة مكتوبة. | مربع بحث، أو «مقالات ذات صلة»، أو ترتيبك الخاص للنتائج. |
POST /v1/ask | إجابة كاملة مع مصادرها، في استجابة واحدة. | الروبوتات التي تنشر رسالة واحدة لكل إجابة؛ ومهام الخادم الخلفية. |
POST /v1/chat | الإجابة على شكل تدفق من الأحداث أثناء كتابتها. | واجهات المحادثة، حيث ينبغي أن يرى المستخدم الإجابة وهي تظهر. |
البحث
curl -X POST https://api.sursa.com/v1/search \
-H "Authorization: Bearer $SURSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "how do I return a damaged item", "collection": "support"}'كل نتيجة مقطع من بطاقة: doc_id وdoc_name للبطاقة، وheading للقسم، وtext للمقطع، والصفحات أو صفوف الجدول
التي وُجد فيها، وثقة البطاقة وتوثيقها.
يختار المعامل collection المجموعة التي يجري فيها البحث؛ ومن دونه تُستخدم المجموعة التي اسمها default. ويضيّق
المعاملان tags وcard_ids البحث إلى البطاقات التي تحمل تلك الوسوم أو إلى بطاقات بعينها.
السؤال والحصول على إجابة كاملة
curl -X POST https://api.sursa.com/v1/ask \
-H "Authorization: Bearer $SURSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "How do I return a damaged item?", "collection": "support"}'نص الإجابة في answer بصيغة Markdown. وإحالاته، مثل [1] أو [1, 3]، تطابق ref في إدخالات sources؛ اربط كل
إحالة بالبطاقة (doc_id) كي يتمكن القارئ من التحقق من الإجابة.
قد يظهر في النص رقم تعذّر تأكيده من المستندات على شكل علامة مثل ⟦n:a1⟧. ويصف الحقل verification.items كل
علامة: قيمتها marker، والسبب reason، والنص original. وإن كانت شيفرتك لا تعالج العلامات، فضع مكان كل علامة
نصها من الحقل original.
حين لا توجد إجابة
الاستجابة دائمًا بالحالة 200. وحين لا يستطيع المساعد الإجابة، يبيّن الحقل empty السبب، ويحمل answer رسالة
قصيرة يمكنك عرضها:
empty | المعنى |
|---|---|
no_hits | لا شيء في قاعدة المعرفة يطابق السؤال. |
restricted | الموضوع موجود في بطاقات لا يُسمح للمفتاح بقراءتها. |
guardrail | السؤال خارج ما تسمح المؤسسة للمساعد بالإجابة عنه. |
no_data_for_period | السؤال عن فترة لا توجد عنها مستندات. |
القيمة no_hits إشارة مفيدة: فهي تُظهر ما يسأل عنه مستخدموك ولا تغطيه قاعدة المعرفة بعد.
الوضعان Fast وPrecise
مرّر "quality": "precise" (دقيق) للحصول على إجابة أكثر تعمقًا عن سؤال صعب، مقابل وقت أطول. والقيمة الافتراضية
"fast" (سريع) تناسب معظم الأسئلة.
عرض الإجابة تدفقًا
في واجهة المحادثة، اعرض الإجابة أثناء كتابتها. تجيب العملية POST /v1/chat مع
"stream": true بالصيغة text/event-stream: حدث واحد لكل رسالة، ولكل حدث سطر id: (رقمه في التدفق، بدءًا من 1)
وسطر data: يحمل JSON. ويبيّن الحقل type في JSON نوع الحدث.
id: 1
data: {"type":"meta","sources":3,"timings":{…},"answer_id":"…"}
id: 2
data: {"type":"sources","sources":[{"ref":1,"doc_id":"8c1d…","doc_name":"Returns","text":"…"}]}
id: 3
data: {"type":"token","text":"Send a photo of the item "}
id: 4
data: {"type":"token","text":"within 7 days [1]."}
id: 5
data: {"type":"done","answer":"Send a photo of the item within 7 days [1].","question_id":9812,"sources":[…]}
type | ما العمل |
|---|---|
meta | بدأت الإجابة. احفظ answer_id إن كنت تسجّل الإجابات. |
sources | البطاقات التي تستند إليها الإجابة. قد يصل أكثر من مرة، والأخير هو المعتمد. |
token | جزء من نص الإجابة. ألحقه بما تعرضه. |
done | اكتملت الإجابة. وله الحقول نفسها التي في استجابة /v1/ask. |
error | توقفت الإجابة. ينتهي التدفق بلا done. انظر أدناه. |
followups | بعد done: أسئلة تالية مقترحة يمكنك عرضها أزرارًا. |
| أي نوع آخر | تجاهله. قد تظهر أنواع أحداث جديدة في أي وقت. |
السطر الذي يبدأ بالرمز : (مثل : ping، الذي يُرسَل كل 15 ثانية أثناء إعداد الإجابة) تعليق. تجاهله.
يحمل الحقل messages المحادثة حتى الآن، الأقدم أولًا، وتنتهي بسؤال المستخدم. أرسل المداخلات الأخيرة القليلة كي
تُفهم أسئلة المتابعة مثل «وماذا عن الطلبات من الخارج؟».
// Streams an answer and calls onText with each piece of text. Node.js 20+ or a browser.
export async function streamAnswer({ messages, collection, requestId, lastEventId, onText }) {
const response = await fetch("https://api.sursa.com/v1/chat", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SURSA_API_KEY}`,
"Content-Type": "application/json",
Accept: "text/event-stream",
...(lastEventId ? { "Last-Event-ID": String(lastEventId) } : {}),
},
body: JSON.stringify({ messages, collection, stream: true, request_id: requestId }),
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
const decoder = new TextDecoder();
let buffer = "";
let event = { id: 0, data: "" };
let done = null;
for await (const piece of response.body) {
buffer += decoder.decode(piece, { stream: true });
let newline;
while ((newline = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, newline).replace(/\r$/, "");
buffer = buffer.slice(newline + 1);
if (line.startsWith("id:")) event.id = Number(line.slice(3).trim());
else if (line.startsWith("data:")) event.data += line.slice(5).trim();
else if (line === "" && event.data) {
const message = JSON.parse(event.data);
if (message.type === "token") onText(message.text, event.id);
else if (message.type === "done") done = message;
else if (message.type === "error") throw Object.assign(new Error(message.message), { event: message, id: event.id });
event = { id: 0, data: "" };
}
}
}
if (!done) throw new Error("the stream ended before the answer was complete"); // resume it
return done;
}بلا "stream": true، تعيد العملية POST /v1/chat الإجابة كاملة بصيغة JSON، مثل /v1/ask.
استئناف تدفق منقطع
الاتصالات تنقطع: تتغيّر شبكة الهاتف المحمول، أو تنتهي مهلة خادم وكيل، أو يُعاد تشغيل خادمك. أعطِ كل سؤال قيمة
request_id، فتنجو الإجابة من الانقطاع:
- مع
request_idتُعَدّ الإجابة باستقلال عن الاتصال. فالانقطاع لا يوقفها، بل تكتمل وتُحفظ. - للاستئناف، أرسل الطلب نفسه مرة أخرى، بقيمة
request_idنفسها، ورقم آخر حدث تلقيته في الترويسةLast-Event-ID. - استخدم قيمة
request_idواحدة (مثل UUID) لكل سؤال، والقيمة نفسها في كل إعادة محاولة لذلك السؤال. والسؤال الجديد يحصل علىrequest_idجديد.
ما يتلقاه الطلب المكرر يعتمد على المرحلة التي بلغتها الإجابة:
| الحالة | ما يتلقاه الطلب المكرر |
|---|---|
| الإجابة لا تزال قيد الكتابة، أو انتهت للتو | الأحداث بعد Last-Event-ID، بالأرقام نفسها: يستمر التدفق ببساطة. |
| انتهت الإجابة قبل بضع دقائق | الإجابة المحفوظة على شكل حدث token بالنص كله وحدث done، مرقّمين بدءًا من 1. ولا يُطرح السؤال مرة أخرى. |
| لا يمكن أن يستمر التدفق من ذلك الرقم | الإجابة من البداية، مرقّمة بدءًا من 1. |
تعالج شيفرتك الحالات الثلاث بقاعدة واحدة: إذا كان رقم أول حدث تتلقاه لا يزيد على Last-Event-ID، فالإجابة تبدأ
من جديد؛ امسح ما عرضته وابنِه مرة أخرى. وإلا فألحق ما يصل.
import { randomUUID } from "node:crypto";
async function answerWithResume(messages, collection, show) {
const requestId = randomUUID();
let text = "";
let lastId = 0;
for (let attempt = 1; ; attempt++) {
let first = true;
try {
return await streamAnswer({
messages, collection, requestId, lastEventId: lastId,
onText(piece, id) {
if (first && lastId && id <= lastId) text = ""; // the answer starts over
first = false;
text += piece;
lastId = id;
show(text);
},
});
} catch (error) {
const retry = error.event ? error.event.retry === true : true; // a network error, or a server restart
if (!retry || attempt === 5) throw error;
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
}قد تعمل واجهة Sursa API على عدة خوادم. ولا يستمر التدفق من Last-Event-ID إلا على الخادم الذي يكتب الإجابة؛ فإن وصل
طلبك المكرر إلى خادم آخر، فإنه يتلقى الإجابة المحفوظة بعد اكتمالها، أو تُكتب الإجابة من جديد من البداية. والقاعدة
أعلاه تغطي الحالتين، ولا يُحتسب السؤال مرتين أبدًا.
بلا request_id، يلغي إغلاق الاتصال الإجابة.
الأخطاء في التدفق
الأخطاء قبل بدء التدفق أخطاء HTTP عادية: 401، أو 403، أو 422، أو 429 مع Retry-After حين تستنفد المؤسسة
حد الأسئلة أو يكون لديها عدد كبير جدًا من
الإجابات الجارية في وقت واحد.
وبعد أن يبدأ التدفق، تكون حالة HTTP دائمًا 200، وتصل المشكلة على شكل حدث error:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
- القيمة
retry: trueتعني أن الإجابة توقفت بسبب الخادم، أثناء تحديث مثلًا، ويمكنك إرسال الطلب نفسه مرة أخرى دون تغيير. - الرموز الأخرى، مثل
timeout، مذكورة في مرجع API. تعامل مع الرمز المجهول كأي خطأ آخر في التدفق: أخبر المستخدم، ودعه يسأل مرة أخرى.
تقييم المستخدمين للإجابات
لكل إجابة قيمة question_id. أرسل تقييم المستخدم إلى
POST /v1/questions/{question_id}/feedback: القيمة 1 للإجابة المفيدة،
و-1 لغير المفيدة، مع reason وnote اختياريين. وتساعد التقييمات المؤسسة على معرفة البطاقات التي تحتاج إلى تحسين.
curl -X POST https://api.sursa.com/v1/questions/9812/feedback \
-H "Authorization: Bearer $SURSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"value": -1, "reason": "outdated", "note": "The return window is now 14 days."}'قائمة تحقق لروبوت في بيئة الإنتاج
- مفتاح له
searchوaskفقط، مقصور على المجموعات التي يحق لجمهور الروبوت رؤيتها. - قيمة
request_idلكل سؤال، وقاعدة الاستئناف أعلاه. - مهلة لقراءة التدفق أطول من 15 ثانية، كي تبقيه تعليقات
: pingحيًا. - إجابات تُعرض بصيغة Markdown، وإحالات مربوطة بمصادرها.
- رسالة واضحة حين يُملأ الحقل
empty، وطريقة للوصول إلى شخص. - معالجة الحالة
429بانتظارRetry-After. - تقييمات تُرسَل، كي تتحسن قاعدة المعرفة مع الوقت.