← Retour à l'application

API — Eckhart Tolle

Référence pour intégrer le RAG dans un client tiers (CLI, script, autre application).

1. Vue d'ensemble

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).

2. Authentification

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.

3. POST /ask

Headers

HeaderValeurRequis
Content-Typeapplication/jsonoui
AuthorizationBearer <token>si RAG_AUTH_TOKEN défini côté serveur

Corps JSON

ChampTypeDéfautContraintesDescription
questionstring— (requis)non vide après stripLa question posée par l'utilisateur.
languagestring"français"libreLangue de réponse demandée au LLM. Le corpus étant FR uniquement, mettre "français".
top_kintvaleur serveurclampé [1, 30]Nombre maximum d'extraits retournés. Ignoré si min_score est fourni.
min_scorefloatnull[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.
historyarray[]20 derniers tours, content tronqué à 8000 charsHistorique : [{"role": "user"|"assistant", "content": "..."}, ...].
sourcestringnull"typed" ou "suggested"Origine de la question (saisie libre ou clic sur suggestion). Sert au logging.
conversation_idstringnull1–64 charsIdentifiant client de la conversation (sert au logging).

Réponse

4. Événements SSE

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

5. Exemples

curl

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.

JavaScript (fetch + parsing SSE)

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

Python (requests + iter_lines)

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 ?"))

6. Codes d'erreur

CodeQuandCorps
400question vide ou absente{"detail": "empty question"}
401Token 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

7. Endpoints secondaires