İçeriğe geç
Sursa Developers

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ç noktaDöndürdüğüKullanım alanı
POST /v1/searchEşleşen pasajlar, en iyisi önce. Yazılı yanıt yok.Arama kutusu, “ilgili makaleler”, kendi sıralamanız.
POST /v1/askKaynakları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/chatYazıldığı sırada bir olay akışı olarak yanıt.Kullanıcının yanıtın belirişini görmesi gereken sohbet arayüzleri.
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:

emptyAnlamı
no_hitsBilgi tabanında soruyla eşleşen hiçbir şey yok.
restrictedKonu, anahtarın okumasına izin verilmeyen kartlarda.
guardrailSoru, kuruluşun asistana yanıtlama izni verdiği konuların dışında.
no_data_for_periodSoru, 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":[…]}
typeNe yapmalı
metaYanıt başladı. Yanıtları günlüğe kaydediyorsanız answer_id değerini saklayın.
sourcesYanıtın dayandığı kartlar. Birden fazla kez gelebilir; sonuncusu geçerlidir.
tokenYanıt metninin bir parçası. Gösterdiğiniz metnin sonuna ekleyin.
doneYanıt tamamlandı. /v1/ask yanıtıyla aynı alanlara sahiptir.
errorYanıt durdu. Akış done olmadan sona erer. Aşağıya bakın.
followupsdone olayından sonra: düğme olarak sunabileceğiniz önerilen sonraki sorular.
başka herhangi bir türAtlayı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_id ile 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_id ile ve aldığınız son olayın numarasını Last-Event-ID baş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 bir request_id alır.

Tekrarlanan isteğin ne alacağı yanıtın hangi aşamada olduğuna bağlıdır:

DurumTekrarlanan istek şunu alır
Yanıt hâlâ yazılıyor ya da az önce bittiLast-Event-ID sonrasındaki olaylar, aynı numaralarla: akış kaldığı yerden devam eder.
Yanıt birkaç dakika önce bittiKaydedilmiş 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 edemiyorBaş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.
  • timeout gibi 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 search ve ask kapsamlarına sahip, botun kitlesinin görebileceği koleksiyonlarla sınırlı bir anahtar.
  • Her soru için bir request_id ve yukarıdaki sürdürme kuralı.
  • : ping yorumları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.
  • empty doldurulduğunda açık bir ileti ve bir kişiye ulaşma yolu.
  • Retry-After süresi kadar beklenerek ele alınan 429 yanıtları.
  • Bilgi tabanının zamanla iyileşmesi için geri gönderilen puanlar.