Израда бота за питања и одговоре
Овај водич гради основу бота који одговара на питања из ваше базе знања: у апликацији за ћаскање, у вашем центру за помоћ или унутар вашег производа. Асистент проналази потребно знање и одговара, наводећи картице које је користио; ваш код шаље питање и приказује резултат.
Потребан вам је кључ са опсезима search и ask. Одговор користи само картице које
кључ сме да чита, па улога кључа, профил приступа и колекције одређују о чему бот може да говори. Боту за клијенте
дајте кључ који види само јавне колекције.
Три начина да добијете одговор
| Операција | Враћа | Користите је за |
|---|---|---|
POST /v1/search | Одломке који се поклапају, најбоље прво. Без написаног одговора. | Поље за претрагу, „сродне чланке“, сопствено рангирање. |
POST /v1/ask | Комплетан одговор са изворима, у једном HTTP одговору. | Ботове који објављују једну поруку по одговору; позадинске послове. |
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.- Оцене које се шаљу назад, како би база знања временом постајала боља.