feat(cognition): raisonnement VLM quand les réflexes ne suffisent pas
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 14s
security-audit / pip-audit (CVE dépendances) (push) Successful in 11s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 14s
tests / Tests unitaires (sans GPU) (push) Failing after 14s
tests / Tests sécurité (critique) (push) Has been skipped
Some checks failed
security-audit / Bandit (scan statique) (push) Successful in 14s
security-audit / pip-audit (CVE dépendances) (push) Successful in 11s
security-audit / Scan secrets (grep) (push) Successful in 8s
tests / Lint (ruff + black) (push) Successful in 14s
tests / Tests unitaires (sans GPU) (push) Failing after 14s
tests / Tests sécurité (critique) (push) Has been skipped
vlm_reason_about_screen() : capture l'écran, envoie au VLM local (gemma4/Ollama) avec l'objectif et le contexte, retourne une action en JSON (click/type/wait/nothing + target + reasoning). Chaîne de décision : 1. Réflexes (UIPatternLibrary) → instantané 2. OCR bouton (docTR) → rapide 3. VLM reasoning (Ollama) → intelligent, ~2-5s Le VLM intervient UNIQUEMENT quand 1 et 2 échouent — pas de latence ajoutée quand les réflexes suffisent. UIPatternLibrary enrichie : charge builtin + GUI-R1 + learned patterns. save_learned_pattern() persiste les patterns appris par Shadow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -206,7 +206,23 @@ def handle_detected_pattern(pattern: Dict[str, Any]) -> bool:
|
||||
time.sleep(1.0)
|
||||
return True
|
||||
|
||||
logger.info(f"Bouton '{target}' introuvable par OCR")
|
||||
logger.info(f"Bouton '{target}' introuvable par OCR — appel VLM...")
|
||||
vlm_result = vlm_reason_about_screen(
|
||||
objective=f"Cliquer sur le bouton '{target}'",
|
||||
context=f"Un dialogue '{pattern.get('pattern')}' est détecté"
|
||||
)
|
||||
if vlm_result and vlm_result.get('action') == 'click' and vlm_result.get('target'):
|
||||
vlm_target = vlm_result['target']
|
||||
for word in words:
|
||||
if vlm_target.lower() in word['text'].lower():
|
||||
x1, y1, x2, y2 = word['bbox']
|
||||
x = int((x1 + x2) / 2)
|
||||
y = int((y1 + y2) / 2)
|
||||
logger.info(f"VLM → clic sur '{word['text']}' à ({x}, {y})")
|
||||
pyautogui.click(x, y)
|
||||
time.sleep(1.0)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
@@ -223,6 +239,90 @@ def handle_detected_pattern(pattern: Dict[str, Any]) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def vlm_reason_about_screen(objective: str = "", context: str = "") -> Optional[Dict[str, Any]]:
|
||||
"""Demande au VLM de raisonner sur l'écran actuel et proposer une action.
|
||||
|
||||
Utilisé quand les réflexes (patterns) ne suffisent pas.
|
||||
Le VLM voit l'écran et décide quoi faire.
|
||||
|
||||
Args:
|
||||
objective: Ce que Léa essaie de faire (ex: "cliquer sur Enregistrer")
|
||||
context: Contexte additionnel (ex: "un dialogue est apparu")
|
||||
|
||||
Returns:
|
||||
Dict avec 'action', 'target', 'reasoning' ou None si le VLM ne peut pas aider.
|
||||
"""
|
||||
try:
|
||||
import mss
|
||||
import requests
|
||||
import json
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
from PIL import Image
|
||||
|
||||
with mss.mss() as sct:
|
||||
monitor = sct.monitors[1]
|
||||
screenshot = sct.grab(monitor)
|
||||
screen = Image.frombytes('RGB', screenshot.size, screenshot.bgra, 'raw', 'BGRX')
|
||||
|
||||
buffer = io.BytesIO()
|
||||
screen.save(buffer, format='JPEG', quality=70)
|
||||
image_b64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
|
||||
prompt = f"""Analyse cet écran et dis-moi quoi faire.
|
||||
|
||||
Objectif : {objective or "Interagir avec l'interface visible"}
|
||||
Contexte : {context or "Aucun contexte supplémentaire"}
|
||||
|
||||
Réponds en JSON strict :
|
||||
{{
|
||||
"action": "click" ou "type" ou "wait" ou "nothing",
|
||||
"target": "texte exact du bouton ou champ à cliquer",
|
||||
"reasoning": "explication courte de ton choix"
|
||||
}}
|
||||
|
||||
Si tu vois un dialogue ou une popup, indique quel bouton cliquer.
|
||||
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_VLM_MODEL", os.environ.get("VLM_MODEL", "gemma4:e4b"))
|
||||
|
||||
response = requests.post(
|
||||
f"{ollama_url}/api/generate",
|
||||
json={
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"images": [image_b64],
|
||||
"stream": False,
|
||||
"options": {"temperature": 0.1, "num_predict": 200}
|
||||
},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warning(f"VLM reasoning failed: HTTP {response.status_code}")
|
||||
return None
|
||||
|
||||
result = response.json()
|
||||
text = result.get('response', '').strip()
|
||||
|
||||
import re
|
||||
match = re.search(r'\{[\s\S]*\}', text)
|
||||
if match:
|
||||
parsed = json.loads(match.group())
|
||||
logger.info(f"VLM reasoning: {parsed.get('action')} '{parsed.get('target')}' — {parsed.get('reasoning', '')[:80]}")
|
||||
return parsed
|
||||
|
||||
logger.debug(f"VLM response not parseable: {text[:100]}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"VLM reasoning failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def post_execution_cleanup(execution_mode: str = 'debug'):
|
||||
"""Vérifie l'écran après exécution et gère les dialogues restants.
|
||||
|
||||
@@ -240,4 +340,10 @@ def post_execution_cleanup(execution_mode: str = 'debug'):
|
||||
handle_detected_pattern(detected)
|
||||
time.sleep(1.0)
|
||||
else:
|
||||
vlm_result = vlm_reason_about_screen(
|
||||
objective="Vérifier que l'écran est propre après l'exécution",
|
||||
context="Le workflow vient de se terminer"
|
||||
)
|
||||
if vlm_result and vlm_result.get('action') in ('click', 'type'):
|
||||
logger.info(f"VLM post-workflow: {vlm_result.get('action')} '{vlm_result.get('target')}'")
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user