Ugrás a tartalomra
Sursa Developers

Kérdés-válasz bot készítése

Ez az útmutató egy olyan bot magját építi fel, amely a tudásbázisod alapján válaszol a kérdésekre: egy csevegőeszközben, a súgóközpontodban vagy a termékeden belül. Az asszisztens megtalálja a szükséges tudást, és válaszol, megjelölve a felhasznált kártyákat; a kódod elküldi a kérdést, és megjeleníti az eredményt.

Olyan kulcsra van szükséged, amely search és ask hatókörrel rendelkezik. A válasz csak azokat a kártyákat használja, amelyeket a kulcs olvashat, így a kulcs szerepköre, adatprofilja és gyűjteményei döntik el, miről beszélhet a bot. Az ügyfeleknek szóló bot olyan kulcsot kapjon, amely csak a nyilvános gyűjteményeket látja.

A válasz megszerzésének három módja

VégpontMit ad visszaMire használd
POST /v1/searchAz illeszkedő szövegrészleteket, a legjobbal kezdve. Megfogalmazott válasz nélkül.Keresőmező, „kapcsolódó cikkek”, saját rangsorolás.
POST /v1/askTeljes válasz a forrásaival, egyetlen válaszban.Válaszonként egy üzenetet küldő botok; háttérfeladatok.
POST /v1/chatA válasz eseményfolyamként, írás közben.Csevegőfelületek, ahol a felhasználónak látnia kell, ahogy a válasz megjelenik.
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"}'

Minden találat egy kártya szövegrészlete: a kártya doc_id és doc_name értéke, a szakasz heading címe, a szövegrészlet text szövege, azok az oldalak vagy táblázatsorok, ahol megtalálható, valamint a kártya megbízhatósága és ellenőrzése.

A collection kiválasztja, melyik gyűjteményben keress; nélküle a default nevű gyűjtemény a keresés tárgya. A tags és a card_ids az adott címkéjű kártyákra vagy bizonyos kártyákra szűkíti a keresést.

Kérdés és teljes válasz

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

Az answer mezőben lévő válaszszöveg Markdown. A hivatkozásai, például [1] vagy [1, 3], a sources elemeinek ref értékével egyeznek; mindegyiket kapcsold össze a kártyával (doc_id), hogy az olvasó ellenőrizhesse a választ.

A szövegben az a szám, amelyet nem sikerült a dokumentumok alapján megerősíteni, jelölőként jelenhet meg, például ⟦n:a1⟧ alakban. A verification.items minden jelölőt leír: a marker értékét, a reason okot és az original szöveget. Ha a kódod nem kezeli a jelölőket, mindegyiket cseréld le az original szövegére.

Ha nincs válasz

A válasz mindig 200. Ha az asszisztens nem tud válaszolni, az empty megmondja, miért, az answer pedig egy rövid, megjeleníthető üzenetet tartalmaz:

emptyJelentése
no_hitsA tudásbázisban semmi nem illeszkedik a kérdéshez.
restrictedA téma olyan kártyákban van, amelyeket a kulcs nem olvashat.
guardrailA kérdés kívül esik azon, amire a szervezet megengedi az asszisztensnek a választ.
no_data_for_periodA kérdés olyan időszakra vonatkozik, amelyről nincsenek dokumentumok.

A no_hits hasznos jelzés: megmutatja, mit kérdeznek a felhasználóid, amit a tudásbázis még nem fed le.

A „Fast” és a „Precise” mód

Nehéz kérdésre alaposabb válaszért add meg a "quality": "precise" értéket („Precise”, pontos mód), ami több időbe kerül. Az alapértelmezett "fast" („Fast”, gyors mód) a legtöbb kérdéshez megfelel.

A válasz továbbítása adatfolyamként

Csevegőfelületen írás közben jelenítsd meg a választ. A POST /v1/chat a "stream": true értékkel text/event-stream választ ad: üzenetenként egy eseményt, mindegyikben egy id: sorral (az esemény sorszáma az adatfolyamban, 1-től) és egy JSON-t tartalmazó data: sorral. A JSON type mezője mondja meg, mi az esemény.

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 tegyél
metaA válasz elkezdődött. Ha naplózod a válaszokat, őrizd meg az answer_id értékét.
sourcesA kártyák, amelyekre a válasz épül. Többször is érkezhet; az utolsó érvényes.
tokenA válasz szövegének egy darabja. Fűzd hozzá a megjelenített szöveghez.
doneA válasz elkészült. Ugyanazok a mezői, mint a /v1/ask válaszának.
errorA válasz megszakadt. Az adatfolyam done nélkül ér véget. Lásd lent.
followupsA done után: javasolt további kérdések, amelyeket gombokként kínálhatsz fel.
bármi másHagyd ki. Új eseménytípusok bármikor megjelenhetnek.

A : karakterrel kezdődő sor (például a : ping, amely a válasz előkészítése közben 15 másodpercenként érkezik) megjegyzés. Hagyd ki.

A messages az eddigi beszélgetést tartalmazza, a legrégebbitől kezdve, a felhasználó kérdésével a végén. Küldd el az utolsó néhány kört, hogy a további kérdéseket, például „és a külföldi rendeléseknél?”, is értse.

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

"stream": true nélkül a POST /v1/chat JSON-ként adja vissza a teljes választ, mint a /v1/ask.

Megszakadt adatfolyam folytatása

A kapcsolatok megszakadnak: vált a mobilhálózat, egy proxy időtúllépésbe fut, újraindul a szervered. Adj minden kérdésnek request_id azonosítót, és a válasz túléli a megszakadást:

  • request_id azonosítóval a válasz a kapcsolattól függetlenül készül el. A kapcsolat bontása nem állítja le: a válasz elkészül, és a Sursa elmenti.
  • A folytatáshoz küldd el újra ugyanazt a kérést, ugyanazzal a request_id azonosítóval, és a Last-Event-ID fejlécben az utolsó kapott esemény sorszámával.
  • Kérdésenként egy request_id azonosítót használj (például egy UUID-t), és ugyanazt a kérdés minden újrapróbálásakor. Az új kérdés új request_id azonosítót kap.

Hogy mit kap a megismételt kérés, az attól függ, hol tart a válasz:

HelyzetA megismételt kérés ezt kapja
A válasz még íródik, vagy épp most készült elA Last-Event-ID utáni eseményeket, ugyanazokkal a sorszámokkal: az adatfolyam egyszerűen folytatódik.
A válasz néhány perce elkészültA mentett választ egy, a teljes szöveget tartalmazó token eseményként és egy done eseményként, 1-től számozva. A kérdés nem fut le újra.
Az adatfolyam nem folytatható attól a sorszámtólA választ az elejétől, 1-től számozva.

A kódod mindhárom esetet egyetlen szabállyal kezeli: ha az első kapott esemény sorszáma nem nagyobb, mint a Last-Event-ID, a válasz elölről kezdődik; töröld, amit megjelenítettél, és építsd fel újra. Egyébként fűzd hozzá.

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

A Sursa API több szerveren is futhat. Az adatfolyam csak azon a szerveren folytatódik a Last-Event-ID alapján, amelyik éppen a választ írja; ha a megismételt kérésed egy másikhoz jut el, az elkészülte után a mentett választ kapja meg, vagy a válasz az elejétől újra megíródik. A fenti szabály mindkét esetet lefedi, és a kérdés soha nem számít kétszer.

request_id nélkül a kapcsolat bezárása megszakítja a választ.

Hibák az adatfolyamban

Az adatfolyam kezdete előtti hibák szokásos HTTP-hibák: 401, 403, 422, vagy 429 a Retry-After fejléccel, ha a szervezet elhasználta a kérdéskorlátját, vagy egyszerre túl sok folyamatban lévő válasza van.

Miután az adatfolyam elindult, a HTTP-állapotkódja mindig 200, és a probléma error eseményként érkezik:

{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
  • A retry: true azt jelenti, hogy a válasz a szerver miatt szakadt meg, például frissítés közben, és ugyanazt a kérést változatlanul újra elküldheted.
  • A többi kódot, például a timeout kódot, az API-referencia sorolja fel. Az ismeretlen kódot kezeld úgy, mint bármely más adatfolyam-hibát: tájékoztasd a felhasználót, és hagyd, hogy újra kérdezzen.

A válaszok értékelése

Minden válasznak van question_id azonosítója. A felhasználó értékelését küldd el a POST /v1/questions/{question_id}/feedback végpontra: 1 a hasznos, -1 a nem hasznos válaszhoz, opcionális reason és note mezővel. Az értékelések segítenek a szervezetnek látni, mely kártyákon kell még dolgozni.

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

Ellenőrzőlista éles bothoz

  • Csak search és ask hatókörű kulcs, a bot közönsége által látható gyűjteményekre korlátozva.
  • Kérdésenként egy request_id, és a fenti folytatási szabály.
  • Az adatfolyam olvasásának időkorlátja legyen hosszabb 15 másodpercnél, hogy a : ping megjegyzések életben tartsák.
  • Markdownként megjelenített válaszok, a forrásaikhoz kapcsolt hivatkozásokkal.
  • Egyértelmű üzenet, ha az empty be van állítva, és lehetőség arra, hogy a felhasználó élő munkatárshoz forduljon.
  • A 429 kezelése a Retry-After kivárásával.
  • Visszaküldött értékelések, hogy a tudásbázis idővel jobb legyen.