feat(p1x): de-hardcode VLM models/endpoints to vlm_config (DGX-ready)

Migre les call-sites VLM serveur vers la configuration centrale pour
fonctionner sur DGX (tunnel Ollama 11434), où gemma4:* est absent et le
port Docker 11435 est mort.

- task_planner, replay_verifier, domain_context, ir_builder, resolve_engine
  (popup): modele -> vlm_config.get_vlm_model(), defaut 11435 -> 11434
  (override GEMMA4_PORT legacy conserve)
- resolve_engine (grounding bbox x2): nouvel helper
  vlm_config.get_bbox_grounding_model() (var dediee RPA_BBOX_GROUNDING_MODEL,
  fallback RPA_GROUNDING_MODEL puis qwen2.5vl:7b-rpa) -> desambiguise le
  conflit D5-v3b, bbox_2d + num_ctx 4096 preserves
- safety_checks_provider: defaut -> get_vlm_model(), override
  RPA_SAFETY_CHECKS_LLM_MODEL preserve
- ui_detector: default_factory + resolution lazy (corrige aussi un gel a
  l'import), pas d'appel reseau a l'import
- field_extractor: property lazy via vlm_config

TDD strict (RED->GREEN), 305 tests verts, tests mockes HTTP (zero dependance
DGX reel), aucun alias Ollama.

Hors perimetre (arbitrage Dom): client Lea agent_v1/executor.py (gele),
chemin V4 observe_reason_act (RPA_REASONING_MODEL), core/config.py defaults.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-06-03 14:06:03 +02:00
parent 4e7c2a7628
commit 4dc7d840d6
21 changed files with 739 additions and 37 deletions

View File

@@ -32,6 +32,20 @@ from agent_v0.server_v1.task_planner import TaskPlanner, TaskPlan
# Fixtures
# =========================================================================
@pytest.fixture(autouse=True)
def _mock_vlm_model():
"""Neutralise la résolution VLM (pas de dépendance Ollama/DGX en test).
Par défaut, get_vlm_model() interroge Ollama (/api/tags) ; on la fige
pour garder les tests déterministes et hors réseau.
"""
with patch(
"agent_v0.server_v1.task_planner.vlm_config.get_vlm_model",
return_value="gemma4:latest",
):
yield
@pytest.fixture
def planner():
"""TaskPlanner avec port gemma4 factice."""
@@ -77,6 +91,65 @@ def _mock_gemma4_response(content: str):
return mock_resp
# =========================================================================
# Tests : dé-hardcodage VLM (modèle via vlm_config, endpoint 11434)
# =========================================================================
class TestVlmConfigDehardcode:
"""Le modèle et l'endpoint ne doivent plus être codés en dur."""
def test_understand_utilise_modele_de_vlm_config(self, sample_workflows):
"""Le payload understand() utilise le modèle résolu par vlm_config."""
captured = {}
def capture_post(url, json=None, **kwargs):
captured["url"] = url
captured["model"] = (json or {}).get("model")
return _mock_gemma4_response("COMPRIS: NON\nWORKFLOW: AUCUN\nBOUCLE: NON\n")
with patch(
"agent_v0.server_v1.task_planner.vlm_config.get_vlm_model",
return_value="modele-resolu:test",
), patch("requests.post", side_effect=capture_post):
planner = TaskPlanner(domain_id="generic")
planner.understand("Ouvre le bloc-notes", available_workflows=sample_workflows)
assert captured["model"] == "modele-resolu:test"
def test_steps_to_actions_utilise_modele_de_vlm_config(self):
"""Le payload _steps_to_actions() utilise le modèle résolu par vlm_config."""
captured = {}
def capture_post(url, json=None, **kwargs):
captured["model"] = (json or {}).get("model")
return _mock_gemma4_response('{"type": "wait", "duration_ms": 100}\n')
with patch(
"agent_v0.server_v1.task_planner.vlm_config.get_vlm_model",
return_value="modele-resolu:test",
), patch("requests.post", side_effect=capture_post):
planner = TaskPlanner(domain_id="generic")
planner._steps_to_actions([{"description": "1. Attendre"}], {})
assert captured["model"] == "modele-resolu:test"
def test_endpoint_par_defaut_cible_11434(self, monkeypatch, sample_workflows):
"""Sans GEMMA4_PORT, l'endpoint vise 11434 (Ollama/tunnel DGX), pas 11435."""
monkeypatch.delenv("GEMMA4_PORT", raising=False)
captured = {}
def capture_post(url, json=None, **kwargs):
captured["url"] = url
return _mock_gemma4_response("COMPRIS: NON\nWORKFLOW: AUCUN\nBOUCLE: NON\n")
with patch("requests.post", side_effect=capture_post):
planner = TaskPlanner(domain_id="generic")
planner.understand("Ouvre le bloc-notes", available_workflows=sample_workflows)
assert ":11434" in captured["url"]
assert ":11435" not in captured["url"]
# =========================================================================
# Tests : understand — ordre simple
# =========================================================================