Brain : main.py (1496 l.) reduit a l'assemblage (~95 l.) ; un router par responsabilite (generation, chat, tables, imports, notebooks, settings, models), factories DI dans api/deps.py, DTOs chat + mapping anti-corruption separes, auto-pull embeddings deplace en infrastructure. Chemins HTTP inchanges. Web : SettingsComponent (729 l.) recentre sur le formulaire (~330 l.) ; sous-composants standalone updates-section (MAJ + licence Patreon + switch canal) et ollama-model-manager (liste/pull/suppression de modeles). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
216 lines
7.1 KiB
Python
216 lines
7.1 KiB
Python
"""DTOs Pydantic du chat contextuel — frontière HTTP avec le Core Java.
|
|
|
|
C'est ici (et seulement ici, avec les autres modules de `app.api`) qu'on
|
|
utilise Pydantic : le domaine ne voit que des dataclasses (voir chat_mapping).
|
|
"""
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class ChatMessageDTO(BaseModel):
|
|
"""Un message de la conversation. Rôles acceptés : user, assistant, system."""
|
|
|
|
role: str = Field(pattern="^(user|assistant|system)$")
|
|
content: str
|
|
|
|
|
|
class PageSummaryDTO(BaseModel):
|
|
"""Résumé enrichi d'une page : identité + contenu + interconnexions.
|
|
|
|
Depuis b9 : values/tags/related_page_titles sont optionnels côté JSON —
|
|
le Core Java ne les sérialise que s'ils sont non-vides (payload léger
|
|
pour un Lore avec beaucoup de pages vierges).
|
|
"""
|
|
|
|
title: str
|
|
template_name: str
|
|
values: dict[str, str] = Field(default_factory=dict)
|
|
tags: list[str] = Field(default_factory=list)
|
|
related_page_titles: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class LoreContextDTO(BaseModel):
|
|
"""Carte structurelle du Lore avec contenu des pages (b9+)."""
|
|
|
|
lore_name: str
|
|
lore_description: str | None = None
|
|
folders: dict[str, list[PageSummaryDTO]] = Field(default_factory=dict)
|
|
tags: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class PageContextDTO(BaseModel):
|
|
"""Contexte d'une page spécifique pour focaliser le chat (optionnel)."""
|
|
|
|
title: str
|
|
template_name: str
|
|
template_fields: list[str] = Field(default_factory=list)
|
|
values: dict[str, str] = Field(default_factory=dict)
|
|
|
|
|
|
class SceneBranchHintDTO(BaseModel):
|
|
"""Indice d'une branche narrative (le Core a deja resolu le nom cible)."""
|
|
|
|
label: str
|
|
target_scene_name: str
|
|
condition: str | None = None
|
|
|
|
|
|
class RoomBranchHintDTO(BaseModel):
|
|
"""Sortie d'une pièce vers une autre pièce du même lieu (donjon)."""
|
|
|
|
label: str
|
|
target_room_name: str
|
|
condition: str | None = None
|
|
|
|
|
|
class RoomSummaryDTO(BaseModel):
|
|
"""Pièce d'un lieu explorable. Omise par le Core si la scène est classique."""
|
|
|
|
name: str
|
|
floor: int | None = None
|
|
description: str | None = None
|
|
enemies: str | None = None
|
|
branches: list[RoomBranchHintDTO] = Field(default_factory=list)
|
|
|
|
|
|
class SceneSummaryDTO(BaseModel):
|
|
"""Résumé d'une scène : nom + description courte (synopsis)."""
|
|
|
|
name: str
|
|
description: str | None = None
|
|
# Optionnel : le Core Java ne serialise illustration_count QUE si > 0
|
|
# (payload plus leger). Defaut 0 = pas d'illustrations ou champ absent.
|
|
illustration_count: int = 0
|
|
# Branches narratives sortantes, omises cote Core si vides.
|
|
branches: list[SceneBranchHintDTO] = Field(default_factory=list)
|
|
# Pièces du lieu explorable, omises par Core si scène classique.
|
|
rooms: list[RoomSummaryDTO] = Field(default_factory=list)
|
|
|
|
|
|
class ChapterSummaryDTO(BaseModel):
|
|
"""Résumé d'un chapitre : nom + description courte + ses scènes."""
|
|
|
|
name: str
|
|
description: str | None = None
|
|
scenes: list[SceneSummaryDTO] = Field(default_factory=list)
|
|
illustration_count: int = 0
|
|
|
|
|
|
class ArcSummaryDTO(BaseModel):
|
|
"""Résumé d'un arc narratif : nom + description courte + ses chapitres."""
|
|
|
|
name: str
|
|
description: str | None = None
|
|
chapters: list[ChapterSummaryDTO] = Field(default_factory=list)
|
|
illustration_count: int = 0
|
|
|
|
|
|
class CharacterSummaryDTO(BaseModel):
|
|
"""Résumé d'un PJ : nom + snippet. Pas de fiche complète au niveau résumé."""
|
|
|
|
name: str
|
|
snippet: str = ""
|
|
|
|
|
|
class NpcSummaryDTO(BaseModel):
|
|
"""Résumé d'un PNJ : symétrique à CharacterSummaryDTO."""
|
|
|
|
name: str
|
|
snippet: str = ""
|
|
|
|
|
|
class CampaignContextDTO(BaseModel):
|
|
"""Carte narrative enrichie : arcs → chapitres → scènes avec synopsis."""
|
|
|
|
campaign_name: str
|
|
campaign_description: str | None = None
|
|
arcs: list[ArcSummaryDTO] = Field(default_factory=list)
|
|
characters: list[CharacterSummaryDTO] = Field(default_factory=list)
|
|
npcs: list[NpcSummaryDTO] = Field(default_factory=list)
|
|
|
|
|
|
class NarrativeEntityDTO(BaseModel):
|
|
"""Entité narrative (arc/chapter/scene/character) en cours d'édition — focus optionnel."""
|
|
|
|
entity_type: str = Field(pattern="^(arc|chapter|scene|character|npc)$")
|
|
title: str
|
|
fields: dict[str, str] = Field(default_factory=dict)
|
|
|
|
|
|
class GameSystemContextDTO(BaseModel):
|
|
"""Règles de JDR présélectionnées par le Core (filtrées par intent).
|
|
|
|
Les sections sont un dict titre_H2 → contenu_markdown. Peuvent être
|
|
vides si aucune section ne matchait l'intent de génération courant.
|
|
"""
|
|
|
|
system_name: str
|
|
system_description: str | None = None
|
|
sections: dict[str, str] = Field(default_factory=dict)
|
|
|
|
|
|
class JournalEntrySummaryDTO(BaseModel):
|
|
"""Une entrée du journal de session.
|
|
|
|
`source_session_name` est présent uniquement pour les évènements issus
|
|
des sessions précédentes — sert à ancrer temporellement dans le prompt.
|
|
"""
|
|
|
|
type: str
|
|
content: str
|
|
occurred_at: str | None = None
|
|
source_session_name: str | None = None
|
|
|
|
|
|
class QuestSummaryDTO(BaseModel):
|
|
"""Résumé d'une quête (Chapter dans un Arc HUB). Voir QuestSummary côté domaine."""
|
|
|
|
name: str
|
|
arc_name: str
|
|
description: str | None = None
|
|
|
|
|
|
class SessionContextDTO(BaseModel):
|
|
"""Contexte d'une Session de jeu en cours (Play Context).
|
|
|
|
Combine le journal complet (`entries`), les EVENTs des sessions précédentes
|
|
(`previous_events`), et — depuis l'ajout du mode Hub — l'état des quêtes
|
|
Hub de la campagne (disponibles / en cours / verrouillées) plus les flags
|
|
narratifs actuellement actifs.
|
|
"""
|
|
|
|
session_name: str
|
|
active: bool
|
|
started_at: str | None = None
|
|
entries: list[JournalEntrySummaryDTO] = Field(default_factory=list)
|
|
previous_events: list[JournalEntrySummaryDTO] = Field(default_factory=list)
|
|
available_quests: list[QuestSummaryDTO] = Field(default_factory=list)
|
|
in_progress_quests: list[QuestSummaryDTO] = Field(default_factory=list)
|
|
locked_quest_titles: list[str] = Field(default_factory=list)
|
|
active_flags: list[str] = Field(default_factory=list)
|
|
|
|
|
|
class ChatStreamRequestDTO(BaseModel):
|
|
"""Requête de chat streamé : historique + contextes structurels.
|
|
|
|
Les contextes (lore, page, campaign, narrative_entity, session) sont
|
|
optionnels, mais au moins l'un des contextes "racines" (lore_context,
|
|
campaign_context ou session_context) doit être fourni. Le validateur
|
|
`check_scope` applique cette règle à la frontière HTTP.
|
|
"""
|
|
|
|
messages: list[ChatMessageDTO] = Field(min_length=1)
|
|
lore_context: LoreContextDTO | None = None
|
|
page_context: PageContextDTO | None = None
|
|
campaign_context: CampaignContextDTO | None = None
|
|
narrative_entity: NarrativeEntityDTO | None = None
|
|
game_system_context: GameSystemContextDTO | None = None
|
|
session_context: SessionContextDTO | None = None
|
|
|
|
def has_scope(self) -> bool:
|
|
"""Vrai si au moins un contexte racine (Lore, Campagne ou Session) est fourni."""
|
|
return (
|
|
self.lore_context is not None
|
|
or self.campaign_context is not None
|
|
or self.session_context is not None
|
|
)
|