Pereiti prie turinio
Sursa Developers

Klausimų ir atsakymų boto kūrimas

Ši instrukcija parodo, kaip sukurti boto pagrindą. Botas atsako į klausimus pagal jūsų žinių bazę pokalbių įrankyje, jūsų pagalbos centre ar jūsų produkte. Asistentas randa reikiamas žinias ir atsako, nurodydamas panaudotas korteles; jūsų kodas išsiunčia klausimą ir parodo rezultatą.

Jums reikia rakto su apimtimis search ir ask. Atsakymas naudoja tik korteles, kurias raktui leidžiama skaityti, todėl apie ką botas gali kalbėti, lemia rakto vaidmuo, duomenų profilis ir rinkiniai. Klientams skirtam botui suteikite raktą, kuris mato tik viešus rinkinius.

Trys būdai gauti atsakymą

OperacijaGrąžinaKam naudoti
POST /v1/searchAtitinkančias ištraukas, geriausios – pirmos. Parašyto atsakymo nėra.Paieškos laukui, „susijusiems straipsniams“, savo rikiavimui.
POST /v1/askVisą atsakymą su šaltiniais viename atsake.Botams, kurie kiekvienam atsakymui skelbia vieną žinutę; serverio pusės darbams.
POST /v1/chatAtsakymą kaip įvykių srautą, kol jis rašomas.Pokalbių sąsajoms, kuriose naudotojas turėtų matyti, kaip atsiranda atsakymas.
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"}'

Kiekvienas atitikmuo – kortelės ištrauka: kortelės doc_id ir doc_name, skyriaus heading, ištraukos text, puslapiai ar lentelės eilutės, kuriuose ji rasta, ir kortelės patikimumas bei tikrinimas.

collection pasirenka rinkinį, kuriame ieškoma; be jo naudojamas rinkinys, pavadintas default. tags ir card_ids susiaurina paiešką iki kortelių su tomis žymomis arba iki konkrečių kortelių.

Klausimas ir visas atsakymas

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

Atsakymo tekstas lauke answer yra Markdown formatu. Jo nuorodos, pavyzdžiui, [1] arba [1, 3], atitinka sources įrašų ref; kiekvieną susiekite su kortele (doc_id), kad skaitytojas galėtų patikrinti atsakymą.

Skaičius tekste, kurio nepavyko patvirtinti pagal dokumentus, gali būti parodytas kaip ženklas, pavyzdžiui, ⟦n:a1⟧. verification.items aprašo kiekvieną ženklą: jo marker, reason ir original tekstą. Jei jūsų kodas ženklų neapdoroja, kiekvieną jų pakeiskite jo original tekstu.

Kai atsakymo nėra

Atsakas visada yra 200. Kai asistentas negali atsakyti, empty nurodo priežastį, o answer pateikia trumpą pranešimą, kurį galima parodyti:

emptyReikšmė
no_hitsŽinių bazėje nėra nieko, kas atitiktų klausimą.
restrictedTema yra kortelėse, kurių raktui neleidžiama skaityti.
guardrailKlausimas nepatenka į sritį, kurioje organizacija leidžia asistentui atsakyti.
no_data_for_periodKlausiama apie laikotarpį, už kurį dokumentų nėra.

no_hits – naudingas signalas: jis parodo, ko jūsų naudotojai klausia, bet žinių bazė dar neaprėpia.

„Fast“ ir „Precise“

Perduokite "quality": "precise", kad į sudėtingą klausimą gautumėte išsamesnį atsakymą, nors tai užtrunka ilgiau. Numatytasis "fast" tinka daugumai klausimų.

Atsakymo transliavimas srautu

Pokalbių sąsajoje rodykite atsakymą, kol jis rašomas. POST /v1/chat su "stream": true atsako text/event-stream formatu: po vieną įvykį kiekvienai žinutei, kiekvienas su eilute id: (jo numeris sraute, nuo 1) ir eilute data: su JSON. JSON laukas type nurodo, koks tai įvykis.

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":[…]}
typeKą daryti
metaAtsakymas pradėtas. Išsaugokite answer_id, jei registruojate atsakymus.
sourcesKortelės, kuriomis remiasi atsakymas. Gali ateiti daugiau nei kartą; galioja paskutinis.
tokenAtsakymo teksto dalis. Pridėkite ją prie to, ką rodote.
doneAtsakymas baigtas. Jis turi tuos pačius laukus kaip /v1/ask atsakas.
errorAtsakymas nutrūko. Srautas baigiasi be done. Žr. toliau.
followupsPo done: siūlomi tolesni klausimai, kuriuos galite pateikti kaip mygtukus.
bet kas kitaPraleiskite. Nauji įvykių tipai gali atsirasti bet kada.

Eilutė, prasidedanti : (pavyzdžiui, : ping, siunčiama kas 15 sekundžių, kol atsakymas rengiamas), yra komentaras. Ją praleiskite.

messages pateikia iki šiol vykusį pokalbį: seniausios replikos – pirmos, paskutinė – naudotojo klausimas. Siųskite kelias paskutines replikas, kad būtų suprasti tolesni klausimai, pavyzdžiui, „o užsakymams iš užsienio?“.

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

Be "stream": true POST /v1/chat grąžina visą atsakymą JSON formatu, kaip /v1/ask.

Nutrūkusio srauto tęsimas

Ryšiai nutrūksta: pasikeičia mobilusis tinklas, tarpiniame serveryje baigiasi laiko limitas, jūsų serveris paleidžiamas iš naujo. Kiekvienam klausimui suteikite request_id, ir atsakymas išliks nepaisant nutrūkimo:

  • Su request_id atsakymas rengiamas nepriklausomai nuo ryšio. Atsijungimas jo nesustabdo: jis užbaigiamas ir išsaugomas.
  • Kad tęstumėte, vėl išsiųskite tą pačią užklausą su tuo pačiu request_id, o antraštėje Last-Event-ID nurodykite paskutinio gauto įvykio numerį.
  • Kiekvienam klausimui naudokite vieną request_id (pavyzdžiui, UUID) ir tą patį – kiekviename to klausimo pakartojime. Naujas klausimas gauna naują request_id.

Ką gauna pakartota užklausa, priklauso nuo to, kokiame etape yra atsakymas:

SituacijaPakartota užklausa gauna
Atsakymas dar rašomas arba ką tik baigtasĮvykius po Last-Event-ID, su tais pačiais numeriais: srautas tiesiog tęsiasi.
Atsakymas baigtas prieš kelias minutesIšsaugotą atsakymą kaip token su visu tekstu ir done, numeruotus nuo 1. Klausimas iš naujo neužduodamas.
Srauto nuo to numerio tęsti negalimaAtsakymą nuo pradžios, numeruotą nuo 1.

Jūsų kodas visus tris atvejus apdoroja viena taisykle: jei pirmojo gauto įvykio numeris ne didesnis už Last-Event-ID, atsakymas pradedamas iš naujo; išvalykite tai, ką rodėte, ir sudarykite iš naujo. Kitu atveju pridėkite.

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 gali veikti keliuose serveriuose. Srautas nuo Last-Event-ID tęsiamas tik tame serveryje, kuris rašo atsakymą; jei jūsų pakartota užklausa pasiekia kitą serverį, ji gauna išsaugotą atsakymą, kai šis bus baigtas, arba atsakymas rašomas iš naujo nuo pradžios. Aukščiau aprašyta taisyklė apima abu atvejus, o klausimas niekada neįskaičiuojamas du kartus.

Be request_id ryšio uždarymas atšaukia atsakymą.

Klaidos sraute

Klaidos prieš prasidedant srautui yra įprastos HTTP klaidos: 401, 403, 422 arba 429 su Retry-After, kai organizacija išnaudojo savo klausimų limitą arba vienu metu turi per daug rengiamų atsakymų.

Kai srautas prasidėjo, jo HTTP būsena visada yra 200, o problema atkeliauja kaip įvykis error:

{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
  • retry: true reiškia, kad atsakymas nutrūko dėl serverio, pavyzdžiui, atnaujinimo metu, ir tą pačią užklausą galite išsiųsti dar kartą be pakeitimų.
  • Kiti kodai, pavyzdžiui, timeout, išvardyti API žinyne. Nežinomą kodą vertinkite kaip bet kurią kitą srauto klaidą: praneškite naudotojui ir leiskite jam paklausti dar kartą.

Atsakymų vertinimas

Kiekvienas atsakymas turi question_id. Naudotojo įvertinimą siųskite į POST /v1/questions/{question_id}/feedback: 1 – naudingas, -1 – nenaudingas, galima pridėti reason ir note. Įvertinimai padeda organizacijai matyti, kurias korteles reikia tobulinti.

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

Kontrolinis sąrašas boto paleidimui

  • Raktas tik su search ir ask, apribotas rinkiniais, kuriuos gali matyti boto auditorija.
  • request_id kiekvienam klausimui ir aukščiau aprašyta tęsimo taisyklė.
  • Srauto skaitymo laiko limitas, ilgesnis nei 15 sekundžių, kad : ping komentarai palaikytų ryšį.
  • Atsakymai atvaizduojami kaip Markdown, nuorodos susietos su jų šaltiniais.
  • Aiškus pranešimas, kai nustatytas empty, ir būdas susisiekti su žmogumi.
  • 429 apdorojamas laukiant Retry-After.
  • Įvertinimai siunčiami atgal, kad žinių bazė laikui bėgant gerėtų.