Kazeia-central/CLAUDE.md

323 lines
18 KiB
Markdown

# Kazeia-central — instructions projet
> Client de poste (PC/Mac) qui pilote les tablettes Kazeia branchées en USB :
> récupération des conversations cliniques, gestion du corpus RAG, et **parité
> complète avec Kazeia-admin** (config, presets, profils, voix, mises à jour).
>
> Ce fichier est le contrat de référence. Il prime sur toute supposition.
---
## 1. Mission et place dans l'écosystème Kazeia
Kazeia est un agent de soutien psychologique **on-device** tournant sur tablettes
Android (OnePlus Pad 3). L'écosystème comprend :
| Composant | Rôle | Repo |
| --- | --- | --- |
| **App patiente** `com.kazeia` | Pipeline STT→LLM→TTS, stocke conversations/profils/RAG/voix | `/opt/Kazeia/kazeia-android/app` |
| **App admin** `com.kazeia.admin` | Console de config **on-device**, parle au provider en local | `/opt/Kazeia/kazeia-android/app-admin` |
| **kazeia-engine** | Moteur natif (GGUF i8mm CPU + .pte QNN NPU), libs JNI | `/opt/Kazeia-engine` (repo séparé) |
| **Kazeia-central** | **Ce projet.** Console de poste qui pilote N tablettes via USB | `/opt/Kazeia-central` (repo séparé, **sibling** de `/opt/Kazeia` et `/opt/Kazeia-engine`) |
Kazeia-central **n'est pas un nouveau pipeline** : c'est une **télécommande de flotte**.
Il parle le **même protocole que Kazeia-admin** (le ContentProvider exporté
`com.kazeia.provider`), mais à distance via `adb` au lieu d'un `ContentResolver`
local. Tout ce que l'admin fait on-device, Kazeia-central doit pouvoir le faire
depuis le PC, sur plusieurs tablettes à la fois.
**Pourquoi un PC plutôt que l'app admin ?** Saisie clavier confortable pour
l'authoring RAG et les prompts, archivage clinique centralisé hors tablette,
gestion de flotte (plusieurs patients/tablettes), et accès à l'**encodeur de voix
Python x86** qui ne tourne que sur PC (cf. §6).
---
## 2. Décisions d'architecture (figées)
1. **Stack : Python (FastAPI) + UI web locale.**
Daemon Python = cœur métier (wrapper adb, store chiffré, intégration encodeur
voix, publication WebDAV). UI = web servie en local (navigateur), wrappable en
app desktop via **pywebview**. Choix motivé par la réutilisation directe de
l'encodeur voix CosyVoice (déjà Python x86) et de l'expertise Python de
l'équipe (Kaz, beta_kazeia).
2. **Transport : ADB + RPC base64-JSON.**
Tout passe par le ContentProvider exporté `com.kazeia.provider` via
`adb shell content call/query/…`. Pour écrire des textes complexes (prompts
multilignes, `presets_json`, fiches RAG), on **ajoute au provider des méthodes
`call()` prenant un JSON encodé base64** (cf. §4.4) — `content --bind` casse sur
`:` et les espaces. **Pas de serveur réseau dans l'app patiente.** Fonctionne
sans root.
3. **Périmètre v1 : parité admin complète.**
Config + presets sampling, profils, conversations, RAG, voix, mises à jour —
tout Kazeia-admin, depuis le PC, en multi-tablette.
4. **Conversations cliniques chiffrées au repos sur le PC.**
Base locale chiffrée (SQLCipher ou âge/`age`), déverrouillée par mot de passe
opérateur. On **conserve le niveau de protection du device**. Rétention et purge
explicites. Contexte RGPD santé (Luxembourg).
---
## 3. Contraintes dures imposées par le code patient (ne pas les contourner)
1. **Le ContentProvider est l'unique surface de pilotage.**
Authority : `com.kazeia.provider`. `exported=true`, pas (encore) de permission
signature → joignable par l'uid `shell` via adb. C'est *exactement* ce que
l'app admin utilise en local.
2. **Les conversations sont chiffrées au repos sur la tablette** (SQLCipher,
passphrase dans `SecureKeyStore`). Conséquence : `adb pull conversations.db`
donne un fichier **illisible**. **Les conversations ne se récupèrent QUE via le
provider** (`/conversations`, `/conversations/session/{id}`), qui les déchiffre
et les sert. Aucun raccourci par le filesystem.
3. **`/data/data/com.kazeia/` est inaccessible en adb** sur device de prod
non-rooté. Donc : config/profils/voix/RAG/updates → **provider** ; gros fichiers
(WAV de voix, modèles) → `adb push/pull` vers le **stockage externe**
`Android/data/com.kazeia/files/`.
4. **La sortie texte d'`adb shell content` est cassée pour le texte riche, dans
les DEUX sens.** En écriture, `content --bind` mange `:` et les espaces. En
lecture, `content query` rend du `Row: 0 col=val, col=val` : dès qu'une valeur
contient un retour à la ligne ou un `:` (ex. `speaker_system_prompt`,
multiligne), le parsing est corrompu. → **Toute I/O de texte non trivial passe
par le RPC base64-JSON (§4.4)**, jamais par `--bind`/`query` brut.
5. **`am startservice` est throttlé sans réveil du provider** ; faire un `content
query`/`call` *avant* pour réveiller le process. (Gotchas test device.)
---
## 4. Contrat de protocole (le « wire »)
Authority : `content://com.kazeia.provider`. Pilotage via
`adb shell content {query|update|insert|delete|call} --uri … [--bind …] [--method …]`.
> ⚠️ La source de vérité reste le code : `KazeiaTelemetryProvider.kt` (patient) et
> les clients `app-admin/.../data/source/Kazeia*Client.kt`. Ce qui suit en est le
> reflet à jour ; re-vérifier avant d'implémenter un endpoint.
### 4.1 Lecture (`content query`)
| URI | Colonnes clés |
| --- | --- |
| `/state` | `pid, uptime_seconds, pss_mb, ion_mb, …` + **handshake à ajouter** : `app_version_code, provider_schema, supported_calls` (cf. §4.4) |
| `/turns` | télémétrie pipeline (timings STT/LLM/TTS par tour) |
| `/crashes` | `timestamp, component, message` |
| `/config` | config complète (cf. §4.5) |
| `/models` | `id, display_name, pte_path, tokenizer_path, role, size_mb, max_seq_len, file_exists, notes` |
| `/profiles`, `/profiles/active`, `/profiles/{id}` | profils patients |
| `/conversations` | sessions : `profile_id, session_id, started_at, ended_at, turn_count` |
| `/conversations/profile/{id}` | sessions d'un profil |
| `/conversations/session/{id}` | tours : `id, profile_id, session_id, timestamp, role(PATIENT\|KAZEIA), text, ttft_ms, total_ms, voice_used, model_used` |
| `/dist_config` | `webdav_base, webdav_user, has_password, is_customized` (jamais le pass en clair) |
| `/rag`, `/rag/{source}` | corpus : index + texte d'un doc |
| `/rag_status` | `enabled, ready, model, dim, doc_count, chunk_count, pending` |
| `/rag_query` (selectionArgs[0] = requête) | `source, score, text` (test retrieval) |
| `/updates` | `phase, app_update_available, app_version_name, app_size, content_count, content_size, progress_done, progress_total, label, error, last_check` |
| `/voices` | `id, name, state, wav_exists, prefix_exists, suffix_exists, wav_size_bytes, wav_duration_seconds, wav_path, created_at` |
### 4.2 Mutation (`content update/insert/delete`) — champs simples
- `update /config` : merge partiel (cf. §4.5). Diffuse `com.kazeia.action.RELOAD_CONFIG`.
- `update /profiles[/{id}]`, `/profiles/active` ; `delete /profiles/{id}` (cascade conv.). Diffuse `RELOAD_PROFILES`.
- `delete /conversations/session/{id}` et `/conversations/profile/{id}`.
- `update /dist_config` : `webdav_base, webdav_user, webdav_pass, reset`.
- `insert /rag` (`source`,`text`) ; `delete /rag/{source}`. Diffuse `RELOAD_RAG`.
- `delete /voices/{id}` (wav + 2 embeddings). Diffuse `RELOAD_VOICES`.
### 4.3 RPC existant (`content call`)
- `update_check``{accepted:true}`, lance `UpdateWorker(mode=check)`. Poller `/updates`.
- `update_install``{accepted:true}`, lance `UpdateWorker(mode=install)`. Poller `/updates`.
### 4.4 RPC base64-JSON — À AJOUTER côté app patiente (modif provider)
Motivation : §3.4 — l'I/O texte d'`adb content` est cassée **dans les deux sens**.
La modif côté Kazeia est **additive** (nouvelles méthodes `call()`, aucun
comportement existant changé → compatible `FROZEN.md`). C'est **le livrable n°1**,
prérequis bloquant des étapes 3-6 de la roadmap. Forme cible :
```
# écriture
adb shell content call --uri content://com.kazeia.provider \
--method cfg_apply_json --arg <base64(JSON)>
# lecture (résultat = base64(JSON) dans le Bundle de retour)
adb shell content call --uri content://com.kazeia.provider --method cfg_dump_json
```
Le `arg` et le retour sont du **base64 d'un objet JSON** (zéro caractère spécial →
survit au shell ET au parsing). Méthodes à spécifier/implémenter :
**Lecture (dump)** — remplace `query` pour tout champ à texte riche :
- `cfg_dump_json` — config complète (prompts multilignes inclus).
- `presets_dump_json``presets_json` complet.
- `profile_dump_json` (arg = id) — profil complet (prompt override inclus).
**Écriture (apply/upsert)** — remplace `--bind` :
- `cfg_apply_json` — patch partiel de config (mêmes clés que §4.5), prompts inclus.
- `presets_apply_json` — remplace/merge `presets_json`.
- `rag_upsert_json``{source, text}` (fiche multiligne).
- `profile_upsert_json` — profil complet (prompt override inclus).
**Export conversations (file-drop)** — PAS via Bundle (limite binder ~1 Mo →
`TransactionTooLargeException` sur un historique réel) :
- `conversations_export` (arg = base64 d'un filtre `{profile_id?, since?, until?}`)
→ le provider **écrit un NDJSON (idéalement chiffré)** dans
`Android/data/com.kazeia/files/exports/` et renvoie le chemin ; le PC fait
`adb pull`. C'est le chemin clinique fiable (volume + parsing + chiffrement).
**Handshake** — pour piloter une flotte hétérogène :
- ajouter à `/state` (ou méthode `capabilities`) : `app_version_code`,
`provider_schema`, `supported_calls` (liste des méthodes présentes). Kazeia-central
s'adapte à la version de chaque tablette au lieu de planter.
**Contrôle d'accès (à spécifier maintenant, activer plus tard)** — le provider est
`exported=true` **sans permission** : toute app sideloadée lit les conversations.
Une `signature permission` casserait l'uid `shell` (donc Kazeia-central) ET l'app
admin → **mauvaise piste**. Retenir un **secret partagé** : token provisionné une
fois, stocké en `EncryptedSharedPreferences`, passé dans les `extras` du `call()` et
vérifié par le provider sur les endpoints sensibles. Préserve l'accès adb.
**Confort authoring local (nice-to-have)** :
- `rag_sync`, `voices_reload` — déclenchent un re-scan après un `adb push` direct de
fichiers, pour éviter l'aller-retour WebDAV en dev mono-tablette.
> **À faire en premier dans ce repo** : rédiger `docs/PROVIDER_RPC_SPEC.md` (signatures,
> schémas JSON, token), à livrer au dev de l'app patiente — même démarche que
> `SAMPLING_ENGINE_SPEC.md` côté kazeia-android.
> Tant que ces méthodes n'existent pas, fallback : `adb push` d'un fichier temp +
> `call` avec le chemin. **Ne jamais bricoler `--bind`/`query` pour du texte riche.**
### 4.5 Schéma config (`/config`, miroir de `ConfigStore.kt`)
- Cascade/TTS : `cascade_enabled`, `tts_enabled`, `stt_engine`, `llm_engine`, `tts_engine`
(prod actuelle : `tts_engine="cosyvoice"`, seul TTS ; `llm_engine`/`stt_engine` vestigiaux).
- RAG : `rag_enabled`, `rag_threshold` (déf. 0.82), `rag_top_k` (déf. 3).
- Debug : `debug_enabled` (masque le bouton logs/métriques côté patient).
- Speaker : `speaker_model_id, speaker_system_prompt, speaker_temperature, speaker_top_p,
speaker_top_k, speaker_repeat_penalty, speaker_presence_penalty, speaker_frequency_penalty,
speaker_max_tokens, speaker_preset_name`.
- Thinker : `thinker_*` (mêmes champs).
- Presets : `presets_json` = tableau `{name, temperature, top_p, top_k, repeat_penalty,
presence_penalty, frequency_penalty, max_tokens}`.
> ⚠️ Le moteur natif n'honore aujourd'hui que `max_tokens` côté sampling
> (cf. `docs/SAMPLING_ENGINE_SPEC.md` de kazeia-android). Kazeia-central pousse
> tous les champs ; leur effet réel dépend de l'avancement du moteur.
### 4.6 Transfert de fichiers (hors provider)
- **Stockage externe** (push/pull OK) : `Android/data/com.kazeia/files/kazeia/…`
- `models/` (LLM/STT/TTS), `voix/voix/{id}.wav`, `qwen3-tts-npu/{id}_voice_{prefix,suffix}.bin`, `rag_corpus/*.{txt,md}`.
- **Bases SQLite** : `kazeia_rag.db` est en **clair** (pullable pour inspection) ;
`conversations.db` est **chiffré** (inutile à puller — passer par le provider).
---
## 5. OTA / publication (WebDAV)
- Catalogue modèles : `catalog.json` ; voix : `voices_catalog.json` ; sur Nextcloud
`box.kazeia.com/.../soft/`. Schéma : `{schema, catalog_version, min_app_version_code,
app{version_code,version_name,remote,size,sha256}, components[{id,label,version,
mandatory,files[{remote,local,size,sha256}]}]}`.
- Génération : script type `make_catalog.py` (calcule taille + sha256). Kazeia-central
doit pouvoir **éditer le corpus localement → publier sur WebDAV → bump
`catalog_version`**, puis déclencher `update_check`/`update_install` par tablette.
- Compte service WebDAV stocké côté app (`/dist_config`), modifiable à distance.
- L'install OTA est **manuelle** (boutons), jamais automatique au lancement.
---
## 6. Intégration encodeur de voix (atout PC)
Workflow voix : **capture** WAV sur tablette → **extraction** des embeddings sur PC
(encodeur CosyVoice/Qwen3-TTS x86, Python) → **sync** des `.bin`/`.cvps` vers N
tablettes. Kazeia-central orchestre les 3 étapes :
1. `adb pull` du WAV depuis `…/files/kazeia/voix/voix/`.
2. Appel **in-process** de l'encodeur Python (raison n°1 du choix Python).
3. `adb push` des embeddings + mise à jour du catalogue voix.
État machine voix : `recorded → processing → ready/error` (refléter dans l'UI).
---
## 7. Organisation du repo (proposée)
```
kazeia-central/
CLAUDE.md # ce fichier
pyproject.toml # FastAPI, uvicorn, pywebview, sqlcipher/age, httpx, pydantic
kazeia_central/
adb/ # wrapper adb : device discovery, content call/query, push/pull
provider/ # client typé du wire (§4) — pydantic models par endpoint
store/ # base chiffrée locale (conversations archivées, audit)
voice/ # bridge encodeur Python CosyVoice
ota/ # édition corpus, make_catalog, push WebDAV
api/ # routes FastAPI consommées par l'UI
web/ # UI (templates/SPA) servie en local
docs/
PROVIDER_RPC_SPEC.md # spec base64-JSON à livrer au dev app patiente (§4.4)
tests/
```
**Repo git autonome à `/opt/Kazeia-central`** — un **sibling** de `/opt/Kazeia` et
`/opt/Kazeia-engine`, PAS un sous-dossier de `/opt/Kazeia`. Raison : le `.gitignore`
whitelist du repo `/opt/Kazeia` ne suit que `kazeia-android/` ; placer Kazeia-central
*dans* `/opt/Kazeia` l'exposerait à ce piège (et un `git clean` y est dangereux). En
sibling, le risque disparaît par construction, exactement comme kazeia-engine.
---
## 8. Sécurité & conformité (RGPD santé)
- Conversations rapatriées = **PII de santé**. Au repos sur PC : **chiffrées**
(SQLCipher/age), clé dérivée d'un mot de passe opérateur, jamais en clair sur disque.
- Journaliser les accès (qui a exporté quoi, quand) — piste d'audit.
- Rétention/purge explicites et configurables ; export PDF/JSON sur action explicite.
- Le mot de passe WebDAV ne transite jamais en clair via `/dist_config` (le provider
ne le renvoie pas) — ne pas le logger.
- Le PC devient le point de concentration clinique : disque chiffré OS **en plus** du
store applicatif chiffré.
---
## 9. Multi-tablette (flotte)
- Découverte par `adb devices -l` ; identifier chaque tablette (serial → label patient).
- Toute action ciblée `-s <serial>`. Opérations de flotte (push corpus, bump OTA) =
fan-out séquentiel par device, avec rapport par tablette (le harness de test montre
que les commandes adb échouent silencieusement par device → toujours vérifier le retour).
---
## 10. Préférences de travail (Richard)
- Répondre en **français**. Code/identifiants dans leur forme d'origine.
- Quand un choix est délégué : **une** recommandation défendue, pas un menu.
- Pas d'empilement de questions ; faire l'hypothèse raisonnable, l'énoncer, avancer.
- Honnêteté technique : valider/rejeter avec arguments concrets, pas de complaisance.
- **Local-first / auto-hébergé** par défaut (pas de SaaS).
- Vérifier en ligne ce qui dépend d'une version/API récente plutôt que deviner.
- Commits : ne committer que sur demande ; finir par
`Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.
## 11. Intangibles (ne JAMAIS toucher)
- `/opt/Kazeia/beta_kazeia/` — référence comportementale FR thérapeutique.
- `/opt/Kazeia/root_oneplus/` — firmware/Magisk.
- `/opt/Kazeia/Kaz/` — chatbot RAG Python du binôme (à *lire* pour s'inspirer du RAG
et des garde-fous, jamais à modifier).
- `/opt/Kazeia/keystore/` — clé de signature (critique).
- Stack patiente **figée** (`/opt/Kazeia/FROZEN.md`) : toute modif du provider (§4.4)
passe par une spec livrée au dev, pas par une réécriture sauvage.
- Jamais de `git clean` dans `/opt/Kazeia` (effacerait les dossiers non suivis).
---
## 12. Roadmap indicative (v1 = parité admin)
0. **Socle** : wrapper adb + client provider typé + découverte device + store chiffré.
1. **Lecture** : dashboard flotte (state/turns/crashes/config/models/updates).
2. **Conversations** : pull via provider → archive chiffrée → export PDF/JSON + purge.
3. **Config & presets** : édition + push (via RPC base64-JSON §4.4).
4. **Profils** : CRUD + profil actif.
5. **RAG** : authoring corpus, test retrieval (`/rag_query`), publication OTA.
6. **Voix** : capture→encodage PC→sync flotte.
7. **Updates** : check/install OTA piloté, bump catalogue.
> Prérequis bloquant des étapes 2-6 : les méthodes `call()` base64-JSON + l'export
> conversations côté app patiente (§4.4). Rédiger `docs/PROVIDER_RPC_SPEC.md` en
> premier, le livrer au dev kazeia-android, puis démarrer le socle Python en
> parallèle (l'étape 0-1, lecture, ne dépend que des endpoints existants).