Hoppa till innehållet
Sursa Developers

Bygg en fråga-svar-bot

Den här guiden bygger kärnan i en bot som besvarar frågor utifrån din kunskapsbas: i ett chattverktyg, i ditt hjälpcenter eller i din produkt. Assistenten hittar rätt kunskap och svarar, med hänvisningar till korten som den använde; din kod skickar frågan och visar resultatet.

Du behöver en nyckel med omfången search och ask. Svaret använder bara de kort som nyckeln får läsa, så nyckelns roll, dataprofil och samlingar avgör vad boten kan tala om. Ge en bot för kunder en nyckel som bara ser offentliga samlingar.

Tre sätt att få ett svar

SlutpunktReturnerarAnvänd den för
POST /v1/searchDe utdrag som matchar, bästa först. Inget skrivet svar.En sökruta, ”relaterade artiklar”, din egen rangordning.
POST /v1/askEtt komplett svar med dess källor, i ett enda svar.Botar som publicerar ett meddelande per svar; bakgrundsjobb.
POST /v1/chatSvaret som en ström av händelser, medan det skrivs.Chattgränssnitt, där användaren ska se svaret växa fram.
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"}'

Varje träff är ett utdrag ur ett kort: kortets doc_id och doc_name, avsnittets heading, utdragets text, sidor eller tabellrader där det hittades samt kortets förtroende och verifiering.

collection väljer samlingen som du söker i; utan den används samlingen som heter default. tags och card_ids begränsar sökningen till kort med de taggarna eller till vissa kort.

Fråga och få ett komplett 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"}'

Svarstexten i answer är Markdown. Dess hänvisningar, till exempel [1] eller [1, 3], motsvarar ref i posterna i sources; länka var och en till kortet (doc_id) så att läsaren kan kontrollera svaret.

Ett tal i texten som inte kunde bekräftas mot dokumenten kan visas som en markering, till exempel ⟦n:a1⟧. verification.items beskriver varje markering: dess marker, orsaken reason och texten original. Om din kod inte hanterar markeringar ersätter du var och en med dess original-text.

När det inte finns något svar

Svaret är alltid 200. När assistenten inte kan svara anger empty varför, och answer innehåller ett kort meddelande som du kan visa:

emptyBetydelse
no_hitsInget i kunskapsbasen matchar frågan.
restrictedÄmnet finns i kort som nyckeln inte får läsa.
guardrailFrågan ligger utanför det som organisationen tillåter assistenten att svara på.
no_data_for_periodFrågan gäller en period som det inte finns några dokument för.

no_hits är en användbar signal: den visar vad dina användare frågar om som kunskapsbasen ännu inte täcker.

Lägena Fast och Precise

Skicka "quality": "precise" för ett grundligare svar på en svår fråga, till priset av längre tid. Standardläget, "fast", passar för de flesta frågor.

Strömma svaret

I ett chattgränssnitt visar du svaret medan det skrivs. POST /v1/chat med "stream": true svarar med text/event-stream: en händelse per meddelande, var och en med en rad id: (dess nummer i strömmen, från 1) och en rad data: med JSON. Fältet type i JSON-koden anger vilken typ av händelse det är.

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":[…]}
typeVad du gör
metaSvaret har börjat. Spara answer_id om du loggar svar.
sourcesKorten som svaret bygger på. Kan komma mer än en gång; den sista gäller.
tokenEn bit av svarstexten. Lägg till den efter det som du visar.
doneSvaret är klart. Händelsen har samma fält som svaret från /v1/ask.
errorSvaret avbröts. Strömmen slutar utan done. Se nedan.
followupsEfter done: förslag på följdfrågor som du kan erbjuda som knappar.
allt annatHoppa över det. Nya händelsetyper kan tillkomma när som helst.

En rad som börjar med : (till exempel : ping, som skickas var 15:e sekund medan svaret förbereds) är en kommentar. Hoppa över den.

messages innehåller konversationen hittills, äldst först, och slutar med användarens fråga. Skicka de senaste omgångarna så att följdfrågor som ”och för beställningar från utlandet?” blir förstådda.

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

Utan "stream": true returnerar POST /v1/chat hela svaret som JSON, precis som /v1/ask.

Återuppta en avbruten ström

Anslutningar bryts: ett mobilnät byts, en proxy når sin tidsgräns, din server startar om. Ge varje fråga ett request_id, så överlever svaret avbrottet:

  • Med request_id förbereds svaret oberoende av anslutningen. Ett avbrott stoppar det inte: det slutförs och sparas.
  • För att återuppta skickar du samma anrop igen, med samma request_id, och numret på den senaste händelsen som du tog emot i huvudet Last-Event-ID.
  • Använd ett request_id (till exempel ett UUID) per fråga och samma vid varje nytt försök med den frågan. En ny fråga får ett nytt request_id.

Vad det upprepade anropet får beror på var svaret befinner sig:

SituationDet upprepade anropet får
Svaret skrivs fortfarande eller har just blivit klartHändelserna efter Last-Event-ID, med samma nummer: strömmen fortsätter helt enkelt.
Svaret blev klart för några minuter sedanDet sparade svaret som en token med hela texten och en done, numrerade från 1. Frågan ställs inte igen.
Strömmen kan inte fortsätta från det numretSvaret från början, numrerat från 1.

Din kod hanterar alla tre fallen med en enda regel: om den första händelse som du tar emot har ett nummer som inte är större än Last-Event-ID börjar svaret om; töm det som du visade och bygg upp det igen. Annars lägger du till.

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 kan köras på flera servrar. En ström fortsätter från Last-Event-ID bara på den server som skriver svaret; om ditt upprepade anrop når en annan server får det det sparade svaret när det är klart, eller så skrivs svaret igen från början. Regeln ovan täcker båda fallen, och frågan räknas aldrig två gånger.

Utan request_id avbryts svaret när anslutningen stängs.

Fel i en ström

Fel innan strömmen startar är vanliga HTTP-fel: 401, 403, 422 eller 429 med Retry-After när organisationen har förbrukat sin gräns för frågor eller har för många pågående svar samtidigt.

När strömmen har startat är dess HTTP-status alltid 200, och ett problem kommer som en händelse error:

{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
  • retry: true betyder att svaret avbröts på grund av servern, till exempel under en uppdatering, och att du kan skicka samma anrop igen oförändrat.
  • Andra koder, till exempel timeout, listas i API-referensen. Behandla en okänd kod som vilket annat strömfel som helst: berätta det för användaren och låt användaren fråga igen.

Låt användarna betygsätta svar

Varje svar har ett question_id. Skicka användarens betyg till POST /v1/questions/{question_id}/feedback: 1 för hjälpsamt, -1 för inte hjälpsamt, med valfria reason och note. Betygen hjälper organisationen att se vilka kort som behöver förbättras.

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

Checklista för en bot i produktion

  • En nyckel med bara search och ask, begränsad till de samlingar som botens målgrupp får se.
  • Ett request_id per fråga och regeln för återupptagning ovan.
  • En tidsgräns för läsningen av strömmen som är längre än 15 sekunder, så att kommentarerna : ping håller den vid liv.
  • Svar som återges som Markdown, med hänvisningar som länkar till sina källor.
  • Ett tydligt meddelande när empty är satt, och ett sätt att nå en människa.
  • 429 som hanteras genom att vänta på Retry-After.
  • Betyg som skickas tillbaka, så att kunskapsbasen blir bättre med tiden.