Langsung ke konten
Sursa Developers

Membuat bot tanya jawab

Panduan ini membangun inti bot yang menjawab pertanyaan dari basis pengetahuan Anda: di alat chat, di pusat bantuan Anda, atau di dalam produk Anda. Asisten menemukan pengetahuan yang relevan dan menjawab, sambil merujuk kartu yang dipakainya; kode Anda mengirim pertanyaan dan menampilkan hasilnya.

Anda membutuhkan kunci dengan cakupan search dan ask. Jawaban hanya memakai kartu yang boleh dibaca oleh kunci, jadi peran, profil data, dan koleksi kunci menentukan apa yang dapat dibicarakan bot. Berikan bot untuk pelanggan kunci yang hanya melihat koleksi publik.

Tiga cara mendapatkan jawaban

EndpointMengembalikanPakai untuk
POST /v1/searchPetikan yang cocok, yang terbaik lebih dulu. Tanpa jawaban tertulis.Kotak pencarian, “artikel terkait”, pemeringkatan Anda sendiri.
POST /v1/askJawaban lengkap beserta sumbernya, dalam satu respons.Bot yang memposting satu pesan per jawaban; job back-end.
POST /v1/chatJawaban sebagai stream peristiwa, selagi ditulis.Antarmuka chat, tempat pengguna sebaiknya melihat jawaban muncul.
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"}'

Setiap hasil adalah petikan dari sebuah kartu: doc_id dan doc_name kartu, heading bagian, text petikan, halaman atau baris tabel tempat petikan itu ditemukan, serta kepercayaan dan verifikasi kartu.

collection memilih koleksi tempat pencarian dilakukan; tanpa parameter itu, koleksi yang bernama default dipakai. tags dan card_ids mempersempit pencarian ke kartu dengan tag tersebut atau ke kartu tertentu.

Bertanya dan mendapatkan jawaban lengkap

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

Teks jawaban di answer berupa Markdown. Rujukannya, seperti [1] atau [1, 3], sesuai dengan ref dari entri di sources; tautkan masing-masing ke kartunya (doc_id) agar pembaca dapat memeriksa jawabannya.

Angka dalam teks yang tidak dapat dikonfirmasi terhadap dokumen dapat muncul sebagai penanda seperti ⟦n:a1⟧. verification.items menjelaskan setiap penanda: marker, reason, dan teks original-nya. Jika kode Anda tidak menangani penanda, ganti masing-masing dengan teks original-nya.

Saat tidak ada jawaban

Responsnya selalu 200. Jika asisten tidak dapat menjawab, empty menyebutkan alasannya, dan answer berisi pesan singkat yang dapat Anda tampilkan:

emptyArtinya
no_hitsTidak ada apa pun di basis pengetahuan yang cocok dengan pertanyaan.
restrictedTopiknya ada di kartu yang tidak boleh dibaca oleh kunci.
guardrailPertanyaan berada di luar hal yang diizinkan organisasi untuk dijawab asisten.
no_data_for_periodPertanyaan menanyakan periode yang tidak memiliki dokumen.

no_hits adalah sinyal yang berguna: sinyal ini menunjukkan apa yang ditanyakan pengguna Anda yang belum dicakup basis pengetahuan.

Fast dan Precise

Berikan "quality": "precise" untuk jawaban yang lebih menyeluruh atas pertanyaan yang sulit, dengan konsekuensi waktu yang lebih lama. Default-nya, "fast", cocok untuk sebagian besar pertanyaan.

Mengalirkan jawaban

Di antarmuka chat, tampilkan jawaban selagi ditulis. POST /v1/chat dengan "stream": true merespons dengan text/event-stream: satu peristiwa per pesan, masing-masing dengan baris id: (nomornya dalam stream, mulai dari 1) dan baris data: berisi JSON. Field type dari JSON itu menyebutkan jenis peristiwanya.

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":[…]}
typeYang perlu dilakukan
metaJawaban sudah dimulai. Simpan answer_id jika Anda mencatat jawaban.
sourcesKartu yang menjadi dasar jawaban. Dapat datang lebih dari sekali; yang terakhir yang berlaku.
tokenSepenggal teks jawaban. Tambahkan ke apa yang Anda tampilkan.
doneJawaban sudah lengkap. Memiliki field yang sama dengan respons /v1/ask.
errorJawaban berhenti. Stream berakhir tanpa done. Lihat di bawah.
followupsSetelah done: saran pertanyaan berikutnya yang dapat Anda tawarkan sebagai tombol.
lainnyaLewati. Jenis peristiwa baru dapat muncul kapan saja.

Baris yang diawali : (seperti : ping, yang dikirim setiap 15 detik selama jawaban sedang disiapkan) adalah komentar. Lewati baris itu.

messages berisi percakapan sejauh ini, yang terlama lebih dulu, diakhiri dengan pertanyaan pengguna. Kirim beberapa giliran terakhir agar pertanyaan lanjutan seperti “dan untuk pesanan dari luar negeri?” dapat dipahami.

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

Tanpa "stream": true, POST /v1/chat mengembalikan jawaban lengkap sebagai JSON, seperti /v1/ask.

Melanjutkan stream yang terputus

Koneksi bisa putus: jaringan seluler berganti, proxy terkena timeout, server Anda dimulai ulang. Berikan setiap pertanyaan request_id, dan jawabannya bertahan dari putusnya koneksi:

  • Dengan request_id, jawaban disiapkan terlepas dari koneksi. Koneksi yang terputus tidak menghentikannya: jawaban diselesaikan dan disimpan.
  • Untuk melanjutkan, kirim lagi permintaan yang sama, dengan request_id yang sama, dan nomor peristiwa terakhir yang Anda terima di header Last-Event-ID.
  • Pakai satu request_id (misalnya UUID) per pertanyaan, dan yang sama di setiap percobaan ulang pertanyaan itu. Pertanyaan baru mendapat request_id baru.

Apa yang diterima permintaan yang diulang bergantung pada keadaan jawabannya:

SituasiPermintaan yang diulang menerima
Jawaban masih ditulis, atau baru saja selesaiPeristiwa setelah Last-Event-ID, dengan nomor yang sama: stream cukup berlanjut.
Jawaban sudah selesai beberapa menit yang laluJawaban yang tersimpan sebagai satu token berisi seluruh teks dan satu done, bernomor mulai dari 1. Pertanyaan tidak diajukan lagi.
Stream tidak dapat berlanjut dari nomor ituJawaban dari awal, bernomor mulai dari 1.

Kode Anda menangani ketiga kasus dengan satu aturan: jika peristiwa pertama yang Anda terima memiliki nomor yang tidak lebih besar daripada Last-Event-ID, jawaban dimulai dari awal; hapus apa yang sudah Anda tampilkan dan bangun lagi. Jika tidak, tambahkan.

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 Sursa dapat berjalan di beberapa server. Stream berlanjut dari Last-Event-ID hanya di server yang sedang menulis jawaban; jika permintaan yang diulang sampai ke server lain, permintaan itu menerima jawaban yang tersimpan setelah jawaban selesai, atau jawaban ditulis lagi dari awal. Aturan di atas mencakup kedua kasus, dan pertanyaan tidak pernah dihitung dua kali.

Tanpa request_id, menutup koneksi akan membatalkan jawaban.

Error dalam stream

Error sebelum stream dimulai adalah error HTTP biasa: 401, 403, 422, atau 429 dengan Retry-After saat organisasi sudah memakai habis batas pertanyaannya atau memiliki terlalu banyak jawaban yang sedang diproses sekaligus.

Setelah stream dimulai, status HTTP-nya selalu 200, dan masalah datang sebagai peristiwa error:

{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
  • retry: true berarti jawaban berhenti karena server, misalnya saat pembaruan, dan Anda dapat mengirim lagi permintaan yang sama tanpa perubahan.
  • Kode lain, seperti timeout, dicantumkan di referensi API. Perlakukan kode yang tidak dikenal seperti error stream lainnya: beri tahu pengguna, dan biarkan mereka bertanya lagi.

Memungkinkan pengguna menilai jawaban

Setiap jawaban memiliki question_id. Kirim penilaian pengguna ke POST /v1/questions/{question_id}/feedback: 1 untuk membantu, -1 untuk tidak membantu, dengan reason dan note opsional. Penilaian membantu organisasi melihat kartu mana yang perlu diperbaiki.

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

Daftar periksa untuk bot produksi

  • Kunci yang hanya memiliki search dan ask, dibatasi pada koleksi yang boleh dilihat audiens bot.
  • request_id per pertanyaan, dan aturan melanjutkan di atas.
  • Timeout pembacaan stream yang lebih lama dari 15 detik, agar komentar : ping menjaganya tetap hidup.
  • Jawaban dirender sebagai Markdown, dengan rujukan yang ditautkan ke sumbernya.
  • Pesan yang jelas saat empty diisi, dan cara untuk menghubungi seseorang.
  • 429 ditangani dengan menunggu selama Retry-After.
  • Penilaian dikirim kembali, agar basis pengetahuan makin baik dari waktu ke waktu.