Claude commited on
Commit
5c1eff9
·
unverified ·
1 Parent(s): 8ab2b81

feat(sprint-C.1): découple runner.orchestration de OCRLLMPipeline

Browse files

Sprint C.1 du plan v2.0 — préparation à la suppression du sous-package
``picarones/pipelines/`` (Sprint D). Le runner legacy reposait sur un
``isinstance(engine, OCRLLMPipeline)`` pour ajouter les
``pipeline_steps`` et le ``prompt_template`` à la metadata du
benchmark. Cette dépendance directe du runner vers
``picarones.pipelines`` est remplacée par un mécanisme polymorphe.

Modifications
-------------
- ``adapters/legacy_engines/base.py::BaseOCREngine`` : nouvel attribut
de classe ``is_pipeline: bool = False`` — un OCR seul n'est pas un
pipeline composé.
- ``pipelines/base.py::OCRLLMPipeline`` :
- Surcharge ``is_pipeline = True`` (marqueur polymorphe).
- Expose deux properties publiques :
- ``pipeline_steps_info`` (substitut public à
``_build_steps_info()``).
- ``prompt_template`` (substitut public à
``_prompt_template``).
L'API privée historique reste disponible pour ne pas casser les
callers internes encore en place ; elle disparaîtra avec
``OCRLLMPipeline`` en Sprint D.
- ``measurements/runner/orchestration.py`` :
- ``isinstance(engine, OCRLLMPipeline)`` →
``getattr(engine, "is_pipeline", False)``.
- ``engine._build_steps_info()`` → ``engine.pipeline_steps_info``.
- ``engine._prompt_template`` → ``engine.prompt_template``.
- Suppression de l'import paresseux ``from picarones.pipelines.base
import OCRLLMPipeline`` dans le runner.

Conséquences
------------
- Le sous-package ``picarones/measurements/runner/`` ne dépend plus
de ``picarones/pipelines/``. Une suite de tests sans
``picarones.pipelines`` installé pourra utiliser le runner legacy
sur des OCR seuls.
- Le runner reste compatible avec ``OCRLLMPipeline`` (qui a
``is_pipeline=True`` et expose les properties publiques) — pas de
régression sur les benchmarks OCR+LLM.
- Sprint D pourra supprimer ``picarones/pipelines/`` en une seule
passe sans toucher au runner.

Callers restants de ``OCRLLMPipeline`` (sortie de session) :

- ``picarones/web/benchmark_utils.py:131`` : instancie directement
``OCRLLMPipeline``. Migration possible quand le runner accepte
des ``PipelineSpec`` directement (Sprint D — runner →
``BenchmarkService``).
- ``picarones/pipelines/_executor_runner.py`` : helper interne
Sprint B, supprimé avec la classe en Sprint D.
- Les tests Sprint 3 et Sprint 15 instancient ``OCRLLMPipeline``
directement — réécrits en Sprint D.

Bilan
-----
- ``pytest tests/`` : 4758 passed, 0 failed.
- ``ruff check`` : clean.
- 3 fichiers modifiés (~25 lignes).
- 1 import legacy supprimé du runner.

https://claude.ai/code/session_011XQZNitg1rCgia8ZD1a2hP

picarones/adapters/legacy_engines/base.py CHANGED
@@ -118,6 +118,13 @@ class BaseOCREngine(BaseModule):
118
  execution_mode: str = "io"
119
  """``"io"`` pour ThreadPoolExecutor (défaut), ``"cpu"`` pour ProcessPoolExecutor."""
120
 
 
 
 
 
 
 
 
121
  def __init__(self, config: Optional[dict] = None) -> None:
122
  self.config: dict = config or {}
123
  # Cache du dernier ``EngineResult`` produit par ``run()`` —
 
118
  execution_mode: str = "io"
119
  """``"io"`` pour ThreadPoolExecutor (défaut), ``"cpu"`` pour ProcessPoolExecutor."""
120
 
121
+ #: ``True`` ssi l'engine est un pipeline composé (OCR+LLM ou VLM).
122
+ #: Sprint C du plan v2.0 : remplace le check legacy
123
+ #: ``isinstance(engine, OCRLLMPipeline)`` par un attribut polymorphe.
124
+ #: Les sous-classes "pipeline composé" (``OCRLLMPipeline``, et tout
125
+ #: futur composite) surchargent à ``True``.
126
+ is_pipeline: bool = False
127
+
128
  def __init__(self, config: Optional[dict] = None) -> None:
129
  self.config: dict = config or {}
130
  # Cache du dernier ``EngineResult`` produit par ``run()`` —
picarones/measurements/runner/orchestration.py CHANGED
@@ -516,13 +516,13 @@ def _build_pipeline_info(engine: BaseOCREngine, doc_results: list[DocumentResult
516
  "llm_provider": meta.get("llm_provider"),
517
  }
518
 
519
- try:
520
- from picarones.pipelines.base import OCRLLMPipeline
521
- if isinstance(engine, OCRLLMPipeline):
522
- info["pipeline_steps"] = engine._build_steps_info()
523
- info["prompt_template"] = engine._prompt_template
524
- except ImportError:
525
- pass
526
 
527
  over_norm_results = [
528
  dr.pipeline_metadata.get("over_normalization")
 
516
  "llm_provider": meta.get("llm_provider"),
517
  }
518
 
519
+ # Sprint C du plan v2.0 : duck typing via ``is_pipeline`` au lieu
520
+ # de ``isinstance(engine, OCRLLMPipeline)``. Découple le runner
521
+ # legacy de la classe ``OCRLLMPipeline`` — préparation à la
522
+ # suppression du sous-package ``picarones.pipelines/`` (Sprint D).
523
+ if getattr(engine, "is_pipeline", False):
524
+ info["pipeline_steps"] = engine.pipeline_steps_info
525
+ info["prompt_template"] = engine.prompt_template
526
 
527
  over_norm_results = [
528
  dr.pipeline_metadata.get("over_normalization")
picarones/pipelines/base.py CHANGED
@@ -130,6 +130,12 @@ class OCRLLMPipeline(BaseOCREngine):
130
  # Interface BaseOCREngine
131
  # ------------------------------------------------------------------
132
 
 
 
 
 
 
 
133
  @property
134
  def name(self) -> str:
135
  return self._name
@@ -138,6 +144,21 @@ class OCRLLMPipeline(BaseOCREngine):
138
  ocr_v = self.ocr_engine._safe_version() if self.ocr_engine else "—"
139
  return f"ocr={ocr_v}; llm={self.llm_adapter.model}"
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  def _run_llm_step(
142
  self, image_path: Path, ocr_text: str,
143
  ) -> tuple[str, Optional[str]]:
 
130
  # Interface BaseOCREngine
131
  # ------------------------------------------------------------------
132
 
133
+ #: Sprint C du plan v2.0 : marqueur polymorphe que le runner
134
+ #: utilise pour ajouter ``pipeline_steps`` + ``prompt_template``
135
+ #: aux ``EngineReport.pipeline_info`` sans avoir à connaître le
136
+ #: type concret ``OCRLLMPipeline``.
137
+ is_pipeline: bool = True
138
+
139
  @property
140
  def name(self) -> str:
141
  return self._name
 
144
  ocr_v = self.ocr_engine._safe_version() if self.ocr_engine else "—"
145
  return f"ocr={ocr_v}; llm={self.llm_adapter.model}"
146
 
147
+ @property
148
+ def pipeline_steps_info(self) -> list[dict]:
149
+ """Description structurée des étapes (Sprint C — API publique).
150
+
151
+ Substitut public à ``_build_steps_info()`` pour les callers
152
+ externes (notamment le runner) qui ont besoin de connaître la
153
+ composition de la pipeline pour la metadata du rapport.
154
+ """
155
+ return self._build_steps_info()
156
+
157
+ @property
158
+ def prompt_template(self) -> str:
159
+ """Template de prompt courant (Sprint C — API publique)."""
160
+ return self._prompt_template
161
+
162
  def _run_llm_step(
163
  self, image_path: Path, ocr_text: str,
164
  ) -> tuple[str, Optional[str]]: