Preskočiť na obsah
Sursa Developers

Vytvorte bota na otázky a odpovede

Tento návod vytvorí základ bota, ktorý odpovedá na otázky z vašej bázy znalostí: v chatovacom nástroji, vo vašom centre pomoci alebo priamo vo vašom produkte. Asistent nájde potrebné znalosti a odpovie, pričom cituje karty, ktoré použil; váš kód odošle otázku a zobrazí výsledok.

Potrebujete kľúč s rozsahmi search a ask. Odpoveď používa len karty, ktoré kľúč smie čítať, takže o tom, o čom bot môže hovoriť, rozhodujú rola kľúča, dátový profil a kolekcie. Botovi pre zákazníkov dajte kľúč, ktorý vidí len verejné kolekcie.

Tri spôsoby, ako získať odpoveď

OperáciaVraciaNa čo ju použiť
POST /v1/searchPasáže, ktoré zodpovedajú dopytu, najlepšie prvé. Bez napísanej odpovede.Vyhľadávacie pole, „súvisiace články“, vlastné poradie výsledkov.
POST /v1/askÚplnú odpoveď s jej zdrojmi v jednej odpovedi HTTP.Boty, ktoré posielajú jednu správu na odpoveď; úlohy na serveri.
POST /v1/chatOdpoveď ako stream udalostí, priebežne, ako vzniká.Chatovacie rozhrania, kde má používateľ vidieť, ako sa odpoveď objavuje.
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ýsledok je pasáž karty: doc_id a doc_name karty, heading sekcie, text pasáže, strany alebo riadky tabuľky, kde sa našla, a dôvera a overenie karty.

collection vyberá kolekciu, v ktorej sa vyhľadáva; bez neho sa použije kolekcia s názvom default. tags a card_ids zúžia vyhľadávanie na karty s týmito štítkami alebo na konkrétne karty.

Otázka s úplnou odpoveďou

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 odpovede v answer je v Markdowne. Jeho odkazy, napríklad [1] alebo [1, 3], zodpovedajú ref položiek v sources; každý z nich prepojte s kartou (doc_id), aby si čitateľ mohol odpoveď overiť.

Číslo v texte, ktoré sa nepodarilo potvrdiť podľa dokumentov, sa môže objaviť ako značka, napríklad ⟦n:a1⟧. verification.items opisuje každú značku: jej marker, dôvod reason a text original. Ak váš kód značky nespracúva, nahraďte každú z nich jej textom original.

Keď odpoveď chýba

Odpoveď HTTP je vždy 200. Keď asistent nevie odpovedať, empty uvádza prečo a answer obsahuje krátku správu, ktorú môžete zobraziť:

emptyVýznam
no_hitsOtázke nezodpovedá nič v báze znalostí.
restrictedTéma je v kartách, ktoré kľúč nesmie čítať.
guardrailOtázka je mimo toho, na čo organizácia asistentovi dovoľuje odpovedať.
no_data_for_periodOtázka sa pýta na obdobie, za ktoré neexistujú žiadne dokumenty.

no_hits je užitočný signál: ukazuje, na čo sa vaši používatelia pýtajú a čo báza znalostí zatiaľ nepokrýva.

„Fast“ a „Precise“

Na dôkladnejšiu odpoveď na ťažkú otázku odošlite "quality": "precise", za cenu dlhšieho času. Predvolený režim "fast" vyhovuje väčšine otázok.

Streamovanie odpovede

V chatovacom rozhraní zobrazujte odpoveď priebežne, ako vzniká. POST /v1/chat s "stream": true odpovedá vo formáte text/event-stream: jedna udalosť na správu, každá s riadkom id: (jej číslo v streame, od 1) a riadkom data: s JSON. Pole type v JSON hovorí, o akú udalosť ide.

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Čo robiť
metaOdpoveď sa začala. Ak odpovede zapisujete do logu, uložte si answer_id.
sourcesKarty, z ktorých odpoveď vychádza. Môže prísť viackrát; platí posledná.
tokenČasť textu odpovede. Pripojte ju k tomu, čo zobrazujete.
doneOdpoveď je hotová. Má rovnaké polia ako odpoveď /v1/ask.
errorOdpoveď sa zastavila. Stream skončí bez done. Pozri nižšie.
followupsPo done: odporúčané ďalšie otázky, ktoré môžete ponúknuť ako tlačidlá.
čokoľvek inéPreskočte. Nové typy udalostí sa môžu objaviť kedykoľvek.

Riadok, ktorý začína na : (napríklad : ping, ktorý sa posiela každých 15 sekúnd, kým sa odpoveď pripravuje), je komentár. Preskočte ho.

messages obsahuje doterajšiu konverzáciu, od najstaršieho kola, a končí otázkou používateľa. Posielajte niekoľko posledných kôl, aby asistent porozumel aj nadväzujúcim otázkam, napríklad „a pri objednávkach zo zahraničia?“.

// 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 vráti POST /v1/chat úplnú odpoveď ako JSON, rovnako ako /v1/ask.

Obnovte prerušený stream

Spojenia sa prerušujú: mobilná sieť sa zmení, na proxy vyprší časový limit, váš server sa reštartuje. Dajte každej otázke request_id a odpoveď prerušenie prežije:

  • S request_id sa odpoveď pripravuje nezávisle od spojenia. Odpojenie ju nezastaví: dokončí sa a uloží.
  • Ak chcete pokračovať, odošlite znova tú istú požiadavku s rovnakým request_id a s číslom poslednej prijatej udalosti v hlavičke Last-Event-ID.
  • Pre každú otázku použite jedno request_id (napríklad UUID) a to isté pri každom opakovaní tejto otázky. Nová otázka dostane nové request_id.

Čo opakovaná požiadavka dostane, závisí od toho, v akom stave je odpoveď:

SituáciaOpakovaná požiadavka dostane
Odpoveď sa ešte píše alebo práve skončilaUdalosti po Last-Event-ID s rovnakými číslami: stream jednoducho pokračuje.
Odpoveď skončila pred niekoľkými minútamiUloženú odpoveď ako token s celým textom a done, číslované od 1. Otázka sa znova nekladie.
Stream nemôže od tohto čísla pokračovaťOdpoveď od začiatku, číslovanú od 1.

Váš kód zvládne všetky tri prípady jedným pravidlom: ak má prvá prijatá udalosť číslo nie väčšie ako Last-Event-ID, odpoveď začína odznova; vymažte, čo ste zobrazili, a zostavte ju znova. Inak pripájajte.

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 bežať na niekoľkých serveroch. Stream pokračuje od Last-Event-ID len na serveri, ktorý odpoveď píše; ak sa vaša opakovaná požiadavka dostane na iný server, dostane uloženú odpoveď, keď je hotová, alebo sa odpoveď napíše znova od začiatku. Pravidlo vyššie pokrýva oba prípady a otázka sa nikdy nezapočíta dvakrát.

Bez request_id zatvorenie spojenia odpoveď zruší.

Chyby v streame

Chyby pred začiatkom streamu sú bežné chyby HTTP: 401, 403, 422 alebo 429 s Retry-After, keď organizácia vyčerpala svoj limit otázok alebo má naraz príliš veľa rozpracovaných odpovedí.

Keď sa stream začne, jeho stavový kód HTTP je vždy 200 a problém príde ako udalosť error:

{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
  • retry: true znamená, že odpoveď sa zastavila pre server, napríklad počas aktualizácie, a tú istú požiadavku môžete odoslať znova bez zmeny.
  • Ďalšie kódy, napríklad timeout, uvádza referencia API. Neznámy kód spracujte ako každú inú chybu streamu: povedzte to používateľovi a nechajte ho opýtať sa znova.

Hodnotenie odpovedí používateľmi

Každá odpoveď má question_id. Hodnotenie používateľa odošlite na POST /v1/questions/{question_id}/feedback: 1 pre užitočnú, -1 pre neužitočnú, s nepovinným reason a note. Hodnotenia pomáhajú organizácii vidieť, ktoré karty treba zlepšiť.

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ý zoznam pre produkčného bota

  • Kľúč len so search a ask, obmedzený na kolekcie, ktoré smú vidieť adresáti bota.
  • request_id pre každú otázku a pravidlo obnovenia uvedené vyššie.
  • Časový limit na čítanie streamu dlhší ako 15 sekúnd, aby ho komentáre : ping udržali pri živote.
  • Odpovede vykreslené ako Markdown s odkazmi prepojenými na ich zdroje.
  • Jasná správa, keď je nastavené empty, a možnosť obrátiť sa na človeka.
  • 429 spracované čakaním podľa Retry-After.
  • Hodnotenia odosielané späť, aby sa báza znalostí časom zlepšovala.