A la premiere analyse d une source, chaque lot est resume (1 appel LLM, cache disque, purge avec la source) et son resume embedde. Aux questions suivantes, la question est comparee aux resumes et seuls les lots proches du meilleur score (marge 0.10, plancher 3 lots) sont relus -> 3-5x moins d appels sur un gros livre pour les questions ciblees. Selection volontairement conservatrice ; best-effort (tout echec -> plein scan) ; desactivable via DEEP_SUMMARY_FILTER=false (exhaustivite maximale). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
155 lines
6.3 KiB
Python
155 lines
6.3 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,
|
|
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)],
|
|
) -> NotebookChatUseCase:
|
|
return NotebookChatUseCase(rag=rag, llm=llm) # 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,
|
|
)
|