Preskoči na sadržaj
Sursa Developers

Izrada bota za pitanja i odgovore

Ovaj vodič gradi jezgru bota koji odgovara na pitanja iz vaše baze znanja: u alatu za chat, u vašem centru za pomoć ili unutar vašeg proizvoda. Asistent pronalazi potrebno znanje i odgovara, navodeći kartice koje je koristio; vaš kod šalje pitanje i prikazuje rezultat.

Potreban vam je ključ s opsezima search i ask. Odgovor koristi samo kartice koje ključ smije čitati, pa uloga, podatkovni profil i zbirke ključa određuju o čemu bot može govoriti. Botu za kupce dodijelite ključ koji vidi samo javne zbirke.

Tri načina za dobivanje odgovora

OperacijaVraćaZa što je koristiti
POST /v1/searchOdlomke koji odgovaraju upitu, poredane od najboljeg. Bez napisanog odgovora.Polje za pretraživanje, „povezani članci”, vlastito rangiranje.
POST /v1/askPotpuni odgovor s njegovim izvorima, u jednom odgovoru.Botove koji objavljuju jednu poruku po odgovoru; pozadinske poslove.
POST /v1/chatOdgovor kao tok događaja, dok se piše.Sučelja za chat, u kojima korisnik treba vidjeti kako se odgovor pojavljuje.
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"}'

Svaki je rezultat odlomak kartice: doc_id i doc_name kartice, heading odjeljka, text odlomka, stranice ili retci tablice u kojima je pronađen te pouzdanost i provjera kartice.

collection odabire zbirku koja se pretražuje; bez njega koristi se zbirka s imenom default. tags i card_ids sužavaju pretraživanje na kartice s tim oznakama ili na određene kartice.

Pitanje i potpuni odgovor

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

Tekst odgovora u answer je Markdown. Njegove reference, kao što su [1] ili [1, 3], odgovaraju polju ref stavki u sources; svaku povežite s karticom (doc_id) kako bi čitatelj mogao provjeriti odgovor.

Broj u tekstu koji se nije mogao potvrditi u dokumentima može se pojaviti kao marker, na primjer ⟦n:a1⟧. verification.items opisuje svaki marker: njegov marker, razlog reason i tekst original. Ako vaš kod ne obrađuje markere, zamijenite svaki njegovim tekstom original.

Kad odgovora nema

Odgovor je uvijek 200. Kad asistent ne može odgovoriti, empty kaže zašto, a answer sadrži kratku poruku koju možete prikazati:

emptyZnačenje
no_hitsNišta u bazi znanja ne odgovara pitanju.
restrictedTema se nalazi u karticama koje ključ ne smije čitati.
guardrailPitanje je izvan onoga na što organizacija dopušta asistentu da odgovara.
no_data_for_periodPitanje se odnosi na razdoblje za koje nema dokumenata.

no_hits je koristan signal: pokazuje što vaši korisnici pitaju, a baza znanja to još ne pokriva.

Brzi i precizni način

Za temeljitiji odgovor na teško pitanje proslijedite "quality": "precise", uz cijenu duljeg čekanja. Zadana vrijednost, "fast", odgovara većini pitanja.

Tok odgovora

U sučelju za chat prikazujte odgovor dok se piše. POST /v1/chat s "stream": true odgovara s text/event-stream: jedan događaj po poruci, svaki s retkom id: (njegov broj u toku, od 1) i retkom data: s JSON-om. Polje type u JSON-u kaže o kakvom je događaju riječ.

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Što učiniti
metaOdgovor je započeo. Sačuvajte answer_id ako bilježite odgovore.
sourcesKartice na kojima se odgovor temelji. Može stići više puta; vrijedi posljednji.
tokenDio teksta odgovora. Dodajte ga onome što prikazujete.
doneOdgovor je potpun. Ima ista polja kao odgovor operacije /v1/ask.
errorOdgovor se zaustavio. Tok završava bez done. V. u nastavku.
followupsNakon done: predložena sljedeća pitanja koja možete ponuditi kao gumbe.
bilo što drugoPreskočite. Nove vrste događaja mogu se pojaviti u bilo kojem trenutku.

Redak koji počinje s : (kao što je : ping, koji se šalje svakih 15 sekundi dok se odgovor priprema) komentar je. Preskočite ga.

messages sadrži dosadašnji razgovor, od najstarijeg, a završava pitanjem korisnika. Pošaljite posljednjih nekoliko replika kako bi se razumjela dodatna pitanja kao što je „a za narudžbe iz inozemstva?”.

// 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 POST /v1/chat vraća potpuni odgovor kao JSON, kao i /v1/ask.

Nastavak prekinutog toka

Veze se prekidaju: mobilna mreža se promijeni, proxyju istekne vrijeme, vaš se poslužitelj ponovno pokrene. Svakom pitanju dodijelite request_id i odgovor će preživjeti prekid:

  • Uz request_id odgovor se priprema neovisno o vezi. Prekid veze ne zaustavlja ga: odgovor se dovršava i sprema.
  • Da biste nastavili, ponovno pošaljite isti zahtjev, s istim request_id, a u zaglavlju Last-Event-ID broj posljednjeg događaja koji ste primili.
  • Koristite jedan request_id (na primjer UUID) po pitanju i isti u svakom ponovnom pokušaju tog pitanja. Novo pitanje dobiva novi request_id.

Što ponovljeni zahtjev dobiva ovisi o tome gdje je odgovor:

SituacijaPonovljeni zahtjev prima
Odgovor se još piše ili je upravo završenDogađaje nakon Last-Event-ID, s istim brojevima: tok se jednostavno nastavlja.
Odgovor je završen prije nekoliko minutaSpremljeni odgovor kao jedan token s cijelim tekstom i done, numerirane od 1. Pitanje se ne postavlja ponovno.
Tok se ne može nastaviti od tog brojaOdgovor od početka, numeriran od 1.

Vaš kod obrađuje sva tri slučaja jednim pravilom: ako prvi događaj koji primite ima broj koji nije veći od Last-Event-ID, odgovor počinje ispočetka; obrišite ono što ste prikazali i izgradite ga ponovno. U suprotnom dodajte novi tekst.

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 može raditi na više poslužitelja. Tok se nastavlja od Last-Event-ID samo na poslužitelju koji piše odgovor; ako vaš ponovljeni zahtjev stigne na drugi poslužitelj, prima spremljeni odgovor nakon što je dovršen ili se odgovor ponovno piše od početka. Gornje pravilo pokriva oba slučaja, a pitanje se nikada ne ubraja dvaput.

Bez request_id zatvaranje veze otkazuje odgovor.

Pogreške tijekom toka

Pogreške prije početka toka obične su HTTP pogreške: 401, 403, 422 ili 429 uz Retry-After kad je organizacija potrošila svoje ograničenje broja pitanja ili ima previše odgovora koji se istodobno pripremaju.

Kad tok započne, njegov HTTP status uvijek je 200, a problem stiže kao događaj error:

{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
  • retry: true znači da se odgovor zaustavio zbog poslužitelja, na primjer tijekom ažuriranja, i da isti zahtjev možete ponovno poslati nepromijenjen.
  • Ostali kodovi, kao što je timeout, navedeni su u referenci API-ja. Nepoznati kod tretirajte kao i svaku drugu pogrešku toka: obavijestite korisnika i omogućite mu da ponovno postavi pitanje.

Ocjenjivanje odgovora

Svaki odgovor ima question_id. Ocjenu korisnika pošaljite na POST /v1/questions/{question_id}/feedback: 1 za koristan, -1 za nekoristan odgovor, uz neobavezne reason i note. Ocjene pomažu organizaciji da vidi koje kartice treba doraditi.

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

Kontrolni popis za bot u produkciji

  • Ključ samo s opsezima search i ask, ograničen na zbirke koje publika bota smije vidjeti.
  • Jedan request_id po pitanju i gornje pravilo nastavka.
  • Vrijeme čekanja pri čitanju toka dulje od 15 sekundi, kako bi ga komentari : ping održavali aktivnim.
  • Odgovori prikazani kao Markdown, s referencama povezanima s izvorima.
  • Jasna poruka kad je postavljen empty i način da se dođe do osobe.
  • 429 obrađen čekanjem koliko navodi Retry-After.
  • Ocjene koje se šalju natrag, kako bi baza znanja s vremenom postajala bolja.