Kérdés-válasz bot készítése
Ez az útmutató egy olyan bot magját építi fel, amely a tudásbázisod alapján válaszol a kérdésekre: egy csevegőeszközben, a súgóközpontodban vagy a termékeden belül. Az asszisztens megtalálja a szükséges tudást, és válaszol, megjelölve a felhasznált kártyákat; a kódod elküldi a kérdést, és megjeleníti az eredményt.
Olyan kulcsra van szükséged, amely search és ask hatókörrel rendelkezik. A válasz csak
azokat a kártyákat használja, amelyeket a kulcs olvashat, így a kulcs szerepköre, adatprofilja és gyűjteményei
döntik el, miről beszélhet a bot. Az ügyfeleknek szóló bot olyan kulcsot kapjon, amely csak a nyilvános
gyűjteményeket látja.
A válasz megszerzésének három módja
| Végpont | Mit ad vissza | Mire használd |
|---|---|---|
POST /v1/search | Az illeszkedő szövegrészleteket, a legjobbal kezdve. Megfogalmazott válasz nélkül. | Keresőmező, „kapcsolódó cikkek”, saját rangsorolás. |
POST /v1/ask | Teljes válasz a forrásaival, egyetlen válaszban. | Válaszonként egy üzenetet küldő botok; háttérfeladatok. |
POST /v1/chat | A válasz eseményfolyamként, írás közben. | Csevegőfelületek, ahol a felhasználónak látnia kell, ahogy a válasz megjelenik. |
Keresés
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"}'Minden találat egy kártya szövegrészlete: a kártya doc_id és doc_name értéke, a szakasz heading címe, a
szövegrészlet text szövege, azok az oldalak vagy táblázatsorok, ahol megtalálható, valamint a kártya
megbízhatósága és ellenőrzése.
A collection kiválasztja, melyik gyűjteményben keress; nélküle a default nevű gyűjtemény a keresés tárgya. A
tags és a card_ids az adott címkéjű kártyákra vagy bizonyos kártyákra szűkíti a keresést.
Kérdés és teljes válasz
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"}'Az answer mezőben lévő válaszszöveg Markdown. A hivatkozásai, például [1] vagy [1, 3], a sources elemeinek
ref értékével egyeznek; mindegyiket kapcsold össze a kártyával (doc_id), hogy az olvasó ellenőrizhesse a választ.
A szövegben az a szám, amelyet nem sikerült a dokumentumok alapján megerősíteni, jelölőként jelenhet meg, például
⟦n:a1⟧ alakban. A verification.items minden jelölőt leír: a marker értékét, a reason okot és az original
szöveget. Ha a kódod nem kezeli a jelölőket, mindegyiket cseréld le az original szövegére.
Ha nincs válasz
A válasz mindig 200. Ha az asszisztens nem tud válaszolni, az empty megmondja, miért, az answer pedig egy rövid,
megjeleníthető üzenetet tartalmaz:
empty | Jelentése |
|---|---|
no_hits | A tudásbázisban semmi nem illeszkedik a kérdéshez. |
restricted | A téma olyan kártyákban van, amelyeket a kulcs nem olvashat. |
guardrail | A kérdés kívül esik azon, amire a szervezet megengedi az asszisztensnek a választ. |
no_data_for_period | A kérdés olyan időszakra vonatkozik, amelyről nincsenek dokumentumok. |
A no_hits hasznos jelzés: megmutatja, mit kérdeznek a felhasználóid, amit a tudásbázis még nem fed le.
A „Fast” és a „Precise” mód
Nehéz kérdésre alaposabb válaszért add meg a "quality": "precise" értéket („Precise”, pontos mód), ami több időbe
kerül. Az alapértelmezett "fast" („Fast”, gyors mód) a legtöbb kérdéshez megfelel.
A válasz továbbítása adatfolyamként
Csevegőfelületen írás közben jelenítsd meg a választ. A POST /v1/chat a
"stream": true értékkel text/event-stream választ ad: üzenetenként egy eseményt, mindegyikben egy id: sorral (az
esemény sorszáma az adatfolyamban, 1-től) és egy JSON-t tartalmazó data: sorral. A JSON type mezője mondja meg, mi
az esemény.
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 | Mit tegyél |
|---|---|
meta | A válasz elkezdődött. Ha naplózod a válaszokat, őrizd meg az answer_id értékét. |
sources | A kártyák, amelyekre a válasz épül. Többször is érkezhet; az utolsó érvényes. |
token | A válasz szövegének egy darabja. Fűzd hozzá a megjelenített szöveghez. |
done | A válasz elkészült. Ugyanazok a mezői, mint a /v1/ask válaszának. |
error | A válasz megszakadt. Az adatfolyam done nélkül ér véget. Lásd lent. |
followups | A done után: javasolt további kérdések, amelyeket gombokként kínálhatsz fel. |
| bármi más | Hagyd ki. Új eseménytípusok bármikor megjelenhetnek. |
A : karakterrel kezdődő sor (például a : ping, amely a válasz előkészítése közben 15 másodpercenként érkezik)
megjegyzés. Hagyd ki.
A messages az eddigi beszélgetést tartalmazza, a legrégebbitől kezdve, a felhasználó kérdésével a végén. Küldd el
az utolsó néhány kört, hogy a további kérdéseket, például „és a külföldi rendeléseknél?”, is értse.
// 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 nélkül a POST /v1/chat JSON-ként adja vissza a teljes választ, mint a /v1/ask.
Megszakadt adatfolyam folytatása
A kapcsolatok megszakadnak: vált a mobilhálózat, egy proxy időtúllépésbe fut, újraindul a szervered. Adj minden
kérdésnek request_id azonosítót, és a válasz túléli a megszakadást:
request_idazonosítóval a válasz a kapcsolattól függetlenül készül el. A kapcsolat bontása nem állítja le: a válasz elkészül, és a Sursa elmenti.- A folytatáshoz küldd el újra ugyanazt a kérést, ugyanazzal a
request_idazonosítóval, és aLast-Event-IDfejlécben az utolsó kapott esemény sorszámával. - Kérdésenként egy
request_idazonosítót használj (például egy UUID-t), és ugyanazt a kérdés minden újrapróbálásakor. Az új kérdés újrequest_idazonosítót kap.
Hogy mit kap a megismételt kérés, az attól függ, hol tart a válasz:
| Helyzet | A megismételt kérés ezt kapja |
|---|---|
| A válasz még íródik, vagy épp most készült el | A Last-Event-ID utáni eseményeket, ugyanazokkal a sorszámokkal: az adatfolyam egyszerűen folytatódik. |
| A válasz néhány perce elkészült | A mentett választ egy, a teljes szöveget tartalmazó token eseményként és egy done eseményként, 1-től számozva. A kérdés nem fut le újra. |
| Az adatfolyam nem folytatható attól a sorszámtól | A választ az elejétől, 1-től számozva. |
A kódod mindhárom esetet egyetlen szabállyal kezeli: ha az első kapott esemény sorszáma nem nagyobb, mint a
Last-Event-ID, a válasz elölről kezdődik; töröld, amit megjelenítettél, és építsd fel újra. Egyébként fűzd hozzá.
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));
}
}
}A Sursa API több szerveren is futhat. Az adatfolyam csak azon a szerveren folytatódik a Last-Event-ID alapján,
amelyik éppen a választ írja; ha a megismételt kérésed egy másikhoz jut el, az elkészülte után a mentett választ
kapja meg, vagy a válasz az elejétől újra megíródik. A fenti szabály mindkét esetet lefedi, és a kérdés soha nem
számít kétszer.
request_id nélkül a kapcsolat bezárása megszakítja a választ.
Hibák az adatfolyamban
Az adatfolyam kezdete előtti hibák szokásos HTTP-hibák: 401, 403, 422, vagy 429 a Retry-After fejléccel,
ha a szervezet elhasználta a kérdéskorlátját, vagy egyszerre túl sok
folyamatban lévő válasza van.
Miután az adatfolyam elindult, a HTTP-állapotkódja mindig 200, és a probléma error eseményként érkezik:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
- A
retry: trueazt jelenti, hogy a válasz a szerver miatt szakadt meg, például frissítés közben, és ugyanazt a kérést változatlanul újra elküldheted. - A többi kódot, például a
timeoutkódot, az API-referencia sorolja fel. Az ismeretlen kódot kezeld úgy, mint bármely más adatfolyam-hibát: tájékoztasd a felhasználót, és hagyd, hogy újra kérdezzen.
A válaszok értékelése
Minden válasznak van question_id azonosítója. A felhasználó értékelését küldd el a
POST /v1/questions/{question_id}/feedback végpontra: 1 a hasznos, -1
a nem hasznos válaszhoz, opcionális reason és note mezővel. Az értékelések segítenek a szervezetnek látni, mely
kártyákon kell még dolgozni.
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."}'Ellenőrzőlista éles bothoz
- Csak
searchésaskhatókörű kulcs, a bot közönsége által látható gyűjteményekre korlátozva. - Kérdésenként egy
request_id, és a fenti folytatási szabály. - Az adatfolyam olvasásának időkorlátja legyen hosszabb 15 másodpercnél, hogy a
: pingmegjegyzések életben tartsák. - Markdownként megjelenített válaszok, a forrásaikhoz kapcsolt hivatkozásokkal.
- Egyértelmű üzenet, ha az
emptybe van állítva, és lehetőség arra, hogy a felhasználó élő munkatárshoz forduljon. - A
429kezelése aRetry-Afterkivárásával. - Visszaküldött értékelések, hogy a tudásbázis idővel jobb legyen.