ThinkArbiter (core/grounding/think_arbiter.py) : - Client HTTP vers le serveur UI-TARS (port 8200) - Appelé uniquement si SmartMatcher score < 0.60 - Vérifie la disponibilité du serveur avant appel - Validé : Demo trouvé à (1479, 183) en 3.6s SignatureStore (core/grounding/element_signature.py) : - Stockage SQLite des signatures d'éléments UI apprises - record_success() enrichit la signature (texte, type, position, voisins) - record_failure() incrémente le compteur d'échecs - lookup() avec fallback (contexte exact → toutes variantes) - Validé : 3 succès → conf_moy=0.917, voisins enrichis Modules standalone — aucun impact sur le système existant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
118 lines
3.7 KiB
Python
118 lines
3.7 KiB
Python
"""
|
|
core/grounding/think_arbiter.py — Layer THINK : VLM arbitre (UI-TARS)
|
|
|
|
Appelé UNIQUEMENT quand le SmartMatcher n'a pas assez confiance :
|
|
- Score < 0.60 : aucun candidat clair → UI-TARS cherche dans tout l'écran
|
|
- Score 0.60-0.90 : candidats ambigus → UI-TARS confirme/infirme
|
|
|
|
Le VLM tourne dans un process séparé (serveur FastAPI port 8200).
|
|
Ce module est un CLIENT HTTP — il ne charge aucun modèle en VRAM.
|
|
|
|
Utilisation :
|
|
from core.grounding.think_arbiter import ThinkArbiter
|
|
|
|
arbiter = ThinkArbiter()
|
|
if arbiter.available:
|
|
result = arbiter.arbitrate(target, candidates, screenshot)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import io
|
|
import time
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from core.grounding.fast_types import DetectedUIElement, LocateResult, MatchCandidate
|
|
from core.grounding.target import GroundingTarget
|
|
|
|
|
|
class ThinkArbiter:
|
|
"""Arbitre VLM pour les cas ambigus — appelle le serveur UI-TARS."""
|
|
|
|
DEFAULT_URL = "http://localhost:8200"
|
|
|
|
def __init__(self, server_url: str = DEFAULT_URL, timeout: int = 30):
|
|
self.server_url = server_url
|
|
self.timeout = timeout
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""Vérifie si le serveur de grounding est accessible."""
|
|
try:
|
|
import requests
|
|
resp = requests.get(f"{self.server_url}/health", timeout=3)
|
|
return resp.status_code == 200 and resp.json().get("model_loaded", False)
|
|
except Exception:
|
|
return False
|
|
|
|
def arbitrate(
|
|
self,
|
|
target: GroundingTarget,
|
|
candidates: List[MatchCandidate],
|
|
screenshot_pil: Optional[Any] = None,
|
|
) -> Optional[LocateResult]:
|
|
"""Demande au VLM de trancher.
|
|
|
|
Args:
|
|
target: Ce qu'on cherche.
|
|
candidates: Candidats SMART (peut être vide).
|
|
screenshot_pil: Screenshot PIL. Si None, le serveur capture lui-même.
|
|
|
|
Returns:
|
|
LocateResult ou None si le VLM ne trouve pas non plus.
|
|
"""
|
|
t0 = time.time()
|
|
|
|
try:
|
|
import requests
|
|
|
|
# Construire le payload
|
|
payload: Dict[str, Any] = {
|
|
"target_text": target.text or "",
|
|
"target_description": target.description or "",
|
|
}
|
|
|
|
# Envoyer l'image si disponible
|
|
if screenshot_pil is not None:
|
|
buf = io.BytesIO()
|
|
screenshot_pil.save(buf, format="JPEG", quality=85)
|
|
payload["image_b64"] = base64.b64encode(buf.getvalue()).decode("utf-8")
|
|
|
|
# Appel au serveur
|
|
resp = requests.post(
|
|
f"{self.server_url}/ground",
|
|
json=payload,
|
|
timeout=self.timeout,
|
|
)
|
|
|
|
dt = (time.time() - t0) * 1000
|
|
|
|
if resp.status_code != 200:
|
|
print(f"🤔 [THINK] Serveur HTTP {resp.status_code}")
|
|
return None
|
|
|
|
data = resp.json()
|
|
|
|
if data.get("x") is None:
|
|
print(f"🤔 [THINK] VLM n'a pas trouvé '{target.text}' ({dt:.0f}ms)")
|
|
return None
|
|
|
|
result = LocateResult(
|
|
x=data["x"],
|
|
y=data["y"],
|
|
confidence=data.get("confidence", 0.85),
|
|
method="think_vlm",
|
|
time_ms=dt,
|
|
tier="think",
|
|
candidates_count=len(candidates),
|
|
)
|
|
|
|
print(f"🤔 [THINK] VLM → ({result.x}, {result.y}) conf={result.confidence:.2f} ({dt:.0f}ms)")
|
|
return result
|
|
|
|
except Exception as ex:
|
|
dt = (time.time() - t0) * 1000
|
|
print(f"⚠️ [THINK] Erreur: {ex} ({dt:.0f}ms)")
|
|
return None
|