Soru-cevap botu oluşturma
Bu kılavuz, bilgi tabanınızdaki bilgilerle soruları yanıtlayan bir botun temelini oluşturur: bir sohbet aracında, yardım merkezinizde ya da ürününüzün içinde. Asistan ilgili bilgiyi bulur ve kullandığı kartlara atıf yaparak yanıt verir; kodunuz soruyu gönderir ve sonucu gösterir.
search ve ask kapsamlarına sahip bir anahtara ihtiyacınız vardır. Yanıt yalnızca
anahtarın okumasına izin verilen kartları kullanır; bu yüzden botun hangi konularda konuşabileceğini anahtarın rolü,
veri profili ve koleksiyonları belirler. Müşterilere yönelik bir bota yalnızca herkese açık koleksiyonlara erişebilen
bir anahtar verin.
Yanıt almanın üç yolu
| Uç nokta | Döndürdüğü | Kullanım alanı |
|---|---|---|
POST /v1/search | Eşleşen pasajlar, en iyisi önce. Yazılı yanıt yok. | Arama kutusu, “ilgili makaleler”, kendi sıralamanız. |
POST /v1/ask | Kaynaklarıyla birlikte eksiksiz bir yanıt, tek bir HTTP yanıtında. | Her yanıt için tek ileti gönderen botlar; arka uç işleri. |
POST /v1/chat | Yazıldığı sırada bir olay akışı olarak yanıt. | Kullanıcının yanıtın belirişini görmesi gereken sohbet arayüzleri. |
Arama
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"}'Her eşleşme bir kartın pasajıdır: kartın doc_id ve doc_name değerleri, bölümün heading değeri, pasajın text
değeri, pasajın bulunduğu sayfalar ya da tablo satırları ve kartın güven ve doğrulama bilgileri.
collection aranacak koleksiyonu seçer; bu parametre olmadan adı default olan koleksiyon kullanılır. tags ve
card_ids aramayı bu etiketlere sahip kartlarla ya da belirli kartlarla daraltır.
Soru sorma ve eksiksiz yanıt alma
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 alanındaki yanıt metni Markdown biçimindedir. [1] ya da [1, 3] gibi atıfları, sources içindeki
girdilerin ref değerleriyle eşleşir; okurun yanıtı kontrol edebilmesi için her birini karta (doc_id) bağlayın.
Metinde belgelerle teyit edilemeyen bir sayı, ⟦n:a1⟧ gibi bir işaret olarak görünebilir. verification.items her
işareti açıklar: marker, reason ve original metni. Kodunuz işaretleri işlemiyorsa her birini original metniyle
değiştirin.
Yanıt olmadığında
Yanıt her zaman 200 olur. Asistan yanıt veremediğinde empty nedenini belirtir, answer ise gösterebileceğiniz
kısa bir ileti içerir:
empty | Anlamı |
|---|---|
no_hits | Bilgi tabanında soruyla eşleşen hiçbir şey yok. |
restricted | Konu, anahtarın okumasına izin verilmeyen kartlarda. |
guardrail | Soru, kuruluşun asistana yanıtlama izni verdiği konuların dışında. |
no_data_for_period | Soru, hiçbir belgenin bulunmadığı bir dönemle ilgili. |
no_hits yararlı bir sinyaldir: kullanıcılarınızın, bilgi tabanının henüz kapsamadığı hangi soruları sorduğunu
gösterir.
“Fast” ve “Precise”
Zor bir soruya daha kapsamlı bir yanıt için, daha uzun süre pahasına "quality": "precise" (hassas) gönderin.
Varsayılan değer olan "fast" (hızlı) çoğu soruya uygundur.
Yanıtı akış olarak alma
Bir sohbet arayüzünde yanıtı yazılırken gösterin. "stream": true ile gönderilen
POST /v1/chat text/event-stream ile yanıt verir: her ileti için bir olay; her olayda
bir id: satırı (olayın akıştaki numarası, 1’den başlayarak) ve JSON içeren bir data: satırı bulunur. JSON’daki
type alanı olayın ne olduğunu söyler.
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 | Ne yapmalı |
|---|---|
meta | Yanıt başladı. Yanıtları günlüğe kaydediyorsanız answer_id değerini saklayın. |
sources | Yanıtın dayandığı kartlar. Birden fazla kez gelebilir; sonuncusu geçerlidir. |
token | Yanıt metninin bir parçası. Gösterdiğiniz metnin sonuna ekleyin. |
done | Yanıt tamamlandı. /v1/ask yanıtıyla aynı alanlara sahiptir. |
error | Yanıt durdu. Akış done olmadan sona erer. Aşağıya bakın. |
followups | done olayından sonra: düğme olarak sunabileceğiniz önerilen sonraki sorular. |
| başka herhangi bir tür | Atlayın. Yeni olay türleri her an ortaya çıkabilir. |
: ile başlayan bir satır (yanıt hazırlanırken 15 saniyede bir gönderilen : ping gibi) bir yorumdur. Atlayın.
messages, şimdiye kadarki sohbeti en eskisi önce olacak şekilde içerir ve kullanıcının sorusuyla biter. “Peki yurt
dışından gelen siparişler için?” gibi takip sorularının anlaşılması için son birkaç turu gönderin.
// 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 olmadan POST /v1/chat, /v1/ask gibi eksiksiz yanıtı JSON olarak döndürür.
Kopan akışı sürdürme
Bağlantılar kopar: bir mobil ağ değişir, bir proxy zaman aşımına uğrar, sunucunuz yeniden başlar. Her soruya bir
request_id verin; yanıt bağlantının kopmasından etkilenmez:
request_idile yanıt bağlantıdan bağımsız olarak hazırlanır. Bağlantının kopması yanıtı durdurmaz: yanıt tamamlanır ve kaydedilir.- Sürdürmek için aynı isteği aynı
request_idile ve aldığınız son olayın numarasınıLast-Event-IDbaşlığına yazarak yeniden gönderin. - Her soru için bir
request_id(örneğin bir UUID) ve o sorunun her yeniden denemesinde aynısını kullanın. Yeni bir soru yeni birrequest_idalır.
Tekrarlanan isteğin ne alacağı yanıtın hangi aşamada olduğuna bağlıdır:
| Durum | Tekrarlanan istek şunu alır |
|---|---|
| Yanıt hâlâ yazılıyor ya da az önce bitti | Last-Event-ID sonrasındaki olaylar, aynı numaralarla: akış kaldığı yerden devam eder. |
| Yanıt birkaç dakika önce bitti | Kaydedilmiş yanıt: metnin tamamını içeren bir token ve bir done, 1’den başlayarak numaralandırılmış. Soru yeniden sorulmaz. |
| Akış o numaradan devam edemiyor | Baştan itibaren yanıt, 1’den başlayarak numaralandırılmış. |
Kodunuz üç durumu da tek bir kuralla ele alır: aldığınız ilk olayın numarası Last-Event-ID değerinden büyük
değilse yanıt baştan başlıyordur; gösterdiklerinizi temizleyin ve yeniden oluşturun. Aksi hâlde sonuna ekleyin.
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 birden fazla sunucuda çalışabilir. Bir akış Last-Event-ID ile yalnızca yanıtı yazan sunucuda devam eder;
tekrarlanan isteğiniz başka bir sunucuya ulaşırsa yanıt bittiğinde kaydedilmiş yanıtı alır ya da yanıt baştan yeniden
yazılır. Yukarıdaki kural iki durumu da kapsar ve soru hiçbir zaman iki kez sayılmaz.
request_id olmadan bağlantıyı kapatmak yanıtı iptal eder.
Akıştaki hatalar
Akış başlamadan önceki hatalar sıradan HTTP hatalarıdır: 401, 403, 422 ya da kuruluş
soru sınırını doldurduğunda veya aynı anda çok fazla
hazırlanmakta olan yanıtı olduğunda Retry-After ile 429.
Akış başladıktan sonra HTTP durumu her zaman 200 olur ve bir sorun error olayı olarak gelir:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
retry: true, yanıtın sunucu kaynaklı bir nedenle, örneğin bir güncelleme sırasında durduğu ve aynı isteği değiştirmeden yeniden gönderebileceğiniz anlamına gelir.timeoutgibi diğer kodlar API başvurusunda listelenir. Bilinmeyen bir kodu diğer akış hataları gibi ele alın: kullanıcıya bildirin ve soruyu yeniden sormasına izin verin.
Kullanıcıların yanıtları puanlamasını sağlama
Her yanıtın bir question_id değeri vardır. Kullanıcının puanını
POST /v1/questions/{question_id}/feedback uç noktasına gönderin: yararlı
için 1, yararlı değil için -1; isteğe bağlı olarak bir reason ve note ile. Puanlar, kuruluşun hangi kartların
üzerinde çalışılması gerektiğini görmesine yardımcı olur.
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."}'Canlı ortamdaki bir bot için kontrol listesi
- Yalnızca
searchveaskkapsamlarına sahip, botun kitlesinin görebileceği koleksiyonlarla sınırlı bir anahtar. - Her soru için bir
request_idve yukarıdaki sürdürme kuralı. : pingyorumlarının akışı canlı tutabilmesi için 15 saniyeden uzun bir akış okuma zaman aşımı.- Markdown olarak görüntülenen ve atıfları kaynaklarına bağlanmış yanıtlar.
emptydoldurulduğunda açık bir ileti ve bir kişiye ulaşma yolu.Retry-Aftersüresi kadar beklenerek ele alınan429yanıtları.- Bilgi tabanının zamanla iyileşmesi için geri gönderilen puanlar.