Přejít na obsah
Sursa Developers

Vytvořte bota pro otázky a odpovědi

Tento návod sestaví jádro bota, který odpovídá na otázky z vaší znalostní báze: v chatovacím nástroji, ve vašem centru nápovědy nebo přímo ve vašem produktu. Asistent najde potřebné znalosti a odpoví s odkazy na karty, které použil; váš kód odešle otázku a zobrazí výsledek.

Potřebujete klíč s rozsahy search a ask. Odpověď používá jen karty, které klíč smí číst, takže o tom, o čem bot může mluvit, rozhoduje role, datový profil a kolekce klíče. Botovi pro zákazníky dejte klíč, který vidí jen veřejné kolekce.

Tři způsoby, jak získat odpověď

OperaceVracíPoužijte pro
POST /v1/searchOdpovídající pasáže, nejlepší první. Bez napsané odpovědi.Vyhledávací pole, „související články“, vlastní pořadí výsledků.
POST /v1/askÚplnou odpověď se zdroji v jedné odpovědi HTTP.Boty, kteří na každou odpověď pošlou jednu zprávu; úlohy na straně serveru.
POST /v1/chatOdpověď jako stream událostí, zatímco vzniká.Chatovací rozhraní, kde má uživatel vidět, jak se odpověď objevuje.
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"}'

Každý výsledek je pasáž karty: doc_id a doc_name karty, heading oddílu, text pasáže, stránky nebo řádky tabulky, kde byla nalezena, a důvěra a ověření karty.

collection určuje kolekci, ve které se vyhledává; bez ní se použije kolekce s názvem default. tags a card_ids zúží vyhledávání na karty s danými štítky nebo na konkrétní karty.

Otázka s úplnou odpovědí

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"}'

Text odpovědi v answer je ve formátu Markdown. Jeho odkazy, například [1] nebo [1, 3], odpovídají ref položek v sources; každý propojte s kartou (doc_id), aby si čtenář mohl odpověď ověřit.

Číslo v textu, které se nepodařilo potvrdit podle dokumentů, se může objevit jako značka, například ⟦n:a1⟧. verification.items popisuje každou značku: její marker, důvod reason a text original. Pokud váš kód značky nezpracovává, nahraďte každou z nich jejím textem original.

Když odpověď chybí

Odpověď je vždy 200. Když asistent odpovědět nedokáže, empty uvádí proč a answer obsahuje krátkou zprávu, kterou můžete zobrazit:

emptyVýznam
no_hitsOtázce nic ve znalostní bázi neodpovídá.
restrictedTéma je v kartách, které klíč nesmí číst.
guardrailOtázka je mimo to, na co organizace asistentovi dovoluje odpovídat.
no_data_for_periodOtázka se týká období, pro které nejsou k dispozici žádné dokumenty.

no_hits je užitečný signál: ukazuje, na co se vaši uživatelé ptají a co znalostní báze zatím nepokrývá.

Režimy Fast a Precise

Pro důkladnější odpověď na obtížnou otázku předejte "quality": "precise", za cenu delšího čekání. Výchozí "fast" se hodí pro většinu otázek.

Streamování odpovědi

V chatovacím rozhraní zobrazujte odpověď průběžně, jak vzniká. POST /v1/chat s "stream": true odpovídá ve formátu text/event-stream: jedna událost na zprávu, každá s řádkem id: (její číslo ve streamu, od 1) a řádkem data: s JSON. Pole type v JSON říká, o jakou událost jde.

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":[…]}
typeCo dělat
metaOdpověď začala. Pokud odpovědi zaznamenáváte, uschovejte si answer_id.
sourcesKarty, ze kterých odpověď vychází. Může přijít víckrát; platí ta poslední.
tokenČást textu odpovědi. Připojte ji k tomu, co zobrazujete.
doneOdpověď je hotová. Má stejná pole jako odpověď /v1/ask.
errorOdpověď se zastavila. Stream skončí bez done. Viz níže.
followupsPo done: navrhované další otázky, které můžete nabídnout jako tlačítka.
cokoli jinéhoPřeskočte. Nové typy událostí se mohou objevit kdykoli.

Řádek, který začíná na : (například : ping, posílaný každých 15 sekund, zatímco se odpověď připravuje), je komentář. Přeskočte ho.

messages obsahuje dosavadní konverzaci od nejstaršího kola a končí otázkou uživatele. Posílejte posledních několik kol, aby asistent porozuměl navazujícím otázkám typu „a u objednávek ze zahraničí?“.

// 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;
}

Bez "stream": true vrací POST /v1/chat úplnou odpověď jako JSON, stejně jako /v1/ask.

Obnovte přerušený stream

Spojení se přerušují: změní se mobilní síť, proxy vyprší časový limit, váš server se restartuje. Dejte každé otázce request_id a odpověď přerušení přežije:

  • S request_id se odpověď připravuje nezávisle na spojení. Odpojení ji nezastaví: dokončí se a uloží.
  • Chcete-li stream obnovit, odešlete znovu stejný požadavek se stejným request_id a s číslem poslední přijaté události v hlavičce Last-Event-ID.
  • Pro každou otázku použijte jedno request_id (například UUID) a stejné ve všech opakováních této otázky. Nová otázka dostane nové request_id.

Co opakovaný požadavek dostane, závisí na tom, v jakém stavu odpověď je:

SituaceOpakovaný požadavek dostane
Odpověď se stále píše, nebo právě skončilaUdálosti po Last-Event-ID se stejnými čísly: stream prostě pokračuje.
Odpověď skončila před několika minutamiUloženou odpověď jako token s celým textem a done, číslované od 1. Otázka se znovu neklade.
Stream nemůže od tohoto čísla pokračovatOdpověď od začátku, číslovanou od 1.

Váš kód zvládne všechny tři případy jedním pravidlem: pokud má první přijatá událost číslo, které není větší než Last-Event-ID, odpověď začíná znovu; vymažte zobrazený text a sestavte ji znovu. Jinak text připojujte.

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 může běžet na několika serverech. Stream pokračuje od Last-Event-ID jen na serveru, který odpověď píše; pokud váš opakovaný požadavek dorazí na jiný server, dostane uloženou odpověď, jakmile je hotová, nebo se odpověď napíše znovu od začátku. Pravidlo výše pokrývá oba případy a otázka se nikdy nezapočítá dvakrát.

Bez request_id uzavření spojení odpověď zruší.

Chyby ve streamu

Chyby před začátkem streamu jsou běžné chyby HTTP: 401, 403, 422 nebo 429 s Retry-After, když organizace vyčerpala svůj limit otázek nebo má najednou příliš mnoho rozpracovaných odpovědí.

Jakmile stream začal, jeho stavový kód HTTP je vždy 200 a problém přichází jako událost error:

{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
  • retry: true znamená, že se odpověď zastavila kvůli serveru, například během aktualizace, a stejný požadavek můžete odeslat znovu beze změny.
  • Další kódy, například timeout, uvádí reference API. S neznámým kódem zacházejte jako s jakoukoli jinou chybou streamu: informujte uživatele a nechte ho zeptat se znovu.

Hodnocení odpovědí uživateli

Každá odpověď má question_id. Hodnocení uživatele odešlete na POST /v1/questions/{question_id}/feedback: 1 pro užitečnou, -1 pro neužitečnou, s volitelným reason a note. Hodnocení pomáhají organizaci zjistit, které karty je potřeba vylepšit.

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."}'

Kontrolní seznam pro produkčního bota

  • Klíč jen se search a ask, omezený na kolekce, které smí vidět publikum bota.
  • request_id pro každou otázku a pravidlo obnovení popsané výše.
  • Časový limit pro čtení streamu delší než 15 sekund, aby ho komentáře : ping udržely naživu.
  • Odpovědi vykreslené jako Markdown s odkazy propojenými se zdroji.
  • Srozumitelná zpráva, když je nastaveno empty, a možnost obrátit se na člověka.
  • 429 zpracované čekáním podle Retry-After.
  • Odesílaná hodnocení, aby se znalostní báze časem zlepšovala.