Mise en ligne de la version 0.2.0
This commit is contained in:
0
brain/app/domain/__init__.py
Normal file
0
brain/app/domain/__init__.py
Normal file
186
brain/app/domain/models.py
Normal file
186
brain/app/domain/models.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""Modèles de domaine pour le cas d'usage de génération de page LoreMind.
|
||||
|
||||
On utilise @dataclass (pas Pydantic) pour garder le domaine exempt de toute
|
||||
dépendance framework. Pydantic apparaît uniquement aux frontières : DTOs HTTP
|
||||
dans `main.py`, Settings dans `core/config.py`.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PageGenerationContext:
|
||||
"""Contexte métier à fournir au LLM pour générer une page LoreMind.
|
||||
|
||||
Les champs correspondent aux entités du Lore Context côté Core Java :
|
||||
- lore_* : l'univers (Lore)
|
||||
- folder_name : le dossier (LoreNode) qui catégorise la page
|
||||
- template_* : le gabarit qui liste les champs à remplir
|
||||
- page_title : le titre de la page à créer
|
||||
"""
|
||||
|
||||
lore_name: str
|
||||
folder_name: str
|
||||
template_name: str
|
||||
template_fields: list[str]
|
||||
page_title: str
|
||||
lore_description: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PageGenerationResult:
|
||||
"""Résultat métier : une valeur textuelle générée par champ du template.
|
||||
|
||||
La clé du dict est le nom du champ (ex: "apparence"), la valeur est
|
||||
le contenu généré par le LLM. Cohérent avec la structure
|
||||
`Page.values: Map<String,String>` côté Core Java.
|
||||
"""
|
||||
|
||||
values: dict[str, str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChatMessage:
|
||||
"""Message d'une conversation — rôle + contenu textuel.
|
||||
|
||||
Rôles possibles (OpenAI/Ollama compatibles) :
|
||||
- "system" : prompt système (contexte, instructions)
|
||||
- "user" : message de l'utilisateur
|
||||
- "assistant" : réponse précédente du LLM
|
||||
"""
|
||||
|
||||
role: str
|
||||
content: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PageSummary:
|
||||
"""Résumé enrichi d'une page du Lore, projeté pour alimenter le prompt.
|
||||
|
||||
Depuis b9 : on ne se contente plus du nom + template, on embarque aussi
|
||||
les valeurs des champs dynamiques (tronquées côté Core Java à 500 car.),
|
||||
les tags, et les titres des pages liées (les IDs techniques sont déjà
|
||||
résolus en titres lisibles côté Java — voir LoreStructuralContextBuilder).
|
||||
|
||||
Les notes privées du MJ restent volontairement absentes ici (confinées
|
||||
à leur page d'édition via PageContext quand l'utilisateur y travaille).
|
||||
"""
|
||||
|
||||
title: str
|
||||
template_name: str
|
||||
values: dict[str, str]
|
||||
tags: list[str]
|
||||
related_page_titles: list[str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LoreStructuralContext:
|
||||
"""Carte structurelle enrichie d'un Lore pour nourrir l'IA.
|
||||
|
||||
Depuis b9 : chaque page expose son contenu (values, tags, liens) via
|
||||
PageSummary. Le prompt n'est plus qu'une table des matières — c'est
|
||||
une encyclopédie condensée que le LLM peut directement citer.
|
||||
|
||||
Le dict `folders` est indexé par nom de dossier et mappe vers la liste
|
||||
des pages qu'il contient (PageSummary).
|
||||
"""
|
||||
|
||||
lore_name: str
|
||||
lore_description: str | None
|
||||
folders: dict[str, list[PageSummary]]
|
||||
tags: list[str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PageContext:
|
||||
"""Contexte d'une page spécifique en cours d'édition.
|
||||
|
||||
Injecté dans le system prompt pour focaliser le chat sur CETTE page
|
||||
précise : son template, ses champs, ses valeurs actuelles. Permet à
|
||||
l'IA d'éviter de parler d'autres pages du Lore par mégarde.
|
||||
|
||||
Complémentaire de `LoreStructuralContext` : l'un donne la carte
|
||||
générale (toutes les pages existantes), l'autre zoome sur la page
|
||||
en cours de discussion.
|
||||
"""
|
||||
|
||||
title: str
|
||||
template_name: str
|
||||
template_fields: list[str]
|
||||
values: dict[str, str]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SceneBranchHint:
|
||||
"""Indice d'une branche narrative vers une autre scène du même chapitre.
|
||||
|
||||
Le Core Java résout déjà `targetSceneId` en nom humain avant l'envoi :
|
||||
l'IA ne voit donc jamais d'UUID, seulement des noms qu'elle peut citer.
|
||||
"""
|
||||
|
||||
label: str
|
||||
target_scene_name: str
|
||||
condition: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SceneSummary:
|
||||
"""Résumé d'une scène : nom + description courte + illustrations + branches."""
|
||||
|
||||
name: str
|
||||
description: str | None
|
||||
# Depuis l'etape 6 : permet a l'IA de savoir qu'une scene a des illustrations
|
||||
# attachees. 0 par defaut pour retrocompat si le Core n'envoie rien.
|
||||
illustration_count: int = 0
|
||||
# Connexions narratives sortantes (livre dont vous etes le heros).
|
||||
branches: list[SceneBranchHint] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChapterSummary:
|
||||
"""Résumé d'un chapitre : nom + description courte + ses scènes."""
|
||||
|
||||
name: str
|
||||
description: str | None
|
||||
scenes: list[SceneSummary]
|
||||
illustration_count: int = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ArcSummary:
|
||||
"""Résumé d'un arc narratif : nom + description courte + ses chapitres."""
|
||||
|
||||
name: str
|
||||
description: str | None
|
||||
chapters: list[ChapterSummary]
|
||||
illustration_count: int = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CampaignStructuralContext:
|
||||
"""Carte narrative enrichie d'une Campagne pour nourrir l'IA.
|
||||
|
||||
Jumeau de LoreStructuralContext côté Campaign. On décrit l'arbre
|
||||
arcs → chapitres → scènes en donnant le NOM + une DESCRIPTION courte
|
||||
(synopsis) à chaque niveau. Les champs longs (notes MJ, narration
|
||||
joueur, combat) restent réservés à l'entité focus via
|
||||
NarrativeEntityContext. Ordre narratif préservé dans la liste `arcs`.
|
||||
"""
|
||||
|
||||
campaign_name: str
|
||||
campaign_description: str | None
|
||||
arcs: list[ArcSummary]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NarrativeEntityContext:
|
||||
"""Contexte d'une entité narrative précise en cours d'édition.
|
||||
|
||||
Équivalent de PageContext côté Campaign. Focalise l'IA sur un Arc,
|
||||
Chapter ou Scene en particulier. `entity_type` ∈ {"arc","chapter","scene"}.
|
||||
Les `fields` sont une map ordonnée nomChamp → valeurActuelle (chaîne
|
||||
vide si non renseigné).
|
||||
"""
|
||||
|
||||
entity_type: str
|
||||
title: str
|
||||
fields: dict[str, str]
|
||||
86
brain/app/domain/ports.py
Normal file
86
brain/app/domain/ports.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Ports (contrats) du domaine du Brain LoreMind.
|
||||
|
||||
Un Port est une INTERFACE abstraite exposée par le domaine vers le monde
|
||||
extérieur. Le domaine définit CE QU'IL ATTEND, pas COMMENT c'est implémenté.
|
||||
|
||||
En Python moderne on privilégie Protocol (PEP 544) sur ABC pour bénéficier
|
||||
du duck typing structurel : toute classe qui possède les bonnes méthodes
|
||||
satisfait le contrat, sans héritage explicite.
|
||||
"""
|
||||
from typing import AsyncIterator, Protocol
|
||||
|
||||
|
||||
class LLMProvider(Protocol):
|
||||
"""Port sortant — contrat pour un fournisseur de modèle de langage.
|
||||
|
||||
Toute implémentation (Ollama, OpenAI, Claude, faux-mock de test) doit
|
||||
exposer au minimum cette méthode `generate`.
|
||||
"""
|
||||
|
||||
async def generate(
|
||||
self,
|
||||
prompt: str,
|
||||
*,
|
||||
output_format: str | None = None,
|
||||
temperature: float | None = None,
|
||||
) -> str:
|
||||
"""Génère une réponse textuelle à partir d'un prompt donné.
|
||||
|
||||
Args:
|
||||
prompt: le texte envoyé au modèle.
|
||||
output_format: contrainte de format optionnelle. Exemple : "json"
|
||||
pour forcer le modèle à renvoyer du JSON valide. Les
|
||||
fournisseurs qui ne supportent pas une valeur donnée doivent
|
||||
l'ignorer silencieusement ou la traduire au mieux.
|
||||
temperature: créativité du modèle, 0.0 (déterministe/factuel) à
|
||||
1.0+ (très créatif, hallucine plus facilement). None =
|
||||
valeur par défaut de l'adapter. Recommandation LoreMind :
|
||||
~0.4 pour du remplissage factuel, ~0.7 pour du chat créatif.
|
||||
|
||||
Raises:
|
||||
LLMProviderError: si le fournisseur sous-jacent a échoué.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class LLMChatProvider(Protocol):
|
||||
"""Port sortant — fournisseur de chat streamé (conversation multi-tours).
|
||||
|
||||
Distinct de LLMProvider par Interface Segregation Principle : le chat
|
||||
streamé est une capacité séparée (messages structurés, flux de tokens)
|
||||
qui mérite son propre contrat. Un même adapter concret (ex: Ollama)
|
||||
peut satisfaire les deux protocoles simultanément grâce au duck typing.
|
||||
"""
|
||||
|
||||
async def stream_chat(
|
||||
self,
|
||||
messages: list["ChatMessage"], # forward ref, évite import circulaire
|
||||
*,
|
||||
system_prompt: str | None = None,
|
||||
temperature: float | None = None,
|
||||
) -> AsyncIterator[str]:
|
||||
"""Streame la réponse du LLM token par token.
|
||||
|
||||
Args:
|
||||
messages: historique de la conversation (chronologique, le dernier
|
||||
message étant typiquement celui de l'utilisateur en attente
|
||||
de réponse).
|
||||
system_prompt: instructions système optionnelles (contexte global,
|
||||
règles de comportement). Prefixe la conversation si fourni.
|
||||
temperature: créativité du modèle (voir `LLMProvider.generate`).
|
||||
|
||||
Yields:
|
||||
Fragments de texte (tokens) au fur et à mesure de la génération.
|
||||
|
||||
Raises:
|
||||
LLMProviderError: si le fournisseur sous-jacent a échoué.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class LLMProviderError(Exception):
|
||||
"""Erreur du domaine signalant qu'un LLMProvider n'a pas pu générer.
|
||||
|
||||
Définie dans le domaine (pas dans l'infra) pour que les couches
|
||||
supérieures puissent l'attraper sans connaître l'adapter concret.
|
||||
"""
|
||||
Reference in New Issue
Block a user