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

@@ -11,7 +11,7 @@ Basée sur l'architecture éprouvée de la V2.
from typing import List, Dict, Optional, Any, Tuple
from pathlib import Path
from dataclasses import dataclass
from dataclasses import dataclass, field
import logging
import os
import time
@@ -25,6 +25,7 @@ logger = logging.getLogger(__name__)
from ..models.ui_element import UIElement, UIElementEmbeddings, VisualFeatures
from .ollama_client import OllamaClient, check_ollama_available
from . import vlm_config
# Import OWL-v2 (optionnel)
try:
@@ -71,10 +72,13 @@ class BoundingBox:
@dataclass
class DetectionConfig:
"""Configuration de la détection UI hybride"""
# VLM — modèle configurable via variable d'environnement RPA_VLM_MODEL
# Par défaut : gemma4:e4b (meilleur grounding + contextualisation)
# Fallback : qwen3-vl:8b si gemma4 non disponible
vlm_model: str = os.environ.get("RPA_VLM_MODEL", os.environ.get("VLM_MODEL", "gemma4:e4b"))
# VLM — modèle configurable via RPA_VLM_MODEL / VLM_MODEL.
# default_factory : lu à l'instanciation (pas figé à l'import) ; None si non
# défini → résolution lazy via vlm_config.get_vlm_model() dans _initialize_vlm
# (pas de hardcode, pas d'appel réseau à l'import).
vlm_model: Optional[str] = field(
default_factory=lambda: os.environ.get("RPA_VLM_MODEL") or os.environ.get("VLM_MODEL")
)
vlm_endpoint: str = "http://localhost:11434"
use_vlm_classification: bool = True # Utiliser VLM pour classifier
@@ -136,11 +140,16 @@ class UIDetector:
"""Initialiser le client VLM"""
try:
if check_ollama_available(self.config.vlm_endpoint):
# Résolution lazy : si aucun modèle explicite, vlm_config résout
# (avec fallback) en interrogeant /api/tags. On normalise la config
# pour que les métadonnées de sortie reflètent le modèle réel.
model = self.config.vlm_model or vlm_config.get_vlm_model(self.config.vlm_endpoint)
self.config.vlm_model = model
self.vlm_client = OllamaClient(
endpoint=self.config.vlm_endpoint,
model=self.config.vlm_model
model=model
)
logger.info(f"✓ VLM initialized: {self.config.vlm_model}")
logger.info(f"✓ VLM initialized: {model}")
else:
logger.warning("Ollama not available, VLM classification disabled")
self.vlm_client = None

View File

@@ -234,6 +234,33 @@ def get_grounding_profile(endpoint: str = DEFAULT_OLLAMA_ENDPOINT) -> dict:
}
def get_bbox_grounding_model() -> str:
"""Retourne le modèle pour le grounding **format bbox_2d natif** (qwen2.5vl).
Distinct de get_grounding_profile() (format JSON {x_pct,y_pct} via prefill,
défaut qwen3.5:9b). Les chemins bbox_2d de resolve_engine
(`parse_bbox_to_norm` / `parse_bbox_to_norm_validated`) exigent un modèle
de la famille qwen2.5vl qui émet des coordonnées en pixels.
D5-v3b (2026-06-03) : désambiguïse l'env var. Historiquement le site bbox
lisait `RPA_GROUNDING_MODEL`, partagé avec get_grounding_profile() qui
attend un modèle JSON → conflit documenté. On introduit une var dédiée.
Ordre de résolution :
1. RPA_BBOX_GROUNDING_MODEL (dédié, prioritaire)
2. RPA_GROUNDING_MODEL (rétrocompat — ancien comportement)
3. DEFAULT_GROUNDING_FALLBACK (qwen2.5vl:7b-rpa, présent sur DGX)
Returns:
Nom du modèle bbox_2d (ex: "qwen2.5vl:7b-rpa")
"""
return (
os.environ.get("RPA_BBOX_GROUNDING_MODEL")
or os.environ.get("RPA_GROUNDING_MODEL")
or DEFAULT_GROUNDING_FALLBACK
)
def needs_think_false(model_name: str) -> bool:
"""Détermine si un modèle nécessite think=false dans le payload.

View File

@@ -16,13 +16,13 @@ from typing import Any, Dict, List, Optional
import requests
from core.detection import vlm_config
from .schema import ExtractionField, ExtractionSchema
logger = logging.getLogger(__name__)
# Configuration Ollama (coherente avec le reste du projet)
OLLAMA_DEFAULT_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
OLLAMA_DEFAULT_MODEL = os.environ.get("RPA_VLM_MODEL", os.environ.get("VLM_MODEL", "gemma4:e4b"))
class FieldExtractor:
@@ -38,19 +38,34 @@ class FieldExtractor:
def __init__(
self,
ollama_url: str = OLLAMA_DEFAULT_URL,
ollama_model: str = OLLAMA_DEFAULT_MODEL,
ollama_model: Optional[str] = None,
timeout: int = 60,
):
"""
Args:
ollama_url: URL du serveur Ollama
ollama_model: Modele VLM a utiliser
ollama_model: Modele VLM a utiliser (None = resolution lazy via vlm_config)
timeout: Timeout en secondes pour les appels VLM
"""
self.ollama_url = ollama_url.rstrip("/")
self.ollama_model = ollama_model
self._ollama_model = ollama_model # None → resolu paresseusement
self.timeout = timeout
@property
def ollama_model(self) -> str:
"""Modele VLM, resolu paresseusement via vlm_config si non fourni.
Resolution differee au premier acces (pas a l'import ni a la
construction) : evite tout hardcode gemma4 et tout appel reseau a froid.
"""
if not self._ollama_model:
self._ollama_model = vlm_config.get_vlm_model(self.ollama_url)
return self._ollama_model
@ollama_model.setter
def ollama_model(self, value: Optional[str]) -> None:
self._ollama_model = value
# ------------------------------------------------------------------
# API publique
# ------------------------------------------------------------------

View File

@@ -23,6 +23,7 @@ from pathlib import Path
from typing import Any, Dict, List, Optional
from .workflow_ir import WorkflowIR, Step, Action, Variable
from core.detection import vlm_config
logger = logging.getLogger(__name__)
@@ -41,7 +42,10 @@ class IRBuilder:
"""
def __init__(self, gemma4_port: str = ""):
self._gemma4_port = gemma4_port or os.environ.get("GEMMA4_PORT", "11435")
# Endpoint VLM : piloté par config (Ollama local ou tunnel DGX = 11434).
# GEMMA4_PORT conservé comme override legacy (ancien conteneur Docker 11435).
_default_port = vlm_config.DEFAULT_OLLAMA_ENDPOINT.rsplit(":", 1)[-1]
self._gemma4_port = gemma4_port or os.environ.get("GEMMA4_PORT", _default_port)
self._gemma4_url = f"http://localhost:{self._gemma4_port}/api/chat"
def build(
@@ -563,7 +567,7 @@ class IRBuilder:
resp = _requests.post(
self._gemma4_url,
json={
"model": "gemma4:e4b",
"model": vlm_config.get_vlm_model(),
"messages": [{"role": "user", "content": prompt}],
"stream": False,
"think": True,