Référence pour intégrer le RAG dans un client tiers (CLI, script, autre application).
L'API expose un seul endpoint utile : POST /ask. La réponse arrive en flux Server-Sent Events (SSE) : sources d'abord, puis tokens de la réponse au fil de la génération, puis suggestions de questions de suivi, puis fin.
L'authentification par Bearer token est optionnelle et dépend de la configuration du serveur (variable d'environnement RAG_AUTH_TOKEN).
L'URL de base correspond à l'origine où l'application est servie (avec préfixe RAG_BASE_PATH si configuré, ex. https://pop-os.tail114371.ts.net/eckt).
Si le serveur a été démarré avec la variable RAG_AUTH_TOKEN définie, toutes les requêtes vers /ask doivent inclure :
Authorization: Bearer <token>
Sans token (ou avec un token incorrect), la réponse est 401 Unauthorized :
{ "detail": "unauthorized" }
Si RAG_AUTH_TOKEN n'est pas défini côté serveur, l'endpoint est ouvert (pas de header requis).
/api, /healthz, /files/* et /thumbs/* ne sont jamais gatés.
| Header | Valeur | Requis |
|---|---|---|
Content-Type | application/json | oui |
Authorization | Bearer <token> | si RAG_AUTH_TOKEN défini côté serveur |
| Champ | Type | Défaut | Contraintes | Description |
|---|---|---|---|---|
question | string | — (requis) | non vide après strip | La question posée par l'utilisateur. |
language | string | "français" | libre | Langue de réponse demandée au LLM. Le corpus étant FR uniquement, mettre "français". |
top_k | int | valeur serveur | clampé [1, 30] | Nombre maximum d'extraits retournés. Ignoré si min_score est fourni. |
min_score | float | null | [0.0, 1.0] | Si fourni, remplace top_k : remonte tous les extraits dont le score de fusion dépasse ce seuil. Si aucun résultat n'atteint ce seuil, le serveur l'abaisse automatiquement par paliers de 0.05 jusqu'à un plancher de 0.20. |
history | array | [] | 20 derniers tours, content tronqué à 8000 chars | Historique : [{"role": "user"|"assistant", "content": "..."}, ...]. |
source | string | null | "typed" ou "suggested" | Origine de la question (saisie libre ou clic sur suggestion). Sert au logging. |
conversation_id | string | null | 1–64 chars | Identifiant client de la conversation (sert au logging). |
Content-Type: text/event-streamCache-Control: no-cache, X-Accel-Buffering: nodata: <json>\n\nÉmis dans cet ordre. Chaque événement est sur sa propre ligne data: ..., suivie d'une ligne vide. Les événements answer_done et suggestions ne sont pas émis si la réponse est vide (court-circuit serveur) — les clients doivent traiter leur absence comme normale.
sources — émis une seule fois, en premier{
"type": "sources",
"sources": [
{
"index": 1,
"kind": "audio",
"doc_kind": null,
"epistemic": null,
"titre": "...",
"site": null,
"categorie": null,
"date_publication": null,
"verbatim": false,
"file": "001-...",
"start": 12.34,
"end": 45.67,
"start_fmt": "00:00:12",
"end_fmt": "00:00:45",
"score": 0.81,
"text": "extrait...",
"video_id": "abc",
"url": "https://www.youtube.com/watch?v=abc&t=12s",
"pdf_url": null,
"thumb_url": "https://img.youtube.com/vi/abc/mqdefault.jpg"
}
]
}
Tous les champs sont toujours présents ; les non-applicables valent null. kind peut être "audio" (transcription Whisper) ou "text" (document indexé). verbatim: true signale un document de référence (biographie, glossaire) restitué littéralement.
Pour les sources "audio" : thumb_url contient la vignette YouTube (mqdefault.jpg) et pdf_url vaut null. Pour les sources "text" : pdf_url et thumb_url pointent vers /files/<basename>.pdf et /thumbs/<basename>.jpg si ces assets existent (sinon null).
context — émis une fois, juste avant le premier token{
"type": "context",
"used_chars": 18450,
"used_tokens": 5640,
"limit_tokens": 256000,
"limit_chars": 800000,
"sources_kept": 5,
"sources_total": 7,
"history_pairs_kept": 3,
"history_pairs_total": 5
}
Informationnel : décrit ce que le serveur a effectivement injecté dans le prompt LLM (sources et tours d'historique conservés vs. soumis, taille en chars et tokens estimés vs. budgets). Sert à alimenter une jauge côté client. Les clients qui n'en ont pas l'usage peuvent ignorer cet événement sans risque.
token — émis N fois pendant la génération{ "type": "token", "text": "morceau de réponse" }
À concaténer dans l'ordre d'arrivée pour reconstituer la réponse complète.
answer_done — émis une fois, marqueur de fin de génération{ "type": "answer_done" }
Permet d'afficher un état « génération des suggestions… » pendant que celles-ci se calculent (étape plus lente).
suggestions — émis une fois, après answer_done{ "type": "suggestions", "suggestions": ["question 1", "question 2", "..."] }
Liste possiblement vide.
error — émis si une exception interrompt le stream{ "type": "error", "message": "..." }
done — toujours émis en dernier{ "type": "done" }
curl -N -X POST https://pop-os.tail114371.ts.net/eckt/ask \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $RAG_TOKEN" \
-d '{"question": "C'\''est quoi un shadok ?"}'
L'option -N désactive le buffering pour voir les événements arriver en direct. Le header Authorization n'est utile que si RAG_AUTH_TOKEN est défini côté serveur.
async function ask(question, { token, baseUrl = '' } = {}) {
const res = await fetch(`${baseUrl}/ask`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
},
body: JSON.stringify({ question }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const reader = res.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 blocks = buffer.split('\n\n');
buffer = blocks.pop(); // bloc partiel restant
for (const block of blocks) {
const line = block.split('\n').find(l => l.startsWith('data: '));
if (!line) continue;
const evt = JSON.parse(line.slice(6));
if (evt.type === 'sources') console.log('Sources:', evt.sources.length);
else if (evt.type === 'token') answer += evt.text;
else if (evt.type === 'answer_done')console.log('(génération terminée)');
else if (evt.type === 'suggestions')console.log('Suggestions:', evt.suggestions);
else if (evt.type === 'error') throw new Error(evt.message);
else if (evt.type === 'done') return answer; // marqueur final ; le while se termine aussi quand le serveur ferme
}
}
return answer;
}
// Usage : ask("Qu'est-ce qu'un shadok ?", { baseUrl: '' })
// .then(a => console.log(a));
import json
import requests
def ask(question, token=None, base_url="https://pop-os.tail114371.ts.net/eckt"):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
r = requests.post(
f"{base_url}/ask",
headers=headers,
json={"question": question},
stream=True,
)
r.raise_for_status()
answer_parts = []
for line in r.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue
evt = json.loads(line[6:])
t = evt["type"]
if t == "sources":
print(f"Sources: {len(evt['sources'])}")
elif t == "token":
answer_parts.append(evt["text"])
elif t == "answer_done":
print("(génération terminée)")
elif t == "suggestions":
print(f"Suggestions: {evt['suggestions']}")
elif t == "error":
raise RuntimeError(evt["message"])
return "".join(answer_parts)
if __name__ == "__main__":
print(ask("Qu'est-ce qu'un shadok ?"))
| Code | Quand | Corps |
|---|---|---|
400 | question vide ou absente | {"detail": "empty question"} |
401 | Token absent ou faux (si RAG_AUTH_TOKEN défini) | {"detail": "unauthorized"} |
| (stream) | Erreur en cours de génération | événement SSE error puis done |
GET /healthz → {"ok": true}. Pour monitoring / probe.GET /files/<chemin> et GET /thumbs/<chemin> : assets (PDF et vignettes) référencés dans pdf_url / thumb_url des sources. À utiliser tels quels.