Siirry sisältöön
Sursa Developers

Kysymys–vastaus-botin rakentaminen

Tässä oppaassa rakennetaan sellaisen botin perusosat, joka vastaa kysymyksiin tietopankkisi pohjalta: chat-työkalussa, tukikeskuksessasi tai tuotteesi sisällä. Avustaja löytää tarvittavan tiedon ja vastaa ja viittaa käyttämiinsä kortteihin; koodisi lähettää kysymyksen ja näyttää tuloksen.

Tarvitset avaimen, jolla on käyttöalueet search ja ask. Vastaus käyttää vain kortteja, joita avain saa lukea, joten avaimen rooli, dataprofiili ja kokoelmat ratkaisevat, mistä botti voi puhua. Anna asiakkaille tarkoitetulle botille avain, joka näkee vain julkiset kokoelmat.

Kolme tapaa saada vastaus

PäätepistePalauttaaKäyttökohde
POST /v1/searchVastaavat tekstikatkelmat, paras ensin. Ei kirjoitettua vastausta.Hakukenttä, ”aiheeseen liittyvät artikkelit”, oma osumien järjestys.
POST /v1/askValmiin vastauksen lähteineen yhdessä vastauksessa.Botit, jotka julkaisevat yhden viestin vastausta kohden; taustatyöt.
POST /v1/chatVastauksen tapahtumien virtana sitä mukaa kuin se kirjoitetaan.Chat-käyttöliittymät, joissa käyttäjän pitää nähdä vastauksen ilmestyvän.
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"}'

Jokainen osuma on kortin tekstikatkelma: kortin doc_id ja doc_name, osion heading, katkelman text, sivut tai taulukon rivit, joilta se löytyi, sekä kortin luottamus ja vahvistus.

collection valitsee haettavan kokoelman; ilman sitä käytetään kokoelmaa, jonka nimi on default. tags ja card_ids rajaavat haun kortteihin, joilla on nämä tunnisteet, tai tiettyihin kortteihin.

Kysyminen ja valmis vastaus

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-kentän vastauksen teksti on Markdownia. Sen viittaukset, kuten [1] tai [1, 3], vastaavat sources-kentän kohteiden ref-arvoja; linkitä kukin viittaus korttiin (doc_id), jotta lukija voi tarkistaa vastauksen.

Tekstissä oleva luku, jota ei voitu varmistaa asiakirjoista, voi näkyä merkkinä, kuten ⟦n:a1⟧. verification.items kuvaa jokaisen merkin: sen marker-arvon, syyn (reason) ja original-tekstin. Jos koodisi ei käsittele merkkejä, korvaa kukin niistä sen original-tekstillä.

Kun vastausta ei ole

Vastaus on aina 200. Kun avustaja ei pysty vastaamaan, empty kertoo syyn, ja answer sisältää lyhyen viestin, jonka voit näyttää:

emptyMerkitys
no_hitsMikään tietopankissa ei vastaa kysymystä.
restrictedAihe on korteissa, joita avain ei saa lukea.
guardrailKysymys on sen ulkopuolella, mihin organisaatio sallii avustajan vastata.
no_data_for_periodKysymys koskee ajanjaksoa, jolta ei ole asiakirjoja.

no_hits on hyödyllinen signaali: se näyttää, mitä käyttäjäsi kysyvät mutta mitä tietopankki ei vielä kata.

”Fast” ja ”Precise”

Anna "quality": "precise" (”Precise”, tarkka), kun haluat vaikeaan kysymykseen perusteellisemman vastauksen, vaikka se vie enemmän aikaa. Oletus "fast" (”Fast”, nopea) sopii useimpiin kysymyksiin.

Vastauksen välittäminen virtana

Näytä chat-käyttöliittymässä vastaus sitä mukaa kuin se kirjoitetaan. POST /v1/chat parametrilla "stream": true vastaa text/event-stream-muodossa: yksi tapahtuma viestiä kohden, ja kussakin on id:-rivi (tapahtuman numero virrassa ykkösestä alkaen) ja data:-rivi, jossa on JSON. JSON-objektin type-kenttä kertoo, mistä tapahtumasta on kyse.

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":[…]}
typeMitä tehdä
metaVastaus on alkanut. Säilytä answer_id, jos kirjaat vastauksia lokiin.
sourcesKortit, joihin vastaus perustuu. Voi tulla useammin kuin kerran; viimeinen on voimassa.
tokenOsa vastauksen tekstistä. Lisää se näyttämäsi tekstin perään.
doneVastaus on valmis. Siinä on samat kentät kuin /v1/ask-pyynnön vastauksessa.
errorVastaus keskeytyi. Virta päättyy ilman done-tapahtumaa. Ks. alla.
followupsdone-tapahtuman jälkeen: ehdotetut jatkokysymykset, joita voit tarjota painikkeina.
mikä tahansa muuOhita se. Uusia tapahtumatyyppejä voi ilmestyä milloin tahansa.

Rivi, joka alkaa merkillä : (kuten : ping, joka lähetetään 15 sekunnin välein vastauksen valmistelun aikana), on kommentti. Ohita se.

messages sisältää tähänastisen keskustelun vanhimmasta alkaen, ja viimeisenä on käyttäjän kysymys. Lähetä muutama viimeisin vuoro, jotta jatkokysymykset, kuten ”entä ulkomailta tulevat tilaukset?”, ymmärretään.

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

Ilman "stream": true -parametria POST /v1/chat palauttaa valmiin vastauksen JSON-muodossa, kuten /v1/ask.

Jatka katkennutta virtaa

Yhteydet katkeavat: mobiiliverkko vaihtuu, välityspalvelimen aikakatkaisu laukeaa, palvelimesi käynnistyy uudelleen. Anna jokaiselle kysymykselle request_id, niin vastaus säilyy katkoksen yli:

  • Kun request_id on annettu, vastaus muodostetaan yhteydestä riippumatta. Yhteyden katkeaminen ei pysäytä sitä: vastaus valmistuu ja tallennetaan.
  • Jatka lähettämällä sama pyyntö uudelleen samalla request_id-tunnuksella ja viimeisen vastaanottamasi tapahtuman numerolla Last-Event-ID-otsakkeessa.
  • Käytä yhtä request_id-tunnusta (esimerkiksi UUID) kysymystä kohden ja samaa tunnusta saman kysymyksen jokaisessa uusintayrityksessä. Uusi kysymys saa uuden request_id-tunnuksen.

Se, mitä toistettu pyyntö saa, riippuu vastauksen vaiheesta:

TilanneToistettu pyyntö saa
Vastausta kirjoitetaan vielä, tai se on juuri valmistunutLast-Event-ID-kohdan jälkeiset tapahtumat samoilla numeroilla: virta yksinkertaisesti jatkuu.
Vastaus valmistui joitakin minuutteja sittenTallennetun vastauksen token-tapahtumana, jossa on koko teksti, ja done-tapahtuman, numeroituina ykkösestä alkaen. Kysymystä ei esitetä uudelleen.
Virta ei voi jatkua kyseisestä numerostaVastauksen alusta alkaen, numeroituna ykkösestä.

Koodisi käsittelee kaikki kolme tapausta yhdellä säännöllä: jos ensimmäisen vastaanottamasi tapahtuman numero on enintään Last-Event-ID, vastaus alkaa alusta; tyhjennä näyttämäsi teksti ja rakenna se uudelleen. Muuten lisää uudet tapahtumat perään.

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 voi toimia useana API-instanssina. Virta jatkuu Last-Event-ID-kohdasta vain siinä instanssissa, joka kirjoittaa vastausta; jos toistettu pyyntösi päätyy toiseen instanssiin, se saa tallennetun vastauksen, kun vastaus on valmis, tai vastaus kirjoitetaan uudelleen alusta. Yllä oleva sääntö kattaa molemmat tapaukset, eikä kysymystä koskaan lasketa kahdesti.

Ilman request_id-tunnusta yhteyden sulkeminen peruu vastauksen.

Virheet virrassa

Ennen virran alkamista tulevat virheet ovat tavallisia HTTP-virheitä: 401, 403, 422 tai 429 ja Retry-After, kun organisaatio on käyttänyt kysymysrajansa tai sillä on samaan aikaan liian monta käynnissä olevaa vastausta.

Kun virta on alkanut, sen HTTP-tila on aina 200, ja ongelma saapuu error-tapahtumana:

{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
  • retry: true tarkoittaa, että vastaus keskeytyi Sursan puolella, esimerkiksi päivityksen aikana, ja voit lähettää saman pyynnön uudelleen muuttamattomana.
  • Muut koodit, kuten timeout, luetellaan API-viitteessä. Käsittele tuntematon koodi kuten mikä tahansa muu virran virhe: kerro käyttäjälle ja anna hänen kysyä uudelleen.

Vastausten arviointi

Jokaisella vastauksella on question_id. Lähetä käyttäjän arvio päätepisteeseen POST /v1/questions/{question_id}/feedback: 1 tarkoittaa hyödyllistä, -1 ei hyödyllistä, ja mukaan voi liittää valinnaiset reason ja note. Arviot auttavat organisaatiota näkemään, mitkä kortit vaativat työtä.

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

Tuotantobotin tarkistuslista

  • Avain, jolla on vain search ja ask ja joka on rajattu kokoelmiin, jotka botin yleisö saa nähdä.
  • request_id kysymystä kohden ja yllä kuvattu jatkamissääntö.
  • Virran lukemisen aikakatkaisu, joka on pidempi kuin 15 sekuntia, jotta : ping -kommentit pitävät yhteyden auki.
  • Vastaukset näytetään Markdown-muotoisina, ja viittaukset linkitetään lähteisiinsä.
  • Selkeä viesti, kun empty on asetettu, ja keino tavoittaa ihminen.
  • 429 käsitellään odottamalla Retry-After-ajan verran.
  • Arviot lähetetään takaisin, jotta tietopankki paranee ajan mittaan.