Guide — Streaming SSE
Principe
POST /spaces/:id/query supporte deux modes selon l'en-tête Accept :
| Accept |
Mode |
application/json |
Réponse complète |
text/event-stream |
Streaming SSE |
Événements
| Événement |
Contenu |
status |
`{"stage":"retrieving\ |
sources |
{"sources":[...]} — sources avant génération (affichage anticipé) |
delta |
{"text":"..."} — incrément de texte |
done |
Réponse finale complète + sources filtrées + usage |
error |
{"error":{"code":"...","message":"..."}} |
ping |
{} — keepalive toutes les 15 s |
Ruby (net/http)
require "net/http"
require "json"
require "uri"
def stream_query(space_id:, question:, api_key:)
uri = URI("https://pyxaura.com/api/v1/spaces/#{space_id}/query")
accumulated = ""
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{api_key}"
req["Content-Type"] = "application/json"
req["Accept"] = "text/event-stream"
req.body = { question: question, options: { persist: false } }.to_json
http.request(req) do |response|
buffer = ""
response.read_body do |chunk|
buffer += chunk
while (match = buffer.match(/^event: (\w+)\ndata: (.+)\n\n/))
event, data_str = match[1], match[2]
buffer = buffer[match.end(0)..]
data = JSON.parse(data_str) rescue {}
case event
when "delta" then accumulated += data["text"].to_s; print data["text"]
when "done" then puts "\n\n---\nSources : #{data["sources"]&.map { |s| s["title"] }.join(", ")}"
when "error" then raise "Erreur API : #{data.dig("error", "message")}"
end
end
end
end
end
accumulated
end
answer = stream_query(
space_id: "spc_9f2",
question: "Quel est le délai de retour ?",
api_key: ENV["PYXIA_API_KEY"]
)
JavaScript (fetch + ReadableStream)
async function streamQuery({ spaceId, question, apiKey }) {
const response = await fetch(`/api/v1/spaces/${spaceId}/query`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify({ question, options: { persist: false } }),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let answer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const parts = buffer.split("\n\n");
buffer = parts.pop();
for (const part of parts) {
const lines = part.split("\n");
const evLine = lines.find(l => l.startsWith("event:"));
const dataLine = lines.find(l => l.startsWith("data:"));
if (!dataLine) continue;
const event = evLine?.replace("event:", "").trim() ?? "message";
const data = JSON.parse(dataLine.replace("data:", "").trim());
if (event === "delta") { answer += data.text; process.stdout.write(data.text); }
if (event === "done") console.log("\n\nSources :", data.sources?.map(s => s.title));
if (event === "error") throw new Error(data.error?.message);
}
}
return answer;
}
Notes importantes
EventSource natif ne supporte pas les en-têtes custom → utiliser fetch + ReadableStream (voir exemples ci-dessus).
- Reconnexion :
Last-Event-ID non supporté en v1. En cas de coupure, relancer la requête entière.
- Proxy/load balancer : ajouter
X-Accel-Buffering: no (Nginx) ou équivalent pour désactiver le buffering.
- Timeout : le
ping toutes les 15 s maintient la connexion. Configurez votre proxy pour tolérer les connexions longues (ex. proxy_read_timeout 120s dans Nginx).