Files
LoreMind/brain/app/api/deps.py
IETM_FIXE\ietm6 49a94bb73e Refactor SRP : decoupage de main.py (Brain) en routers et du SettingsComponent (web)
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>
2026-06-10 14:41:43 +02:00

144 lines
6.1 KiB
Python

"""Factories d'injection de dépendance — le point d'inversion de l'hexagone.
C'est ICI (et seulement ici) qu'on choisit QUEL adapter concret incarne chaque
port (LLM, embeddings, extracteur PDF), en fonction des Settings — modifiables
à chaud depuis l'écran Paramètres de l'UI. Les routers ne connaissent que les
ports et les use cases, jamais Ollama/Mistral/etc.
"""
from typing import Annotated
from fastapi import Depends, HTTPException
from app.application.adapt_campaign import AdaptCampaignUseCase
from app.application.chat import ChatUseCase
from app.application.embeddings import EmbeddingError
from app.application.generate_page import GeneratePageUseCase
from app.application.import_campaign import ImportCampaignUseCase
from app.application.import_rules import ImportRulesUseCase
from app.application.notebook_chat import NotebookChatUseCase
from app.application.notebook_deep import NotebookDeepUseCase
from app.application.notebook_rag import NotebookRagUseCase
from app.core.config import Settings, get_settings
from app.domain.ports import LLMProvider, LLMProviderError
from app.infrastructure.gemini_adapter import GeminiLLMProvider
from app.infrastructure.mistral_adapter import MistralLLMProvider
from app.infrastructure.mistral_embedding_adapter import MistralEmbeddingProvider
from app.infrastructure.ollama_adapter import OllamaLLMProvider
from app.infrastructure.ollama_embedding_adapter import OllamaEmbeddingProvider
from app.infrastructure.onemin_adapter import OneMinAiLLMProvider
from app.infrastructure.openrouter_adapter import OpenRouterLLMProvider
from app.infrastructure.pdf_extractor import PyMuPdfTextExtractor
# Extracteur PDF partagé : la détection OCR (version Tesseract) a un coût
# (subprocess) qu'on ne veut pas payer à chaque requête → singleton module.
_PDF_EXTRACTOR = PyMuPdfTextExtractor()
def get_llm_provider(
settings: Annotated[Settings, Depends(get_settings)],
) -> LLMProvider:
"""Factory d'adapter — point d'inversion de dépendance.
C'est ici (et uniquement ici) qu'on choisit QUEL adapter concret
incarne le port, en fonction du champ `llm_provider` des Settings
(modifiable a chaud depuis l'ecran Parametres de l'UI).
"""
try:
if settings.llm_provider == "onemin":
return OneMinAiLLMProvider(settings)
if settings.llm_provider == "openrouter":
return OpenRouterLLMProvider(settings)
if settings.llm_provider == "mistral":
return MistralLLMProvider(settings)
if settings.llm_provider == "gemini":
return GeminiLLMProvider(settings)
return OllamaLLMProvider(settings)
except LLMProviderError as exc:
# Ex : cle 1min.ai manquante. On renvoie du 400 plutot que du 500
# pour que le frontend puisse afficher un message actionnable.
raise HTTPException(status_code=400, detail=str(exc)) from exc
def get_generate_page_use_case(
llm: Annotated[LLMProvider, Depends(get_llm_provider)],
) -> GeneratePageUseCase:
"""Factory du use case — injecte le port LLMProvider sans connaître l'adapter."""
return GeneratePageUseCase(llm=llm)
def get_chat_use_case(
llm: Annotated[LLMProvider, Depends(get_llm_provider)],
) -> ChatUseCase:
"""Factory du use case chat.
L'adapter OllamaLLMProvider satisfait les deux protocoles (LLMProvider
et LLMChatProvider) par duck typing ; on lui passe la même instance.
"""
return ChatUseCase(llm=llm) # type: ignore[arg-type]
def get_import_rules_use_case(
llm: Annotated[LLMProvider, Depends(get_llm_provider)],
settings: Annotated[Settings, Depends(get_settings)],
) -> ImportRulesUseCase:
"""Factory du use case d'import de règles PDF (extraction + structuration)."""
return ImportRulesUseCase(
llm=llm, extractor=_PDF_EXTRACTOR, chunk_target_tokens=settings.import_chunk_tokens)
def get_import_campaign_use_case(
llm: Annotated[LLMProvider, Depends(get_llm_provider)],
settings: Annotated[Settings, Depends(get_settings)],
) -> ImportCampaignUseCase:
"""Factory du use case d'import de campagne PDF (extraction + arborescence)."""
return ImportCampaignUseCase(
llm=llm, extractor=_PDF_EXTRACTOR, chunk_target_tokens=settings.import_chunk_tokens)
def get_adapt_campaign_use_case(
llm: Annotated[LLMProvider, Depends(get_llm_provider)],
settings: Annotated[Settings, Depends(get_settings)],
) -> AdaptCampaignUseCase:
"""Factory du use case d'adaptation d'un PDF à une campagne (conseils streamés)."""
# L'adapter satisfait aussi LLMChatProvider (stream_chat) par duck typing.
# Budget d'entrée = taille de morceau configurée (qui passe déjà côté provider).
return AdaptCampaignUseCase( # type: ignore[arg-type]
llm=llm, extractor=_PDF_EXTRACTOR, max_input_tokens=settings.import_chunk_tokens)
def get_embedding_provider(
settings: Annotated[Settings, Depends(get_settings)],
):
"""Factory de l'adapter d'embeddings (RAG) selon `embedding_provider`."""
try:
if settings.embedding_provider == "mistral":
return MistralEmbeddingProvider(settings)
return OllamaEmbeddingProvider(settings)
except EmbeddingError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
def get_notebook_rag_use_case(
embedder: Annotated[object, Depends(get_embedding_provider)],
settings: Annotated[Settings, Depends(get_settings)],
) -> NotebookRagUseCase:
return NotebookRagUseCase(
extractor=_PDF_EXTRACTOR,
embedder=embedder, # type: ignore[arg-type]
min_score=settings.rag_min_score,
)
def get_notebook_chat_use_case(
llm: Annotated[LLMProvider, Depends(get_llm_provider)],
rag: Annotated[NotebookRagUseCase, Depends(get_notebook_rag_use_case)],
) -> NotebookChatUseCase:
return NotebookChatUseCase(rag=rag, llm=llm) # type: ignore[arg-type]
def get_notebook_deep_use_case(
llm: Annotated[LLMProvider, Depends(get_llm_provider)],
settings: Annotated[Settings, Depends(get_settings)],
) -> NotebookDeepUseCase:
return NotebookDeepUseCase(llm=llm, batch_tokens=settings.import_chunk_tokens)