# Prompt #6 — D6 AI Client (LLM FR, contexte MQTT)

**Rôle :**
Tu es _Développeur AI client_ du projet **Skull Pi**. Tu livres un service Python (`svc-ai`) qui consomme le texte ASR, applique un _prompt système_ éditable, appelle un LLM local/remote (modèles **gpt-5-mini** ou **gpt-5-nano**, langue FR), puis publie les réponses prêtes pour TTS. Le service doit être robuste (timeouts, retries, rate-limit), et configurable à chaud.

**Cible & stack :**

- Dossier : `/opt/Skull/apps/ai`, exécution `python -m ai.main`.
- Bus : MQTT local `127.0.0.1:1883`.
- Config : `.env` + topics retained.
- Fichiers : `/opt/Skull/config/prompt.txt` (prompt système par défaut).

---

## Objectif fonctionnel

1. Entrées :

   - `asr/text` → texte utilisateur FR.
   - `ai/request` → requêtes directes (ex. depuis UI).
   - Config : `skull/config/ia`, `skull/config/prompt_system`.

2. Traitement :

   - Construit un _message_ avec **prompt système**, _contexte court_ (quelques tours récents), et _entrée utilisateur_.
   - Appelle le modèle choisi (mini/nano) avec **timeouts**, **retries exponentiels**, **limite tokens**.
   - Applique _post-processing_ (nettoyage, 2–3 phrases max par défaut, FR).
   - Filtrage soft des thèmes (politique : éviter politique/santé/argent/données perso — via prompt système).

3. Sorties :

   - `ai/response` → texte final (pour TTS).
   - `ai/usage` → métadonnées (tokens, latence).
   - `ai/capabilities` (retained).

---

## Spécification MQTT

### Entrées

- `asr/text`

  ```json
  { "ts_ms": 0, "text": "raconte une blague" }
  ```

- `ai/request`

  ```json
  { "ts_ms": 0, "text": "Présente-toi en deux phrases." }
  ```

- `skull/config/ia` (retained)

  ```json
  {
    "model": "gpt-5-mini",
    "lang": "fr",
    "eos_ms": 800,
    "max_tokens": 256,
    "temperature": 0.7,
    "top_p": 0.9
  }
  ```

- `skull/config/prompt_system` (retained)

  ```json
  {
    "text": "Tu es un crâne joyeux, poli, humour léger. Évite politique, santé, argent."
  }
  ```

### Sorties

- `ai/response`

  ```json
  {
    "ts_ms": 0,
    "text": "Salut ! Je suis Skull, ton crâne loquace. Que puis-je faire pour toi ?"
  }
  ```

- `ai/usage`

  ```json
  {
    "ts_ms": 0,
    "latency_ms": 420,
    "input_tokens": 120,
    "output_tokens": 40,
    "model": "gpt-5-mini",
    "cached": false
  }
  ```

- `ai/capabilities` (retained)

  ```json
  { "models": ["gpt-5-mini", "gpt-5-nano"], "lang": "fr", "max_tokens": 512 }
  ```

---

## Détails d’implémentation

1. **Structure package** (`/opt/Skull/apps/ai`)

```
ai/
  __init__.py
  main.py            # boucle MQTT + orchestration
  llm.py             # client modèle (abstraction, retries, timeouts)
  context.py         # mémoire courte (N derniers tours, purge)
  prompt.py          # assemblage prompt système + règles + user
  filters.py         # garde-fous (longueur, thèmes interdits soft)
  mqtt.py
  config.py
  health.py
  tests/
```

2. **Client LLM (llm.py)**

- Interface unique `generate(prompt: str, *, model: str, max_tokens: int, temperature: float, top_p: float, timeout_s: float) -> LLMResult`.
- Retries **expo** (max 3) sur erreurs transitoires (timeouts, 429). _Jitter_ 100–400 ms.
- _Circuit-breaker_ basique : si 5 échecs / 60 s ⇒ bascule **fallback model** (ex. nano) 30 s.
- _Timeout_ par requête configurable (ex. 6 s nano, 12 s mini).
- Journalise latence et tokens (si dispo via API).

3. **Contexte & prompt**

- Chargement initial de `/opt/Skull/config/prompt.txt`.
- Fusion avec `skull/config/prompt_system.text` (priorité retained).
- Contexte court : conserver **dernier tour utilisateur + 1 réponse** (ou 2/2 si `mini`).
- _Format_ :

  ```
  [SYSTEM] {prompt_system}
  [RULES] Réponds en français, concis (2-3 phrases). Évite politique, santé, argent, données personnelles.
  [USER] {texte_utilisateur}
  [CONTEXT] {extraits_récents}
  ```

- Coupe _hard_ si prompt total > budget (réserve 30% pour sortie).

4. **Post-processing & filtres**

- Trim espaces, supprimer balises/code non demandés, normaliser ponctuation FR.
- Limiter longueur : si > 3 phrases, tronquer proprement.
- _Soft policy_: si détection mots-clés sensibles, rediriger gentillement (“Sujet sensible, changeons de thème amusant !”).

5. **Flux & QoS**

- Souscrit à `asr/text` **et** `ai/request`.
- _Debounce_ 300 ms pour éviter doublons ASR.
- File interne (queue) avec _back-pressure_ (max 3 en vol).
- Publie `ai/response` **avant** `voice/tts` (le TTS sera déclenché par orchestrator).

6. **Health & logs**

- `ai/health` périodique (2 s) : `{"ok":true,"latency_p50":...}`.
- Logs JSON → `/opt/Skull/logs/ai.log` (incl. causes retry, bascule fallback).

7. **Sécurité & résilience**

- Si réponse vide / invalide → _retry_ puis message de secours : “Désolé, je n’ai pas compris. Reformule s’il te plaît.”
- Si débit d’entrées trop élevé → ignorer messages ASR tant qu’une génération est en cours (flag `busy`).
- Support _cancel_: si `ai/cancel` reçu, annule le tour en cours (si API le permet) et publie `{"text":"(interrompu)"}`.

---

## Configuration (.env)

```
MQTT_HOST=127.0.0.1
MQTT_PORT=1883
AI_MODEL=gpt-5-mini
AI_TIMEOUT_S=12
AI_MAX_TOKENS=256
AI_TEMPERATURE=0.7
AI_TOP_P=0.9
AI_LANG=fr
```

---

## Déploiement / systemd

- Service `skull-ai.service`.
- Wrapper `/opt/Skull/bin/skull-ai.sh` :

  ```bash
  #!/usr/bin/env bash
  set -euo pipefail
  source /opt/Skull/venv/bin/activate
  exec python -m ai.main
  ```

---

## Livrables

- Code Python _type-hinté_, `ruff` + `mypy` OK.
- Tests unitaires (mocks API LLM) + tests de charge légère (rafale 5 requêtes).
- `README.md` : variables env, schémas MQTT, exemples.

---

## Critères d’acceptation

- Temps médian _asr→ai/response_ ≤ **900 ms** (nano) / ≤ **1500 ms** (mini) sur Pi Zero 2 W avec réseau stable.
- `ai/capabilities` retained correct ≤ 2 s après boot.
- Respect des règles : réponses FR, 2–3 phrases, évitement thèmes sensibles.
- Robustesse :

  - 3 erreurs transitoires → retry + bascule fallback effective.
  - Perte config retained → prompt.txt local utilisé sans crash.

- Débit : soutient 1 requête/s pendant 10 s sans file infinie (drops contrôlés si nécessaire).

---

## Plan de tests unitaires (isolés & faciles)

1. **Assemblage prompt**

   - Entrée user + prompt_system → vérifie sections, langue, coupe budget.

2. **Limiter longueur**

   - Mock réponse longue (10 phrases) → tronquée à 3 avec terminaison propre.

3. **Retries & backoff**

   - Simuler 2 timeouts puis 1 succès → latence cumulée cohérente, publication unique.

4. **Fallback model**

   - Forcer 5 erreurs/60 s → bascule nano, tag `model:"gpt-5-nano"` dans `ai/usage`.

5. **Soft policy**

   - Entrée avec mot-clé “argent” → réponse redirigée polie (phrase générique), aucun contenu financier.

6. **Debounce ASR**

   - Deux `asr/text` à 100 ms d’intervalle même contenu → une seule génération.

7. **Cancel**

   - Lancer génération puis `ai/cancel` → publication `(interrompu)` et libération `busy`.

8. **Capabilities retained**

   - Restart service → `ai/capabilities` présent & conforme.

---

## Test manuel rapide

1. Démarrer le service et s’abonner :

   ```
   mosquitto_sub -t 'ai/#' -v
   ```

2. Envoyer une requête :

   ```
   mosquitto_pub -t ai/request -m '{"text":"Dis une courte devinette."}'
   ```

   → Attendre `ai/response` + `ai/usage`.

3. Charger un prompt système personnalisé :

   ```
   mosquitto_pub -t skull/config/prompt_system -m '{"text":"Tu es un crâne très poli et drôle. Pas de sujets sérieux."}'
   ```

4. Simuler rafale : publier 3 `ai/request` rapides → vérifier queue et réponses séquentielles.
5. Forcer fallback : couper l’API (mock) → vérifier bascule nano et message d’excuse si échec final.

---

**À livrer** : MR “D6 AI — Client LLM robuste (FR)” avec code, tests, service, README.
