Hopp til innholdet
Sursa Developers

Bygg en bot for spørsmål og svar

Denne veiledningen bygger de sentrale delene av en bot som svarer på spørsmål ut fra kunnskapsbasen din: i et chatteverktøy, i hjelpesenteret ditt eller inne i produktet ditt. Assistenten finner den relevante kunnskapen og svarer, med henvisninger til kortene den brukte; koden din sender spørsmålet og viser resultatet.

Du trenger en nøkkel med scopene search og ask. Svaret bruker bare kortene som nøkkelen har lov til å lese, så nøkkelens rolle, dataprofil og samlinger avgjør hva boten kan snakke om. Gi en bot for kunder en nøkkel som bare ser offentlige samlinger.

Tre måter å få et svar på

OperasjonReturnererBruk den til
POST /v1/searchUtdragene som samsvarer, de beste først. Ikke noe skrevet svar.Et søkefelt, «relaterte artikler», din egen rangering.
POST /v1/askEt fullstendig svar med kildene, i ett HTTP-svar.Boter som legger ut én melding per svar; jobber på serversiden.
POST /v1/chatSvaret som en strøm av hendelser mens det skrives.Chattegrensesnitt der brukeren skal se svaret dukke opp.
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"}'

Hvert treff er et utdrag fra et kort: doc_id og doc_name for kortet, heading for seksjonen, text for utdraget, sidene eller tabellradene der det ble funnet, og kortets tillitsscore og verifisering.

collection velger samlingen det skal søkes i; uten den brukes samlingen som heter default. tags og card_ids begrenser søket til kort med disse taggene eller til bestemte kort.

Spør og få et fullstendig svar

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

Teksten i svaret i answer er Markdown. Henvisningene i den, for eksempel [1] eller [1, 3], svarer til ref i oppføringene i sources; lenk hver av dem til kortet (doc_id), slik at leseren kan kontrollere svaret.

Et tall i teksten som ikke kunne bekreftes mot dokumentene, kan vises som en markør, for eksempel ⟦n:a1⟧. verification.items beskriver hver markør: marker, reason og teksten i original. Hvis koden din ikke håndterer markører, erstatter du hver av dem med teksten i original.

Når det ikke finnes noe svar

HTTP-svaret er alltid 200. Når assistenten ikke kan svare, sier empty hvorfor, og answer inneholder en kort melding du kan vise:

emptyBetydning
no_hitsIngenting i kunnskapsbasen samsvarer med spørsmålet.
restrictedTemaet finnes i kort som nøkkelen ikke har lov til å lese.
guardrailSpørsmålet ligger utenfor det organisasjonen tillater assistenten å svare på.
no_data_for_periodSpørsmålet gjelder en periode det ikke finnes dokumenter for.

no_hits er et nyttig signal: Det viser hva brukerne dine spør om som kunnskapsbasen ennå ikke dekker.

«Fast» og «Precise»

Send "quality": "precise" for et grundigere svar på et vanskelig spørsmål, på bekostning av mer tid. Standardverdien, "fast", passer for de fleste spørsmål.

Strøm svaret

I et chattegrensesnitt viser du svaret mens det skrives. POST /v1/chat med "stream": true svarer med text/event-stream: én hendelse per melding, hver med en id:-linje (nummeret i strømmen, fra 1) og en data:-linje med JSON. Feltet type i JSON-en sier hva hendelsen er.

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":[…]}
typeHva du gjør
metaSvaret har begynt. Ta vare på answer_id hvis du logger svar.
sourcesKortene svaret bygger på. Kan komme mer enn én gang; den siste gjelder.
tokenEn bit av teksten i svaret. Legg den til det du viser.
doneSvaret er ferdig. Hendelsen har de samme feltene som svaret fra /v1/ask.
errorSvaret stoppet. Strømmen slutter uten done. Se nedenfor.
followupsEtter done: foreslåtte neste spørsmål som du kan tilby som knapper.
alt annetHopp over det. Nye hendelsestyper kan dukke opp når som helst.

En linje som begynner med : (for eksempel : ping, som sendes hvert 15. sekund mens svaret forberedes), er en kommentar. Hopp over den.

messages inneholder samtalen så langt, eldste først, og slutter med brukerens spørsmål. Send de siste rundene, slik at oppfølgingsspørsmål som «og for bestillinger fra utlandet?» blir forstått.

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

Uten "stream": true returnerer POST /v1/chat hele svaret som JSON, som /v1/ask.

Gjenoppta en avbrutt strøm

Tilkoblinger brytes: Et mobilnett skifter, en proxy får tidsavbrudd, serveren din starter på nytt. Gi hvert spørsmål en request_id, så overlever svaret bruddet:

  • Med request_id forberedes svaret uavhengig av tilkoblingen. En frakobling stopper det ikke: Det fullføres og lagres.
  • For å gjenoppta sender du den samme forespørselen på nytt, med den samme request_id og nummeret på den siste hendelsen du mottok, i headeren Last-Event-ID.
  • Bruk én request_id (for eksempel en UUID) per spørsmål, og den samme i hvert nytt forsøk på det spørsmålet. Et nytt spørsmål får en ny request_id.

Hva den gjentatte forespørselen får, avhenger av hvor langt svaret har kommet:

SituasjonDen gjentatte forespørselen mottar
Svaret skrives fortsatt eller er nettopp ferdigHendelsene etter Last-Event-ID, med de samme numrene: Strømmen fortsetter ganske enkelt.
Svaret ble ferdig for noen minutter sidenDet lagrede svaret som en token med hele teksten og en done, nummerert fra 1. Spørsmålet stilles ikke på nytt.
Strømmen kan ikke fortsette fra det nummeretSvaret fra begynnelsen, nummerert fra 1.

Koden din håndterer alle tre tilfellene med én regel: Hvis den første hendelsen du mottar, har et nummer som ikke er større enn Last-Event-ID, begynner svaret på nytt; tøm det du har vist, og bygg det opp igjen. Ellers legger du til.

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

Sursas API kan kjøre på flere servere. En strøm fortsetter fra Last-Event-ID bare på serveren som skriver svaret; hvis den gjentatte forespørselen din når en annen server, mottar den det lagrede svaret når det er ferdig, eller svaret skrives på nytt fra begynnelsen. Regelen ovenfor dekker begge tilfellene, og spørsmålet telles aldri to ganger.

Uten request_id avbrytes svaret når tilkoblingen lukkes.

Feil i en strøm

Feil før strømmen starter, er vanlige HTTP-feil: 401, 403, 422 eller 429 med Retry-After når organisasjonen har brukt opp grensen for spørsmål eller har for mange svar under arbeid samtidig.

Når strømmen har startet, er HTTP-statusen alltid 200, og et problem kommer som en error-hendelse:

{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
  • retry: true betyr at svaret stoppet på grunn av serveren, for eksempel under en oppdatering, og at du kan sende den samme forespørselen på nytt uendret.
  • Andre koder, for eksempel timeout, er oppført i API-referansen. Behandle en ukjent kode som enhver annen feil i strømmen: Si fra til brukeren, og la vedkommende spørre på nytt.

La brukerne vurdere svarene

Hvert svar har en question_id. Send brukerens vurdering til POST /v1/questions/{question_id}/feedback: 1 for nyttig, -1 for ikke nyttig, med valgfri reason og note. Vurderingene hjelper organisasjonen med å se hvilke kort som trenger arbeid.

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

Sjekkliste for en bot i produksjon

  • En nøkkel med bare search og ask, begrenset til samlingene som botens målgruppe har lov til å se.
  • En request_id per spørsmål og regelen for gjenopptakelse ovenfor.
  • Et tidsavbrudd for lesing av strømmen som er lengre enn 15 sekunder, slik at : ping-kommentarene holder den i live.
  • Svar som vises som Markdown, med henvisningene lenket til kildene.
  • En tydelig melding når empty er satt, og en måte å komme i kontakt med et menneske på.
  • 429 håndtert ved å vente i Retry-After sekunder.
  • Vurderinger som sendes tilbake, slik at kunnskapsbasen blir bedre over tid.