diff --git a/core/cognition/vram_orchestrator.py b/core/cognition/vram_orchestrator.py index 5e5d5962c..65f3c2a25 100644 --- a/core/cognition/vram_orchestrator.py +++ b/core/cognition/vram_orchestrator.py @@ -3,7 +3,7 @@ Orchestrateur VRAM — gère le chargement/déchargement des modèles selon le m Deux modes : - SHADOW : streaming server + agent_chat actifs, VLM raisonnement déchargé -- REPLAY : VLM raisonnement (qwen2.5vl:7b) chargé, services non-essentiels stoppés +- REPLAY : VLM raisonnement (cf. get_reasoning_model) chargé, services non-essentiels stoppés Bascule automatique ou manuelle selon le contexte. """ @@ -15,10 +15,12 @@ import time from enum import Enum from typing import Optional +from core.detection.vlm_config import get_reasoning_model + logger = logging.getLogger(__name__) OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434") -REASONING_MODEL = os.environ.get("RPA_REASONING_MODEL", "qwen2.5vl:7b") +REASONING_MODEL = get_reasoning_model() MIN_VRAM_FOR_REASONING = 5.0 # Go minimum pour charger le modèle de raisonnement diff --git a/core/detection/vlm_config.py b/core/detection/vlm_config.py index b96e4dc48..acd75bc43 100644 --- a/core/detection/vlm_config.py +++ b/core/detection/vlm_config.py @@ -261,6 +261,42 @@ def get_bbox_grounding_model() -> str: ) +# ──────────────────────────────────────────────────────────────────────────── +# P1.z (2026-06-04) : résolution centralisée du modèle V4/reasoning, DGX-safe +# ──────────────────────────────────────────────────────────────────────────── + +# Modèle de raisonnement V4/ORA par défaut — DGX-safe. +# Les chemins reasoning (ORALoop, détection dialogue/popup, vram_orchestrator) +# font du VLM généraliste sur screenshot (JSON action/decision), pas du grounding +# bbox. Le default est aligné sur le modèle présent sur le tunnel DGX +# (qwen2.5vl:7b-rpa), PAS sur `qwen2.5vl:7b` brut qui est absent du DGX → 404. +DEFAULT_REASONING_MODEL = "qwen2.5vl:7b-rpa" + + +def get_reasoning_model() -> str: + """Retourne le modèle pour les chemins V4/reasoning (ORALoop, détection + dialogue/popup, orchestration VRAM). + + Distinct du grounding (get_grounding_profile / get_bbox_grounding_model) : + ici on raisonne en langage naturel + JSON sur un screenshot, pas de + coordonnées. Pas d'appel réseau (résolution lazy, safe à l'import). + + Ordre de résolution : + 1. RPA_REASONING_MODEL (dédié, prioritaire) + 2. RPA_VLM_MODEL / VLM_MODEL (hérite de la config VLM existante) + 3. DEFAULT_REASONING_MODEL (qwen2.5vl:7b-rpa, présent sur DGX) + + Returns: + Nom du modèle de raisonnement (ex: "qwen2.5vl:7b-rpa"). + """ + return ( + os.environ.get("RPA_REASONING_MODEL") + or os.environ.get("RPA_VLM_MODEL") + or os.environ.get("VLM_MODEL") + or DEFAULT_REASONING_MODEL + ) + + def needs_think_false(model_name: str) -> bool: """Détermine si un modèle nécessite think=false dans le payload. diff --git a/core/execution/input_handler.py b/core/execution/input_handler.py index b06c77e92..a41bed510 100644 --- a/core/execution/input_handler.py +++ b/core/execution/input_handler.py @@ -14,6 +14,8 @@ import shutil import time from typing import Any, Dict, List, Optional +from core.detection.vlm_config import get_reasoning_model + logger = logging.getLogger(__name__) try: @@ -291,7 +293,7 @@ Si l'écran est normal sans action nécessaire, réponds action="nothing". Réponds UNIQUEMENT le JSON, pas d'explication.""" ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434") - model = os.environ.get("RPA_REASONING_MODEL", "qwen2.5vl:7b") + model = get_reasoning_model() response = requests.post( f"{ollama_url}/api/generate", diff --git a/core/execution/observe_reason_act.py b/core/execution/observe_reason_act.py index 0ab5d59a9..364c0b84e 100644 --- a/core/execution/observe_reason_act.py +++ b/core/execution/observe_reason_act.py @@ -21,6 +21,8 @@ import re from dataclasses import dataclass from typing import Any, Callable, Dict, List, Optional +from core.detection.vlm_config import get_reasoning_model + logger = logging.getLogger(__name__) # Import du contexte cognitif (mémoire de travail) @@ -407,7 +409,7 @@ Règles: # --- Appel VLM (Ollama) --- ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434") - model = os.environ.get("RPA_REASONING_MODEL", "qwen2.5vl:7b") + model = get_reasoning_model() print(f"🧠 [ORA/reason_instruction] Appel VLM {model}...") @@ -1207,7 +1209,7 @@ Règles: image_b64 = base64.b64encode(buffer.getvalue()).decode('utf-8') ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434") - model = os.environ.get("RPA_REASONING_MODEL", "qwen2.5vl:7b") + model = get_reasoning_model() resp = requests.post(f"{ollama_url}/api/generate", json={ "model": model, @@ -1963,7 +1965,7 @@ Règles: ) ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434") - model = os.environ.get("RPA_REASONING_MODEL", "qwen2.5vl:7b") + model = get_reasoning_model() response = requests.post( f"{ollama_url}/api/generate", diff --git a/tests/unit/test_reasoning_model.py b/tests/unit/test_reasoning_model.py new file mode 100644 index 000000000..7a2c1e436 --- /dev/null +++ b/tests/unit/test_reasoning_model.py @@ -0,0 +1,82 @@ +"""Tests P1.z — résolution centralisée du modèle V4/reasoning (DGX-safe). + +Garantit que les chemins V4/reasoning (ORALoop, détection dialogue/popup, +vram_orchestrator) ne tombent plus sur le default brut `qwen2.5vl:7b` (absent +du tunnel DGX → 404) quand `RPA_REASONING_MODEL` n'est pas positionné. +""" + +import importlib +import os + +import pytest + +from core.detection import vlm_config + + +# Le default historique dangereux : présent dans aucun modèle DGX → 404. +DANGEROUS_RAW_DEFAULT = "qwen2.5vl:7b" + +# Les 3 env vars qui influencent la résolution reasoning. +_REASONING_ENVS = ("RPA_REASONING_MODEL", "RPA_VLM_MODEL", "VLM_MODEL") + + +@pytest.fixture +def clean_reasoning_env(monkeypatch): + """Neutralise toute env reasoning/VLM pour tester le default pur.""" + for var in _REASONING_ENVS: + monkeypatch.delenv(var, raising=False) + return monkeypatch + + +def test_default_is_dgx_safe(clean_reasoning_env): + """Sans aucune env, le default ne doit PAS être `qwen2.5vl:7b` brut.""" + model = vlm_config.get_reasoning_model() + assert model != DANGEROUS_RAW_DEFAULT + # Le default doit pointer vers un modèle présent sur DGX (tag -rpa). + assert model == vlm_config.DEFAULT_REASONING_MODEL + assert "-rpa" in model + + +def test_rpa_reasoning_model_has_priority(clean_reasoning_env): + """RPA_REASONING_MODEL prime sur tout le reste.""" + clean_reasoning_env.setenv("RPA_REASONING_MODEL", "mon-modele:custom") + clean_reasoning_env.setenv("RPA_VLM_MODEL", "autre:vlm") + assert vlm_config.get_reasoning_model() == "mon-modele:custom" + + +def test_falls_back_to_vlm_model(clean_reasoning_env): + """Sans RPA_REASONING_MODEL, on hérite de la config VLM existante.""" + clean_reasoning_env.setenv("RPA_VLM_MODEL", "qwen2.5vl:7b-rpa") + assert vlm_config.get_reasoning_model() == "qwen2.5vl:7b-rpa" + + +def test_falls_back_to_legacy_vlm_model_alias(clean_reasoning_env): + """L'alias de compat VLM_MODEL est honoré en dernier recours avant default.""" + clean_reasoning_env.setenv("VLM_MODEL", "qwen2.5vl:7b-rpa") + assert vlm_config.get_reasoning_model() == "qwen2.5vl:7b-rpa" + + +@pytest.mark.parametrize( + "module_name", + [ + "core.execution.input_handler", + "core.execution.observe_reason_act", + "core.cognition.vram_orchestrator", + ], +) +def test_v4_modules_have_no_raw_hardcoded_default(module_name): + """Non-régression : aucun module V4 ne hardcode plus + `os.environ.get("RPA_REASONING_MODEL", "qwen2.5vl:7b")`.""" + module = importlib.import_module(module_name) + source = open(module.__file__, encoding="utf-8").read() + assert 'RPA_REASONING_MODEL", "qwen2.5vl:7b"' not in source, ( + f"{module_name} hardcode encore le default dangereux qwen2.5vl:7b" + ) + + +def test_vram_orchestrator_reasoning_model_is_dgx_safe(clean_reasoning_env): + """Le constant module-level REASONING_MODEL, recalculé sans env, + ne doit pas valoir le default brut dangereux.""" + from core.cognition import vram_orchestrator + importlib.reload(vram_orchestrator) + assert vram_orchestrator.REASONING_MODEL != DANGEROUS_RAW_DEFAULT