Files
LoreMind/brain/app/api/deps.py
IETM_FIXE\ietm6 7f519588b6
All checks were successful
Build & Push Images / build (brain) (push) Successful in 1m44s
Build & Push Images / build (core) (push) Successful in 2m0s
Build & Push Images / build-switcher (push) Successful in 23s
Build & Push Images / build (web) (push) Successful in 1m50s
Amélioration de l'exploitation des PDF par l'IA
Amélioration des feedbacks en cas d'erreur d'exploitation des PDF
2026-06-12 01:28:45 +02:00

192 lines
8.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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.
"""
import logging
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
logger = logging.getLogger(__name__)
# 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 _effective_import_chunk_tokens(settings: Settings) -> int:
"""Taille de morceau réellement utilisable pour l'import.
Avec Ollama, le morceau (entrée) ET sa réécriture en sections (sortie ≈ même
taille) doivent tenir ensemble dans `num_ctx` — sinon Ollama remplit la fenêtre
avec le prompt et la génération s'arrête après quelques tokens (JSON coupé net,
morceau perdu). Budget : entrée×~1.3 (les morceaux sont mesurés en tokens
cl100k, plus compacts que les tokenizers locaux) + consignes + sortie×~1.4
≤ num_ctx → morceau ≤ (num_ctx 800) / 2.7. On plafonne, avec un log pour
rester transparent. Les providers cloud (gros contexte) ne sont pas plafonnés.
"""
requested = settings.import_chunk_tokens
if settings.llm_provider != "ollama":
return requested
cap = max(1000, int((settings.llm_num_ctx - 800) / 2.7))
if requested > cap:
logger.warning(
"Taille de morceau d'import réduite de %s à %s tokens : avec num_ctx=%s, "
"un morceau plus gros ne laisserait pas la place à la sortie du modèle "
"(génération coupée). Augmentez num_ctx pour utiliser de plus gros morceaux.",
requested, cap, settings.llm_num_ctx,
)
return cap
return requested
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)."""
# Modèle LOCAL → mode segmentation : le LLM ne renvoie que les frontières des
# sections (~200 tokens) et le texte original est découpé localement. Réécrire
# tout le contenu à ~100 tokens/s prendrait des dizaines de minutes par livre.
# Les providers cloud (rapides, grand contexte) gardent la réécriture nettoyée.
return ImportRulesUseCase(
llm=llm, extractor=_PDF_EXTRACTOR,
chunk_target_tokens=_effective_import_chunk_tokens(settings),
segment_only=settings.llm_provider == "ollama")
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=_effective_import_chunk_tokens(settings),
map_concurrency=settings.llm_map_concurrency,
)
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)],
settings: Annotated[Settings, Depends(get_settings)],
) -> NotebookChatUseCase:
return NotebookChatUseCase(
rag=rag, llm=llm, rerank_enabled=settings.rag_rerank) # type: ignore[arg-type]
def get_notebook_deep_use_case(
llm: Annotated[LLMProvider, Depends(get_llm_provider)],
embedder: Annotated[object, Depends(get_embedding_provider)],
settings: Annotated[Settings, Depends(get_settings)],
) -> NotebookDeepUseCase:
return NotebookDeepUseCase(
llm=llm,
batch_tokens=settings.import_chunk_tokens,
map_concurrency=settings.llm_map_concurrency,
embedder=embedder,
summary_filter=settings.deep_summary_filter,
)