सवाल-जवाब बॉट बनाना
यह गाइड ऐसे बॉट का मुख्य हिस्सा बनाती है, जो आपके नॉलेज बेस से सवालों के जवाब देता है: किसी चैट टूल में, आपके हेल्प सेंटर में, या आपके प्रोडक्ट के अंदर। असिस्टेंट ज़रूरी जानकारी ढूँढता है और जवाब देता है, इस्तेमाल किए गए कार्ड का हवाला देते हुए; आपका कोड सवाल भेजता है और नतीजा दिखाता है।
आपको 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], sources की एंट्री के ref से मेल
खाते हैं; हर हवाले को कार्ड (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"
ज़्यादातर सवालों के लिए ठीक है।
जवाब स्ट्रीम करें
चैट इंटरफ़ेस में जवाब लिखे जाते समय ही दिखाएँ। "stream": true के साथ POST /v1/chat
text/event-stream से जवाब देता है: हर संदेश के लिए एक इवेंट, हर एक में id: लाइन (स्ट्रीम में उसका नंबर, 1 से)
और JSON वाली data: लाइन। JSON का type फ़ील्ड बताता है कि इवेंट क्या है।
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, या Retry-After के साथ 429, जब संगठन
अपनी सवालों की सीमा इस्तेमाल कर चुका हो या उसके एक साथ बहुत सारे
जवाब तैयार हो रहे हों।
स्ट्रीम शुरू होने के बाद उसका 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सेट होने पर साफ़ संदेश, और किसी व्यक्ति तक पहुँचने का तरीक़ा।Retry-Afterतक इंतज़ार करके429को संभालना।- वापस भेजी गई रेटिंग, ताकि नॉलेज बेस समय के साथ बेहतर होता जाए।