Salt la conținut
Sursa Developers

Construiți un bot de întrebări și răspunsuri

Acest ghid construiește partea de bază a unui bot care răspunde la întrebări din baza de cunoștințe: într-o aplicație de chat, în centrul de asistență sau în interiorul produsului. Asistentul găsește cunoștințele necesare și răspunde, citând fișele pe care le-a folosit; codul dumneavoastră trimite întrebarea și afișează rezultatul.

Aveți nevoie de o cheie cu domeniile de acces search și ask. Răspunsul folosește doar fișele pe care cheia are voie să le citească, așa că rolul, profilul de date și colecțiile cheii decid despre ce poate vorbi botul. Unui bot pentru clienți dați-i o cheie care vede doar colecțiile publice.

Trei moduri de a obține un răspuns

EndpointReturneazăFolosiți-l pentru
POST /v1/searchPasajele care corespund, cele mai potrivite primele. Fără răspuns scris.O casetă de căutare, „articole similare”, propria ordonare.
POST /v1/askUn răspuns complet, cu sursele lui, într-un singur răspuns HTTP.Boți care publică un mesaj pentru fiecare răspuns; joburi pe server.
POST /v1/chatRăspunsul ca flux de evenimente, în timp ce este scris.Interfețe de chat, în care utilizatorul trebuie să vadă cum apare răspunsul.
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"}'

Fiecare rezultat este un pasaj dintr-o fișă: doc_id și doc_name ale fișei, heading al secțiunii, text al pasajului, paginile sau rândurile de tabel în care a fost găsit, precum și încrederea și verificarea fișei.

collection alege colecția în care se caută; fără el, se folosește colecția numită default. tags și card_ids restrâng căutarea la fișele cu acele etichete sau la anumite fișe.

Întrebare și răspuns complet

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

Textul răspunsului din answer este Markdown. Trimiterile din el, cum ar fi [1] sau [1, 3], corespund câmpului ref al intrărilor din sources; legați fiecare trimitere de fișă (doc_id), ca cititorul să poată verifica răspunsul.

Un număr din text care nu a putut fi confirmat pe baza documentelor poate apărea ca marcaj, de exemplu ⟦n:a1⟧. verification.items descrie fiecare marcaj: câmpul lui marker, motivul (reason) și textul original. Dacă codul nu tratează marcajele, înlocuiți-le pe fiecare cu textul lui original.

Când nu există răspuns

Răspunsul este întotdeauna 200. Când asistentul nu poate răspunde, empty spune de ce, iar answer conține un mesaj scurt pe care îl puteți afișa:

emptySemnificație
no_hitsNimic din baza de cunoștințe nu corespunde întrebării.
restrictedSubiectul se află în fișe pe care cheia nu are voie să le citească.
guardrailÎntrebarea depășește ce permite organizația asistentului să răspundă.
no_data_for_periodÎntrebarea se referă la o perioadă pentru care nu există documente.

no_hits este un semnal util: arată ce întreabă utilizatorii și nu este încă acoperit de baza de cunoștințe.

Modurile „Fast” și „Precise”

Transmiteți "quality": "precise" pentru un răspuns mai temeinic la o întrebare dificilă, cu prețul unui timp mai lung. Valoarea implicită, "fast", se potrivește majorității întrebărilor.

Transmiterea răspunsului în flux

Într-o interfață de chat, afișați răspunsul în timp ce este scris. POST /v1/chat cu "stream": true răspunde cu text/event-stream: câte un eveniment pentru fiecare mesaj, fiecare cu un rând id: (numărul lui în flux, de la 1) și un rând data: cu JSON. Câmpul type din JSON spune ce eveniment este.

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":[…]}
typeCe trebuie făcut
metaRăspunsul a început. Păstrați answer_id dacă înregistrați răspunsurile în jurnal.
sourcesFișele pe care se bazează răspunsul. Poate veni de mai multe ori; contează ultimul.
tokenO bucată din textul răspunsului. Adăugați-o la ce afișați.
doneRăspunsul este complet. Are aceleași câmpuri ca răspunsul lui /v1/ask.
errorRăspunsul s-a oprit. Fluxul se încheie fără done. Vedeți mai jos.
followupsDupă done: întrebări următoare sugerate, pe care le puteți oferi ca butoane.
orice altcevaIgnorați-l. Pot apărea oricând tipuri noi de evenimente.

Un rând care începe cu : (de exemplu : ping, trimis la fiecare 15 secunde cât timp răspunsul este pregătit) este un comentariu. Ignorați-l.

messages conține conversația de până acum, de la cel mai vechi mesaj, încheiată cu întrebarea utilizatorului. Trimiteți ultimele câteva schimburi, ca întrebările suplimentare, de exemplu „și pentru comenzile din străinătate?”, să fie înțelese.

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

Fără "stream": true, POST /v1/chat returnează răspunsul complet ca JSON, la fel ca /v1/ask.

Reluarea unui flux întrerupt

Conexiunile se întrerup: se schimbă rețeaua mobilă, un proxy închide conexiunea la expirare, serverul dumneavoastră repornește. Dați fiecărei întrebări un request_id, iar răspunsul supraviețuiește întreruperii:

  • Cu request_id, răspunsul este pregătit independent de conexiune. O deconectare nu îl oprește: el este finalizat și salvat.
  • Pentru a relua, trimiteți din nou aceeași cerere, cu același request_id, și numărul ultimului eveniment primit în antetul Last-Event-ID.
  • Folosiți un singur request_id (de exemplu un UUID) pentru fiecare întrebare și același la fiecare reîncercare a acelei întrebări. O întrebare nouă primește un request_id nou.

Ce primește cererea repetată depinde de stadiul răspunsului:

SituațieCererea repetată primește
Răspunsul este încă scris sau tocmai s-a încheiatEvenimentele de după Last-Event-ID, cu aceleași numere: fluxul pur și simplu continuă.
Răspunsul s-a încheiat acum câteva minuteRăspunsul salvat, ca un token cu tot textul și un done, numerotate de la 1. Întrebarea nu este pusă din nou.
Fluxul nu poate continua de la acel numărRăspunsul de la început, numerotat de la 1.

Codul tratează toate cele trei cazuri cu o singură regulă: dacă primul eveniment primit are un număr cel mult egal cu Last-Event-ID, răspunsul începe din nou; ștergeți ce ați afișat și construiți-l din nou. Altfel, adăugați la ce aveți.

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

API-ul Sursa poate rula pe mai multe servere. Un flux continuă de la Last-Event-ID doar pe serverul care scrie răspunsul; dacă cererea repetată ajunge la alt server, ea primește răspunsul salvat după ce acesta s-a încheiat sau răspunsul este scris din nou de la început. Regula de mai sus acoperă ambele cazuri, iar întrebarea nu este socotită niciodată de două ori.

Fără request_id, închiderea conexiunii anulează răspunsul.

Erori într-un flux

Erorile de dinainte de pornirea fluxului sunt erori HTTP obișnuite: 401, 403, 422 sau 429 cu Retry-After, când organizația și-a epuizat limita de întrebări sau are prea multe răspunsuri în lucru în același timp.

După ce fluxul a pornit, statusul lui HTTP este întotdeauna 200, iar o problemă sosește ca eveniment error:

{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
  • retry: true înseamnă că răspunsul s-a oprit din cauza serverului, de exemplu în timpul unei actualizări, și puteți trimite din nou aceeași cerere, neschimbată.
  • Celelalte coduri, cum ar fi timeout, sunt enumerate în referința API. Tratați un cod necunoscut ca pe orice altă eroare de flux: anunțați utilizatorul și lăsați-l să întrebe din nou.

Evaluarea răspunsurilor de către utilizatori

Fiecare răspuns are un question_id. Trimiteți evaluarea utilizatorului la POST /v1/questions/{question_id}/feedback: 1 pentru util, -1 pentru inutil, cu un reason și o note opționale. Evaluările ajută organizația să vadă ce fișe trebuie îmbunătățite.

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

Listă de verificare pentru un bot în producție

  • O cheie doar cu search și ask, limitată la colecțiile pe care le poate vedea publicul botului.
  • Câte un request_id pentru fiecare întrebare și regula de reluare de mai sus.
  • Un timp de expirare la citirea fluxului mai lung de 15 secunde, ca comentariile : ping să-l mențină activ.
  • Răspunsurile afișate ca Markdown, cu trimiterile legate de sursele lor.
  • Un mesaj clar când empty este setat și o modalitate de a ajunge la o persoană.
  • 429 tratat prin așteptarea indicată de Retry-After.
  • Evaluările trimise înapoi, ca baza de cunoștințe să se îmbunătățească în timp.