Ajout de la partie IA

This commit is contained in:
2026-04-20 14:52:20 +02:00
parent 94bbf8beff
commit 5b133aa2fe
50 changed files with 3236 additions and 11 deletions

View File

@@ -213,11 +213,204 @@ Tous des `List<String>` d'IDs de LoreNode :
- [ ] Composant Angular réutilisable : `app-lore-link-picker` (autocomplete + liste de chips)
- [ ] Endpoint `GET /api/lore-nodes?ids=a,b,c` (résolution multi-IDs) côté Java
#### Phase 3 : Backend Python (Priorité basse - PLUS TARD)
- [ ] Initialisation de la structure
- [ ] Configuration Ollama en local
- [ ] Adaptateur OllamaProvider
- [ ] Routes API pour la génération
#### Phase 3 : Backend Python — Brain IA (DÉMARRÉE le 19 avril 2026)
Stack retenue : **FastAPI + Ollama local + Architecture Hexagonale** (Ports/Adapters via `Protocol` PEP 544). Le Brain est l'executor cognitif : il reçoit des demandes du Core Java, construit un prompt, appelle un LLM et renvoie un résultat structuré. Le Core Java reste le chef d'orchestre (règle `.windsurfrules` ligne 21).
##### Étape b1 — Squelette FastAPI ✅
- [x] Structure `brain/app/` + venv + `requirements.txt` minimal (fastapi, uvicorn).
- [x] Endpoint `GET /health` (sonde de vie) et Swagger UI auto sur `/docs`.
- [x] `.gitignore` (venv, __pycache__, .env).
##### Étape b2 — Refactor hexagonal ✅
- [x] Config Pydantic Settings (`.env.example` + `app/core/config.py` + `get_settings()` singleton via `@lru_cache`).
- [x] Port `LLMProvider` (Protocol PEP 544) + exception domaine `LLMProviderError` dans `app/domain/ports.py`.
- [x] Adapter `OllamaLLMProvider` dans `app/infrastructure/ollama_adapter.py` — isole tout le code `httpx` et le protocole Ollama.
- [x] Factory `get_llm_provider()` dans `main.py` = **unique point d'inversion de dépendance** (changer d'1 ligne pour switch vers OpenAI/Claude demain).
- [x] Controller `POST /generate` fin : reçoit le port via `Depends`, ignore l'Adapter concret.
- [x] Fiche academy `docs/academy/hexagonal-python.md` (théorie + analogie JDR + Python vs Java + quiz 5 QCM).
- Modèle par défaut : `gemma4:e2b` (validé avec sortie "Borin le forgeron nain" en ~2s). Swap possible via `LLM_MODEL` dans `.env`.
##### Étape b3 — Génération structurée JSON ✅
- [x] **b3.1** — Modèles de domaine `PageGenerationContext` / `PageGenerationResult` en `@dataclass(frozen=True)` dans `app/domain/models.py` (domaine sans dépendance Pydantic).
- [x] **b3.1** — Port `LLMProvider.generate()` enrichi d'un kwarg `output_format: str | None` (pass-through vers `format: "json"` d'Ollama).
- [x] **b3.2** — Use case `GeneratePageUseCase` dans `app/application/generate_page.py` : construction prompt système (français, orienté MJ de JDR) + appel LLM avec `output_format="json"` + parsing JSON défensif (filtrage sur `template_fields`, champs manquants → chaîne vide, cast `str` systématique).
- [x] **b3.3** — Endpoint `POST /generate-page` + DTOs Pydantic `GeneratePageRequestDTO` / `GeneratePageResponseDTO` en frontière HTTP + factory `get_generate_page_use_case()` pour l'injection du use case.
##### Étape b4 — Branchement Core Java ↔ Brain ✅ (19 avril 2026, soir)
> ✅ **Chaîne complète opérationnelle** : clic "Assistant IA" dans `page-edit` → Angular → Java (`PageGenerationController` → `GeneratePageValuesUseCase` → `BrainAiClient`) → Python (`/generate-page`) → Ollama → retour JSON → merge dans les textareas. Zéro persistance côté génération, l'utilisateur valide et sauvegarde manuellement. Une nouvelle fiche academy `docs/academy/bounded-context.md` a été ajoutée pour formaliser le 3ᵉ Bounded Context (GenerationContext).
>
> **Démarrage de la stack complète pour tester :**
> ```bash
> # Terminal 1 — Ollama (normalement déjà en service système)
> # http://localhost:11434, modèle `gemma4:e2b` tiré
>
> # Terminal 2 — Brain Python
> cd brain && source .venv/Scripts/activate && uvicorn app.main:app --reload --port 8000
>
> # Terminal 3 — Core Java
> cd core && mvn spring-boot:run
>
> # Terminal 4 — Frontend Angular
> cd web && npm start
> ```
###### b4.1 — Domaine DDD côté Java (GenerationContext) ✅
- [x] Package `domain/generationcontext/` créé.
- [x] `GenerationContext` (immuable `@Value @Builder`) + `GenerationResult` (immuable `@Value`).
- [x] Port `AiProvider` (interface pure, zéro annotation Spring) + exception domaine `AiProviderException` (RuntimeException).
###### b4.2 — Adapter HTTP Java → Brain Python ✅
- [x] `BrainAiClient` dans `infrastructure/ai/` — implémente `AiProvider`.
- [x] **Revirement technique assumé** : `RestTemplate` retenu plutôt que `WebClient`. Raisons : déjà présent dans `spring-boot-starter-web` (zéro nouvelle dépendance), usage synchrone suffisant pour un bouton "Assistant IA" (pas de streaming au MVP), plus simple à lire pour un développeur qui n'a pas encore rencontré le paradigme réactif Reactor. Le passage à `WebClient` reste possible sans toucher au domaine (c'est précisément le bénéfice de l'hexagonal).
- [x] Config `brain.base-url` + `brain.timeout-seconds` dans `application.properties`, bean `RestTemplate` dédié dans `RestTemplateConfig` (connect 10s, read 120s).
- [x] DTOs package-private `BrainGeneratePageRequest` / `BrainGeneratePageResponse` avec `@JsonProperty` snake_case (pas de config Jackson globale — isolation au package `infrastructure/ai`).
- [x] Traduction d'erreurs : `ResourceAccessException` (timeout/Brain down) / `RestClientResponseException` (4xx/5xx) → `AiProviderException`. Filet de sécurité pour toute autre `Exception`.
###### b4.3 — Use case Java `GeneratePageValuesUseCase` ✅
- [x] `application/generationcontext/GeneratePageValuesUseCase.java` — injection constructeur des 5 ports (PageRepository, TemplateRepository, LoreRepository, LoreNodeRepository, AiProvider).
- [x] `execute(pageId)` : 4 lookups (Page → Template → Lore → LoreNode), validation template.fields non-vide, construction du `GenerationContext`, appel `AiProvider.generatePage`, retour direct de `result.values`.
- [x] **Décision produit respectée** : zéro persistance. Exceptions différenciées : `IllegalArgumentException` (page introuvable) vs `IllegalStateException` (incohérence BDD ou template sans champs).
###### b4.4 — REST endpoint Java ✅
- [x] **Écart assumé vs plan initial** : endpoint placé dans un nouveau `PageGenerationController` dédié (pas dans `PageController`) — SRP strict, alignement sur le Bounded Context `generationcontext`. URL RESTful conservée : `POST /api/pages/{id}/generate`.
- [x] DTO `GenerationSuggestionsDTO { Map<String, String> values }` dans `infrastructure/web/dto/generationcontext/`.
- [x] Gestion d'erreurs inline (pas d'`@ControllerAdvice` — cohérent avec le style du reste du projet) : 200, 404, 422 (template sans champs, détecté sur le message), 502 (`AiProviderException`), 500 (autre `IllegalStateException`).
###### b4.5 — Frontend Angular : bouton "Assistant IA" branché ✅
- [x] `PageService.generateValues(pageId)``POST /api/pages/{id}/generate`, retourne `Record<string, string>`.
- [x] `page-edit` : état `aiLoading` + `aiError`, méthode `runAssistantAI()`, libellé du bouton qui passe à "Génération…" pendant l'appel, bouton désactivé si template sans champs ou appel en cours.
- [x] Merge soft simplifié : toute suggestion non-vide écrase (l'utilisateur a demandé la régénération), suggestion vide laisse la valeur courante intacte.
- [x] Banner d'erreur dismissable au-dessus du formulaire : message différencié 502 (Brain down) vs autre.
- [x] Pas de sauvegarde auto — l'utilisateur valide et clique "Sauvegarder" pour persister via le PUT existant.
###### b4.6 — Améliorations post-MVP (backlog, pas bloquant)
- [ ] Température LLM configurable côté UI (slider "créativité" mappé sur le paramètre `temperature` d'Ollama).
- [ ] Historique des générations par page (BDD côté Java + vue "revert to previous suggestion").
- [ ] Retry automatique avec backoff côté `BrainAiClient` en cas d'erreur transitoire.
- [ ] Prompt personnalisable par Lore (ex: ton "sombre et épique" vs "aventure familiale") — stocké sur l'entité `Lore` côté Java, transmis dans le `GenerationContext`.
##### Étape b5 — Chat IA conversationnel avec Structural Context ✅ (19 avril 2026, soir)
> ✅ **UX "IA qui écrit sous tes yeux"** livrée de bout en bout. Drawer chat à droite de `page-edit` (pattern validé sur les maquettes `lore/Assistance IA dans une page.png` et `campagne/Assistance IA.png`). L'IA voit la structure du Lore (dossiers, pages, templates, tags) sans recevoir le contenu. Conversation éphémère (perdue à la fermeture du drawer). Intégration limitée au Lore pour l'instant — Campagne viendra quand on voudra l'étendre.
###### b5.1 — Backend Python : endpoint `/chat/stream` SSE ✅
- [x] Dataclasses `ChatMessage` + `LoreStructuralContext` dans `domain/models.py` (immuables, sans Pydantic).
- [x] Protocol `LLMChatProvider` dans `domain/ports.py` — distinct de `LLMProvider` par ISP (Interface Segregation Principle). `OllamaLLMProvider` satisfait les deux par duck typing.
- [x] `OllamaLLMProvider.stream_chat()` : consomme `/api/chat` d'Ollama en mode `stream=True`, parse le NDJSON ligne par ligne, yield les tokens non-vides. Le formatage SSE est la responsabilité du controller, pas de l'adapter.
- [x] `ChatUseCase` dans `application/chat.py` : construit un system prompt riche avec le Structural Context (carte des dossiers/pages/templates/tags), délègue au port.
- [x] Endpoint `POST /chat/stream` avec `StreamingResponse(media_type="text/event-stream")`. Format de flux : `data: {"token":"..."}`, `event: done`, `event: error`.
###### b5.2 — Core Java : port `AiChatProvider` + adapter `WebClient` SSE ✅
- [x] Ajout de `spring-boot-starter-webflux` au `pom.xml` (requis pour `WebClient`, seul outil Spring capable de consommer SSE côté client) + `spring.main.web-application-type=servlet` dans `application.properties` pour forcer Tomcat malgré WebFlux.
- [x] Domaine `generationcontext/` : `ChatMessage`, `LoreStructuralContext` (+ inner `FolderPage`), `ChatRequest`, port `AiChatProvider`.
- [x] **Choix pédagogique : API par callbacks** (`Consumer<String> onToken`, `Runnable onComplete`, `Consumer<Throwable> onError`) plutôt que `Flux<String>`. Raisons : zéro dépendance Reactor dans le domaine, plus simple à comprendre pour un développeur qui n'a pas rencontré le paradigme réactif, mappage naturel vers `SseEmitter` côté controller.
- [x] Adapter `BrainAiChatClient` : `WebClient.retrieve().bodyToFlux(ServerSentEvent)`, dispatch `doOnNext` → callbacks, `blockLast()` pour rester synchrone, timeout 120s, traduction d'erreurs en `AiProviderException`.
###### b5.3 — Core Java : endpoint `POST /api/ai/chat/stream` SSE ✅
- [x] Use case `StreamChatForLoreUseCase` dans `application/generationcontext/` : charge `Lore` + `LoreNode[]` + `Page[]` + `Template[]` (4 lookups), construit le `LoreStructuralContext`, délègue au port. 4 ports injectés côté LoreContext + 1 côté GenerationContext.
- [x] `AiChatController` expose `POST /api/ai/chat/stream` (`produces = text/event-stream`). Le streaming tourne dans un thread séparé via `AsyncTaskExecutor` pour ne pas bloquer le servlet. `SseEmitter` (timeout 5 min) thread-safe : les callbacks du port peuvent écrire dessus depuis n'importe quel thread.
- [x] DTOs `ChatMessageDTO` + `ChatStreamRequestDTO` dans `infrastructure/web/dto/generationcontext/`. Helper `jsonEscape()` interne (pas de pull Jackson ici).
###### b5.4 — Frontend : composant `app-ai-chat-drawer` ✅
- [x] Service `AiChatService.streamChat()` dans `web/src/app/services/ai-chat.service.ts` : `fetch()` + `ReadableStream` + décodage ligne-par-ligne SSE. Retourne un `Observable<ChatStreamEvent>` qui emit `{type:'token'}` par fragment, complete sur `event: done`, error sur `event: error` ou échec réseau. Annule proprement via `AbortController` à l'unsubscribe.
- [x] **Pas d'`EventSource`** : l'API navigateur native ne supporte que GET sans body — on a besoin de POST avec JSON (messages + loreId).
- [x] Composant standalone `AiChatDrawerComponent` dans `shared/ai-chat-drawer/` : `@Input` `loreId`/`isOpen`/`welcomeMessage`/`quickSuggestions[]`/`primaryAction`, `@Output` `close`/`primaryActionClick`. État local : `messages[]`, `currentAssistantText` (buffer de streaming), `isStreaming`, `errorMessage`. Conversation éphémère perdue à la fermeture (choix MVP assumé).
- [x] UX fidèle aux maquettes : bulles user (droite violet) / assistant (gauche sombre), welcome message, typing-indicator avant le premier token, caret clignotant pendant le streaming, suggestions rapides en bas, input + bouton envoyer.
###### b5.5 — Intégration dans `page-edit` ✅
- [x] Bouton "Assistant IA" du header change de rôle : il ouvre désormais le drawer (`toggleChat()`) au lieu d'appeler le one-shot directement.
- [x] Le one-shot b4 reste accessible via `primaryAction` du drawer (bouton violet pleine largeur "Remplir automatiquement tous les champs") : clic → ferme le drawer + déclenche `runAssistantAI()` → textareas se remplissent. Le meilleur des deux mondes, sans duplication de code.
- [x] Suggestions rapides hardcodées (MVP) : "Étoffe l'histoire", "Suggère des liens avec d'autres pages du Lore", "Propose une intrigue secondaire".
- [x] Bouton "Assistant IA" stylé `active` quand le drawer est ouvert (fond gris + bordure violette) — feedback visuel clair.
###### b5.6 — Fiche academy `streaming-sse-rag.md` ✅
- [x] Théorie : SSE vs WebSocket vs polling avec analogie JDR (pigeon voyageur qui revient par fragments).
- [x] Structural Context vs RAG sémantique : pourquoi on n'a PAS encore de DB vectorielle.
- [x] Code réel extrait des 3 étages de la stack (Python / Java / Angular).
- [x] Section "Le savais-tu ?" sur la nature debug-friendly du format SSE (`curl -N` suffit).
- [x] Quiz 5 QCM.
###### b5.7 — À faire plus tard (étendre au reste de l'app)
- [ ] Intégration du drawer dans `arc-edit`, `chapter-edit`, `scene-edit` (Campagne). Nécessite un nouveau port `AiChatProvider.streamChatForCampaign(campaignId, messages)` qui charge Campagne courante + Lore associé (asymétrie demandée : une Campagne voit son Lore, un Lore ne voit PAS ses campagnes).
- [ ] Persistance optionnelle de la conversation (entité `Conversation` côté Java, historique reprenable).
##### Étape b6 — IA dans la création de page (wizard) ✅ (20 avril 2026, nuit)
> ✅ **Mode wizard livré.** Sur `page-create`, bouton "✨ Créer avec l'IA" à côté du "Créer la page" classique. Au clic, le drawer chat s'ouvre avec un prompt système contextualisé au template (nom + liste exacte des champs + règles de cohérence) qui force l'IA à terminer chaque réponse par un bloc JSON `<values>{...}</values>`. L'utilisateur dialogue jusqu'à être satisfait puis clique "Appliquer et créer la page" → la page est créée en 2 étapes (POST coquille + PUT values) et navigation vers l'édition.
###### b6.1 — Flux côté `page-create` ✅
- [x] Bouton "✨ Créer avec l'IA" ajouté entre "Annuler" et "Créer la page". Désactivé tant que titre + template + dossier ne sont pas renseignés (même `canSubmit` que le bouton classique).
- [x] Au clic : le drawer s'ouvre (`chatOpen = true`) avec un `welcomeMessage` contextualisé : *"Super, on va créer une page 'PNJ' ! Décrivez-la-moi en quelques mots…"* (généré dynamiquement à partir du nom du template choisi).
- [x] Création en 2 étapes (POST puis PUT) pour appliquer les `values` — le backend `POST /api/pages` n'accepte pas encore `values` en payload. Choix pragmatique (zéro modification backend) documenté dans le code.
- [x] Erreurs gérées : *"L'assistant n'a pas encore répondu"*, *"Impossible d'extraire les valeurs"*, *"Page créée mais impossible d'appliquer les valeurs"*. Affichées en banner rouge sous le formulaire.
###### b6.2 — Parsing JSON de la réponse assistant ✅
- [x] Le system prompt wizard (construit côté Angular dans `page-create.component.ts` getter `wizardSystemPrompt`) demande à l'IA de terminer CHAQUE réponse par un bloc `<values>{...}</values>` avec les clés exactes du template.
- [x] Parsing côté Angular : regex `/<values>\s*([\s\S]*?)\s*<\/values>/i` + `JSON.parse` avec try/catch + coercion des valeurs non-string en string.
- [x] Chaque fin de réponse assistant alimente `lastWizardReply` via le nouveau `@Output() assistantReply` du drawer.
###### b6.3 — Évolutions du drawer (réutilisable) ✅
- [x] Nouveau `@Input() systemPromptAddon: string | null` — injecté comme message `role: 'system'` **invisible côté UI** en tête du payload envoyé au backend à chaque tour. Permet au parent de contextualiser la conversation sans polluer l'historique visuel.
- [x] Nouveau `@Output() assistantReply = EventEmitter<string>()` — émis à chaque complétion d'un message assistant. Le parent l'utilise pour extraire le bloc `<values>` du wizard.
- [x] Le composant reste **unique** (pas de composant "wizard" séparé) : la config se fait entièrement via inputs/outputs. SRP respecté (le drawer ne connaît rien du wizard, juste du chat).
##### Étape b7 — Anti-hallucination ✅ (20 avril 2026, nuit)
> ✅ **Nuance centrale encodée** : l'IA peut (et doit) inventer des éléments originaux, mais ne peut pas faire référence à des éléments du Lore comme s'ils existaient si on ne les lui a pas montrés. Appliqué via température abaissée + system prompts durcis.
###### b7.1 — Température configurable par use case ✅
- [x] `LLMProvider.generate()` et `LLMChatProvider.stream_chat()` enrichis d'un kwarg `temperature: float | None = None` dans `brain/app/domain/ports.py` (docstring explicite la recommandation LoreMind).
- [x] `OllamaLLMProvider` propage via `options.temperature` dans les payloads `/api/generate` et `/api/chat` (la clé `options` est la convention Ollama pour les hyperparamètres).
- [x] `GeneratePageUseCase` : constante `_DEFAULT_TEMPERATURE = 0.4` (remplissage factuel, peu créatif).
- [x] `ChatUseCase` : constante `_DEFAULT_TEMPERATURE = 0.7` (conversation créative mais cohérente).
###### b7.2 — System prompts durcis ✅
- [x] `ChatUseCase._BASE_SYSTEM` : section "Règles de cohérence (IMPORTANT)" ajoutée avec la nuance centrale (✅ inventer OK, ❌ référencer l'inexistant KO, ❌ dates/chiffres précis inventés).
- [x] `GeneratePageUseCase._SYSTEM_INSTRUCTIONS` : même section, adaptée au remplissage factuel. Invite explicitement à rester vague (*"il y a longtemps"*, *"un bourg voisin"*) quand une précision externe manque plutôt que d'inventer.
###### b7.3 — DTO `temperature` optionnel — REPORTÉ au backlog
- [ ] Exposition du paramètre dans les DTOs Pydantic `/generate-page` et `/chat/stream` (override depuis Java/front). **Non fait volontairement** — YAGNI tant que personne ne demande l'override. Les constantes des use cases suffisent. À ajouter quand un slider "créativité" apparaîtra côté UI (backlog b4.6).
##### Étape b8 — Contextualisation page courante (serveur) ✅ (20 avril 2026, nuit)
> ✅ **PageContext injecté côté backend**. Sur `page-edit`, le drawer transmet le `pageId` → le Core charge la Page + son Template et construit un `PageContext` (titre, template, champs, valeurs actuelles) → envoyé en `page_context` au Brain Python → injecté dans le system prompt comme bloc "PAGE EN COURS D'ÉDITION" avec instruction de focalisation exclusive. L'IA ne déborde plus sur d'autres pages/templates.
###### b8.1 — Brain Python ✅
- [x] Nouveau dataclass `PageContext` dans `domain/models.py` (title, template_name, template_fields, values).
- [x] `ChatUseCase.stream()` accepte un `page_context: PageContext | None = None` optionnel. Rétro-compat totale (sans argument = comportement b5).
- [x] `_build_system_prompt` ajoute un bloc "--- PAGE EN COURS D'ÉDITION ---" quand `page_context` est fourni, listant titre + template + champs + valeurs actuelles + instruction de focalisation exclusive.
- [x] DTO Pydantic `PageContextDTO` + champ optionnel `page_context: PageContextDTO | None = None` sur `ChatStreamRequestDTO`. Mapping DTO → domain dans `main.py`.
###### b8.2 — Core Java ✅
- [x] Nouveau value object `PageContext` dans `domain/generationcontext/` (parallèle architectural de `LoreStructuralContext`). Lombok @Value/@Builder.
- [x] `ChatRequest` enrichi d'un champ `pageContext` nullable (JavaDoc explicite le "null = chat générique").
- [x] `StreamChatForLoreUseCase.execute()` prend désormais un `pageId` nullable. Nouvelle méthode privée `buildPageContext(pageId)` charge Page + Template via les repos existants (zéro nouveau port). Gestion des pages orphelines (template absent → PageContext minimal sans champs, pas d'exception).
- [x] `BrainAiChatClient.toPayload()` sérialise `page_context` en snake_case uniquement si fourni (payload léger par défaut).
- [x] DTO `ChatStreamRequestDTO` gagne un `pageId` optionnel + `AiChatController` le propage au use case.
###### b8.3 — Angular ✅
- [x] `AiChatService.streamChat(loreId, messages, pageId?)` : nouveau 3ᵉ argument optionnel, inclus dans le payload JSON uniquement s'il est truthy.
- [x] `AiChatDrawerComponent` gagne un `@Input() pageId: string | null = null` propagé au service.
- [x] `page-edit.component.html` passe `[pageId]="pageId"` au drawer — le composant avait déjà cet ID (depuis la route).
- [x] `page-create` (wizard b6) **volontairement** ne passe pas de `pageId` : la page n'existe pas encore. Le wizard continue de fonctionner via son `systemPromptAddon` sans aucune modification — les deux mécanismes cohabitent proprement.
###### b7.4 — Fiche academy `anti-hallucination.md` ✅
- [x] Théorie : pourquoi les LLM hallucinent (prédiction probabiliste sans vérification).
- [x] Analogie JDR : le "MJ qui improvise" — improvisation créative (OK, nouveau PNJ) vs improvisation incohérente (KO, référence à un PNJ inexistant comme session 2).
- [x] Les 5 leviers détaillés (température, prompt strict, contexte riche, modèle plus gros, chain-of-thought) avec tableau coût/effet.
- [x] Application concrète à LoreMind : ce qu'on a retenu (1+2+3), ce qu'on garde en backlog (4+5).
- [x] Section "Le savais-tu ?" sur le fait que `temperature=0` n'est pas 100% déterministe (parallélisme GPU).
- [x] Quiz 5 QCM.
##### Dette technique Brain (non bloquante, à reprendre plus tard)
- [ ] **Client `httpx` réutilisé** via FastAPI `lifespan` — actuellement un nouveau client est créé à chaque requête dans `OllamaLLMProvider.generate`. Impact : pool de connexions perdu entre requêtes. À corriger quand le débit augmente.
- [ ] **Tests pytest** : créer un `FakeLLMProvider` et tester `GeneratePageUseCase` en isolation. L'hexagonal a été mis en place précisément pour ça — il serait dommage de ne pas en tirer parti.
- [ ] **Logging structuré** (`loguru` ou `logging` standard avec `JsonFormatter`) à la place des prints implicites pour faciliter le debug en conditions réelles.
- [ ] **Endpoint `GET /info`** exposant le modèle actuellement configuré (utile pour diagnostiquer "ce que voit le serveur" sans SSHer dans le container).
- [ ] **Validation Pydantic** plus stricte : `max_length` sur `prompt`, `max_items` sur `template_fields` (ex: 20 max), longueur du `page_title`.
- [ ] **Gestion `output_format` autres que `"json"`** : aujourd'hui on passe la valeur brute à Ollama. Si le Brain doit supporter un adapter qui ne comprend que certains formats, valider côté port.
## Structure des dossiers
@@ -285,6 +478,57 @@ Ces points sont à garder en tête pour de futures refactorisations. Pas bloquan
- **Gestion des migrations DB** — actuellement `spring.jpa.hibernate.ddl-auto=update` (auto-alter). Acceptable en dev, **inutilisable en prod** (perte de données possible). À remplacer par Flyway ou Liquibase avant la mise en prod (chaque changement de schéma devra être versionné en fichier SQL).
## Dernière mise à jour
20 avril 2026 (nuit, session 2) — **Phase 3 étape b8 bouclée : contextualisation page courante injectée côté serveur**.
**PageContext serveur (b8.1 → b8.3)** :
- **Python** : nouveau dataclass `PageContext`, `ChatUseCase.stream()` accepte un param optionnel, system prompt gagne un bloc "PAGE EN COURS D'ÉDITION" avec instruction de focalisation exclusive. DTO Pydantic ajouté.
- **Java** : value object `PageContext` symétrique au `LoreStructuralContext`. `StreamChatForLoreUseCase` accepte un `pageId` nullable, charge Page + Template via les ports existants (zéro nouveau port). `BrainAiChatClient` sérialise en snake_case. DTO + controller propagent.
- **Angular** : service + drawer enrichis d'un `pageId?` optionnel. `page-edit` le passe au drawer. Le wizard de `page-create` ne le passe PAS (page inexistante) — les deux mécanismes (systemPromptAddon pour wizard, pageId pour édition) cohabitent proprement.
- **Résout** le bug *"l'IA propose des idées pour d'autres templates"* : maintenant l'IA reçoit explicitement le template + ses champs + les valeurs actuelles de la page éditée, avec injonction de ne pas déborder.
20 avril 2026 (nuit) — **Phase 3 étapes b6 + b7 bouclées : wizard création de page + anti-hallucination**.
**Wizard création de page (b6.1 → b6.3)** :
- **page-create** : bouton "✨ Créer avec l'IA" à côté du "Créer la page" classique. Au clic, drawer chat en mode wizard avec system prompt contextualisé au template choisi (nom + champs exacts + règle du bloc `<values>` obligatoire en fin de réponse).
- **Drawer enrichi** : nouveau `@Input() systemPromptAddon` (message `role:'system'` invisible préfixé au payload à chaque tour) + `@Output() assistantReply` (émis à chaque complétion assistant, alimente le parsing du wizard).
- **Parsing `<values>`** : regex + JSON.parse côté Angular, fallback gracieux sur erreur avec messages explicites.
- **Création en 2 étapes** (POST coquille + PUT values) — choix pragmatique : zéro modification backend nécessaire.
**Anti-hallucination (b7.1 → b7.4)** :
- **Température différenciée par use case** : `0.4` pour le one-shot factuel, `0.7` pour le chat créatif. Paramètre `temperature` ajouté aux ports `LLMProvider` + `LLMChatProvider`, propagé via `options.temperature` dans les payloads Ollama.
- **System prompts durcis** avec la nuance clé (✅ inventer des éléments originaux = OK, ❌ référencer comme existant un élément absent de la carte = KO). Prompt wizard côté Angular + prompts chat/one-shot côté Python Brain.
- **Academy** : fiche `docs/academy/anti-hallucination.md` avec analogie JDR (le MJ qui improvise bien vs mal), 5 leviers classés coût/effet, application concrète à LoreMind, quiz 5 QCM.
- DTO `temperature` optionnel **reporté au backlog** (YAGNI tant qu'aucune UI d'override n'existe).
19 avril 2026 (soir, session 3) — **Phase 3 étape b5 bouclée : chat IA conversationnel streamé + Structural Context**.
**Chat IA conversationnel (b5.1 → b5.6)** :
- **Python (b5.1)** : endpoint `POST /chat/stream` (SSE), port `LLMChatProvider` (ISP), use case `ChatUseCase` avec injection du Structural Context dans le system prompt.
- **Java domaine/adapter (b5.2)** : port `AiChatProvider` par **callbacks** (choix pédagogique pour éviter Reactor dans le domaine), adapter `BrainAiChatClient` via `WebClient` (ajout de `spring-boot-starter-webflux` au `pom.xml` + `spring.main.web-application-type=servlet` pour rester en Tomcat).
- **Java REST (b5.3)** : `AiChatController` expose `POST /api/ai/chat/stream`, streaming dans `AsyncTaskExecutor`, `SseEmitter` thread-safe, use case `StreamChatForLoreUseCase` qui charge Lore+nodes+pages+templates pour construire le Structural Context.
- **Angular (b5.4)** : service `AiChatService` avec `fetch()` + `ReadableStream` (pas `EventSource` qui ne supporte que GET), composant standalone réutilisable `AiChatDrawerComponent` avec bulles user/assistant, typing indicator, caret clignotant, suggestions rapides, `primaryAction` optionnelle.
- **Intégration page-edit (b5.5)** : bouton "Assistant IA" toggle le drawer, one-shot b4 relocalisé en `primaryAction` ("Remplir automatiquement"). Suggestions rapides hardcodées MVP.
- **Academy (b5.6)** : fiche `docs/academy/streaming-sse-rag.md` avec analogie JDR (pigeon voyageur), comparaison Full-dump/Structural/RAG sémantique, code des 3 étages, quiz 5 QCM.
- **Restera à étendre (b5.7)** : Campagne (asymétrique), page-create en mode wizard, éventuelle persistance de conversations.
19 avril 2026 (soir, session 2) — **Phase 3 étape b4 bouclée : chaîne IA de bout en bout opérationnelle**.
**Branchement Core Java ↔ Brain Python (b4.1 → b4.5)** :
- **Domaine** (b4.1) : 3ᵉ Bounded Context `generationcontext` créé (`GenerationContext`, `GenerationResult`, port `AiProvider`, `AiProviderException`). Zéro dépendance technique.
- **Adapter HTTP** (b4.2) : `BrainAiClient` + DTOs package-private snake_case + `RestTemplateConfig` (timeout 120s). Choix assumé de `RestTemplate` plutôt que `WebClient` pour la simplicité de lecture. Config `brain.base-url` + `brain.timeout-seconds` dans `application.properties`.
- **Use case** (b4.3) : `GeneratePageValuesUseCase` orchestre LoreContext (chargement Page/Template/Lore/LoreNode) et GenerationContext (appel IA). 5 ports injectés, 0 Adapter référencé. Zéro persistance assumée.
- **REST** (b4.4) : `POST /api/pages/{id}/generate` dans un `PageGenerationController` dédié (SRP vs colocation dans `PageController`). Gestion d'erreurs : 200/404/422/502/500.
- **Frontend** (b4.5) : `PageService.generateValues()` + état `aiLoading`/`aiError` dans `page-edit` + merge soft (écrase sur suggestion non-vide, préserve sur vide) + banner d'erreur dismissable.
- **Academy** : nouvelle fiche `docs/academy/bounded-context.md` avec analogie JDR (3 mondes de règles), bénéfices, exemple appliqué et quiz 5 QCM.
19 avril 2026 (soir) — **Phase 3 démarrée : Brain Python opérationnel jusqu'à b3.2**.
**Brain LoreMind (`brain/`)** :
- Squelette FastAPI + Swagger `/docs` (étape b1).
- Architecture hexagonale complète (étape b2) : Port `LLMProvider` (Protocol PEP 544), adapter `OllamaLLMProvider`, controller fin, injection par `Depends`. Fiche academy `docs/academy/hexagonal-python.md` rédigée.
- Génération structurée en cours (étape b3) : modèles de domaine `PageGenerationContext` / `PageGenerationResult`, port enrichi d'un kwarg `output_format`, use case `GeneratePageUseCase` avec prompt système français + parsing JSON défensif. Reste l'endpoint HTTP `POST /generate-page` (b3.3) pour exposer le use case.
- Validation manuelle : `gemma4:e2b` répond en ~2s sur un prompt simple. Le modèle est swappable via `.env` (`LLM_MODEL`).
19 avril 2026 - **Phase 5C en cours : compteurs + breadcrumb livrés**.
**Breadcrumb (fil d'Ariane) dans `page-edit`** :