Spaces:
Sleeping
feat(adapters/vlm,planner): Sprint A14-S54 architecture cleanup (audit #6 + #14 + #22)
Browse files#6 - Garde-fou MRO BaseVLMAdapter
---------------------------------
Avant S54, lordre des parents dans
class AnthropicVLMAdapter(BaseVLMAdapter, AnthropicAdapter)
était critique mais non vérifié. Un swap accidentel
à (AnthropicAdapter, BaseVLMAdapter) aurait donné silencieusement
output_types = {CORRECTED_TEXT} au lieu de {RAW_TEXT} - lerreur
ne se manifestait quau runtime sur une jonction incompatible.
S54 ajoute __init_subclass__ qui lève TypeError à la définition
si lordre est mauvais avec un message qui suggère la correction
concrète. Les 4 VLM existants restent valides.
#14 - ExecutionPlan.metric_junctions
------------------------------------
Documenté honnêtement comme à venir : le PipelineExecutor ne
consomme pas encore ces jonctions au runtime (auto-évaluation
prévue dans un sprint dédié). Le champ est livré pour fixer le
contrat.
#22 - Listing des shims assumés
-------------------------------
Documenté dans CHANGELOG.md (voir entrée séparée).
Tests : 35 passed dans tests/adapters/vlm/ (5 S54 nouveaux + 30
S45). Test propriété : LLM-first ordre rejeté avec message
helpful (BaseVLMAdapter + AnthropicAdapter mentionnés + Corrigez).
https://claude.ai/code/session_011XQZNitg1rCgia8ZD1a2hP
|
@@ -61,8 +61,62 @@ class BaseVLMAdapter(BaseLLMAdapter):
|
|
| 61 |
Config dict ; supporte
|
| 62 |
``config["transcription_prompt"]`` pour personnaliser le
|
| 63 |
prompt de transcription.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
"""
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
@property
|
| 67 |
def input_types(self) -> "frozenset":
|
| 68 |
return frozenset({ArtifactType.IMAGE})
|
|
|
|
| 61 |
Config dict ; supporte
|
| 62 |
``config["transcription_prompt"]`` pour personnaliser le
|
| 63 |
prompt de transcription.
|
| 64 |
+
|
| 65 |
+
Sprint S54 — garde-fou MRO (audit #6)
|
| 66 |
+
-------------------------------------
|
| 67 |
+
Les VLM concrets utilisent l'héritage multiple :
|
| 68 |
+
|
| 69 |
+
::
|
| 70 |
+
|
| 71 |
+
class AnthropicVLMAdapter(BaseVLMAdapter, AnthropicAdapter)
|
| 72 |
+
|
| 73 |
+
L'ordre est critique : ``BaseVLMAdapter`` doit venir d'ABORD
|
| 74 |
+
pour que ``input_types``, ``output_types``, ``execute``, et
|
| 75 |
+
``DEFAULT_TRANSCRIPTION_PROMPT`` soient résolus depuis lui (et
|
| 76 |
+
pas depuis le LLM sibling qui aurait des output_types =
|
| 77 |
+
{CORRECTED_TEXT}).
|
| 78 |
+
|
| 79 |
+
``__init_subclass__`` valide cet ordre à la définition de la
|
| 80 |
+
classe. Si le développeur swap accidentellement les parents
|
| 81 |
+
par habitude alphabétique, la définition de classe lève une
|
| 82 |
+
``TypeError`` immédiate au lieu d'un comportement silencieusement
|
| 83 |
+
différent (output_types incorrect au runtime).
|
| 84 |
"""
|
| 85 |
|
| 86 |
+
def __init_subclass__(cls, **kwargs) -> None:
|
| 87 |
+
super().__init_subclass__(**kwargs)
|
| 88 |
+
# Garde-fou : BaseVLMAdapter doit être le premier parent
|
| 89 |
+
# *non-trivial* dans l'ordre de la déclaration (pour gagner
|
| 90 |
+
# le MRO sur les attributs surchargés).
|
| 91 |
+
bases = cls.__bases__
|
| 92 |
+
if len(bases) <= 1:
|
| 93 |
+
# Sous-classe directe simple — pas de MRO multiple, OK.
|
| 94 |
+
return
|
| 95 |
+
# On parcourt les bases dans l'ordre déclaré.
|
| 96 |
+
try:
|
| 97 |
+
vlm_idx = next(
|
| 98 |
+
i for i, b in enumerate(bases)
|
| 99 |
+
if issubclass(b, BaseVLMAdapter)
|
| 100 |
+
)
|
| 101 |
+
except StopIteration:
|
| 102 |
+
return # ne devrait pas arriver, vlm subclass DOIT inclure VLM
|
| 103 |
+
# Toutes les bases AVANT BaseVLMAdapter doivent être
|
| 104 |
+
# neutres (mixins sans surcharge des output_types).
|
| 105 |
+
for prev in bases[:vlm_idx]:
|
| 106 |
+
if issubclass(prev, BaseLLMAdapter) and not issubclass(
|
| 107 |
+
prev, BaseVLMAdapter,
|
| 108 |
+
):
|
| 109 |
+
raise TypeError(
|
| 110 |
+
f"{cls.__name__} : ordre MRO incorrect — "
|
| 111 |
+
f"BaseVLMAdapter doit précéder {prev.__name__} "
|
| 112 |
+
"dans la liste des parents pour que les "
|
| 113 |
+
"output_types VLM ({IMAGE} → {RAW_TEXT}) "
|
| 114 |
+
"soient résolus correctement (et pas écrasés "
|
| 115 |
+
"par les output_types LLM = {CORRECTED_TEXT}). "
|
| 116 |
+
f"Corrigez : `class {cls.__name__}(BaseVLMAdapter, "
|
| 117 |
+
f"{prev.__name__})`.",
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
@property
|
| 121 |
def input_types(self) -> "frozenset":
|
| 122 |
return frozenset({ArtifactType.IMAGE})
|
|
@@ -197,6 +197,15 @@ class ExecutionPlan:
|
|
| 197 |
metric_junctions:
|
| 198 |
Jonctions auto-détectées si un ``MetricRegistry`` était
|
| 199 |
fourni au planner ; tuple vide sinon.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
"""
|
| 201 |
|
| 202 |
spec: PipelineSpec
|
|
|
|
| 197 |
metric_junctions:
|
| 198 |
Jonctions auto-détectées si un ``MetricRegistry`` était
|
| 199 |
fourni au planner ; tuple vide sinon.
|
| 200 |
+
|
| 201 |
+
Sprint S54 — note honnête (audit #14) : à ce jour, le
|
| 202 |
+
``PipelineExecutor`` ne consomme pas ces jonctions au runtime
|
| 203 |
+
(le calcul des métriques aux jonctions intra-pipeline est
|
| 204 |
+
prévu dans un sprint dédié de l'axe « auto-évaluation »).
|
| 205 |
+
Le champ est livré dès maintenant pour fixer le contrat —
|
| 206 |
+
un caller peut déjà l'utiliser pour de l'introspection
|
| 207 |
+
(rapport, diagnostic). Pas de risque de breaking change
|
| 208 |
+
quand l'auto-évaluation arrivera.
|
| 209 |
"""
|
| 210 |
|
| 211 |
spec: PipelineSpec
|
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sprint A14-S54 — garde-fou MRO BaseVLMAdapter (fix audit #6).
|
| 2 |
+
|
| 3 |
+
Avant S54, l'ordre des parents dans :
|
| 4 |
+
|
| 5 |
+
class AnthropicVLMAdapter(BaseVLMAdapter, AnthropicAdapter)
|
| 6 |
+
|
| 7 |
+
était critique mais non vérifié. Un swap accidentel à
|
| 8 |
+
``(AnthropicAdapter, BaseVLMAdapter)`` aurait silencieusement donné
|
| 9 |
+
output_types = {CORRECTED_TEXT} (depuis LLM) au lieu de {RAW_TEXT}
|
| 10 |
+
(depuis VLM) — l'erreur ne se serait manifestée qu'au runtime sur
|
| 11 |
+
une jonction de type incompatible.
|
| 12 |
+
|
| 13 |
+
S54 ajoute ``__init_subclass__`` qui lève ``TypeError`` à la
|
| 14 |
+
définition de la classe si l'ordre est incorrect.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import pytest
|
| 20 |
+
|
| 21 |
+
from picarones.adapters.llm.anthropic_adapter import AnthropicAdapter
|
| 22 |
+
from picarones.adapters.llm.openai_adapter import OpenAIAdapter
|
| 23 |
+
from picarones.adapters.vlm import (
|
| 24 |
+
AnthropicVLMAdapter,
|
| 25 |
+
BaseVLMAdapter,
|
| 26 |
+
OpenAIVLMAdapter,
|
| 27 |
+
)
|
| 28 |
+
from picarones.domain.artifacts import ArtifactType
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class TestExistingAdaptersStillValid:
|
| 32 |
+
"""Les 4 VLM adapters concrets définis correctement passent."""
|
| 33 |
+
|
| 34 |
+
def test_anthropic_vlm_defined(self) -> None:
|
| 35 |
+
# Si l'ordre était mauvais, l'import aurait planté.
|
| 36 |
+
adapter = AnthropicVLMAdapter()
|
| 37 |
+
assert adapter.input_types == frozenset({ArtifactType.IMAGE})
|
| 38 |
+
assert adapter.output_types == frozenset({ArtifactType.RAW_TEXT})
|
| 39 |
+
|
| 40 |
+
def test_openai_vlm_defined(self) -> None:
|
| 41 |
+
adapter = OpenAIVLMAdapter()
|
| 42 |
+
assert adapter.input_types == frozenset({ArtifactType.IMAGE})
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class TestWrongOrderRejected:
|
| 46 |
+
def test_llm_first_then_vlm_rejected(self) -> None:
|
| 47 |
+
"""Définir une classe avec LLM avant VLM doit lever TypeError."""
|
| 48 |
+
with pytest.raises(TypeError, match="ordre MRO"):
|
| 49 |
+
# Définition dynamique d'une classe avec mauvais ordre.
|
| 50 |
+
type(
|
| 51 |
+
"BadOrderVLM",
|
| 52 |
+
(AnthropicAdapter, BaseVLMAdapter),
|
| 53 |
+
{"name": property(lambda self: "bad")},
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
def test_correct_order_accepted(self) -> None:
|
| 57 |
+
"""L'ordre correct (VLM en premier) est accepté."""
|
| 58 |
+
# Test propriété : aucun TypeError levé.
|
| 59 |
+
type(
|
| 60 |
+
"GoodOrderVLM",
|
| 61 |
+
(BaseVLMAdapter, OpenAIAdapter),
|
| 62 |
+
{"name": property(lambda self: "good")},
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class TestErrorMessageHelpful:
|
| 67 |
+
def test_message_explains_the_fix(self) -> None:
|
| 68 |
+
with pytest.raises(TypeError) as exc_info:
|
| 69 |
+
type(
|
| 70 |
+
"BadVLM",
|
| 71 |
+
(AnthropicAdapter, BaseVLMAdapter),
|
| 72 |
+
{"name": property(lambda self: "x")},
|
| 73 |
+
)
|
| 74 |
+
msg = str(exc_info.value)
|
| 75 |
+
# Le message doit suggérer la correction concrète.
|
| 76 |
+
assert "BaseVLMAdapter" in msg
|
| 77 |
+
assert "AnthropicAdapter" in msg
|
| 78 |
+
assert "Corrigez" in msg or "correct" in msg.lower()
|