Feat: Action analyser_avec_ia (Ollama qwen2.5-vl)
Nouvelle action d'intelligence artificielle: - Analyse de contenu visuel via Ollama - 8 types d'analyse prédéfinis: general, formulaire, erreur, boutons, tableau, menu, validation, extraction - Prompts personnalisables - Support température et max_tokens - Variable de sortie configurable Modèle par défaut: qwen2.5-vl:7b Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Actions Intelligence VWB - Module d'initialisation
|
||||
Auteur : Dom, Claude - 14 janvier 2026
|
||||
|
||||
Ce module contient les actions d'intelligence artificielle
|
||||
pour le Visual Workflow Builder.
|
||||
|
||||
Actions disponibles :
|
||||
- VWBAnalyserAvecIAAction : Analyse de contenu visuel avec IA (Ollama)
|
||||
"""
|
||||
|
||||
from .analyser_avec_ia import VWBAnalyserAvecIAAction, VWBAIAnalyzeTextAction
|
||||
|
||||
__all__ = [
|
||||
'VWBAnalyserAvecIAAction',
|
||||
'VWBAIAnalyzeTextAction', # Alias anglais
|
||||
]
|
||||
|
||||
__version__ = '1.0.0'
|
||||
__author__ = 'Dom, Claude'
|
||||
__date__ = '14 janvier 2026'
|
||||
@@ -0,0 +1,377 @@
|
||||
"""
|
||||
Action Analyser avec IA - Analyse de contenu visuel via Ollama
|
||||
Auteur : Dom, Claude - 14 janvier 2026
|
||||
|
||||
Cette action permet d'analyser du contenu visuel (texte, images, interfaces)
|
||||
en utilisant un modèle de vision IA (Ollama avec qwen2.5-vl ou similaire).
|
||||
|
||||
Cas d'usage :
|
||||
- Comprendre le contenu d'une zone d'écran
|
||||
- Extraire des informations structurées
|
||||
- Valider visuellement des états d'interface
|
||||
- Analyser des documents ou formulaires
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
from datetime import datetime
|
||||
import time
|
||||
import base64
|
||||
import io
|
||||
import requests
|
||||
|
||||
from ..base_action import BaseVWBAction, VWBActionResult, VWBActionStatus
|
||||
from ...contracts.error import VWBErrorType, create_vwb_error
|
||||
from ...contracts.visual_anchor import VWBVisualAnchor
|
||||
|
||||
|
||||
# Configuration Ollama par défaut
|
||||
OLLAMA_DEFAULT_URL = "http://localhost:11434"
|
||||
OLLAMA_DEFAULT_MODEL = "qwen2.5-vl:7b"
|
||||
|
||||
|
||||
class VWBAnalyserAvecIAAction(BaseVWBAction):
|
||||
"""
|
||||
Action d'analyse de contenu visuel avec IA.
|
||||
|
||||
Utilise Ollama avec un modèle de vision (qwen2.5-vl) pour :
|
||||
- Analyser le contenu d'une zone d'écran
|
||||
- Répondre à des questions sur l'interface
|
||||
- Extraire des informations structurées
|
||||
- Valider des états visuels
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
action_id: str,
|
||||
parameters: Dict[str, Any],
|
||||
screen_capturer=None
|
||||
):
|
||||
"""
|
||||
Initialise l'action d'analyse IA.
|
||||
|
||||
Args:
|
||||
action_id: Identifiant unique de l'action
|
||||
parameters: Paramètres de l'analyse
|
||||
screen_capturer: Instance du ScreenCapturer (optionnel)
|
||||
"""
|
||||
super().__init__(
|
||||
action_id=action_id,
|
||||
name="Analyser avec IA",
|
||||
description="Analyse du contenu visuel avec intelligence artificielle",
|
||||
parameters=parameters,
|
||||
screen_capturer=screen_capturer
|
||||
)
|
||||
|
||||
# Zone à analyser (ancre visuelle ou région)
|
||||
self.ancre_visuelle: Optional[VWBVisualAnchor] = (
|
||||
parameters.get('visual_anchor') or
|
||||
parameters.get('ancre_visuelle')
|
||||
)
|
||||
self.region = parameters.get('region') # {x, y, width, height}
|
||||
|
||||
# Prompt d'analyse
|
||||
self.prompt = parameters.get('prompt', parameters.get('question', ''))
|
||||
self.prompt_systeme = parameters.get('prompt_systeme', parameters.get('system_prompt', ''))
|
||||
|
||||
# Type d'analyse prédéfini
|
||||
self.type_analyse = parameters.get('type_analyse', parameters.get('analysis_type', 'general'))
|
||||
|
||||
# Configuration Ollama
|
||||
self.ollama_url = parameters.get('ollama_url', OLLAMA_DEFAULT_URL)
|
||||
self.ollama_model = parameters.get('ollama_model', parameters.get('model', OLLAMA_DEFAULT_MODEL))
|
||||
|
||||
# Options
|
||||
self.timeout_ms = parameters.get('timeout_ms', 30000)
|
||||
self.temperature = parameters.get('temperature', 0.3)
|
||||
self.max_tokens = parameters.get('max_tokens', 1000)
|
||||
|
||||
# Variable de sortie
|
||||
self.variable_sortie = parameters.get('variable_sortie', parameters.get('output_variable', 'analyse_ia'))
|
||||
|
||||
# Prompts prédéfinis par type d'analyse
|
||||
self.prompts_predefinifs = {
|
||||
'general': "Décris ce que tu vois dans cette image de manière concise.",
|
||||
'formulaire': "Analyse ce formulaire. Liste les champs visibles, leur état (rempli/vide) et les valeurs si lisibles.",
|
||||
'erreur': "Y a-t-il un message d'erreur visible ? Si oui, quel est son contenu exact ?",
|
||||
'boutons': "Liste tous les boutons visibles avec leur texte et leur état apparent (actif/inactif/grisé).",
|
||||
'tableau': "Analyse ce tableau. Décris sa structure (colonnes, lignes) et résume son contenu.",
|
||||
'menu': "Décris les options de menu visibles et leur hiérarchie.",
|
||||
'validation': "Cette interface semble-t-elle dans un état valide ? Décris ce que tu observes.",
|
||||
'extraction': "Extrait toutes les informations textuelles visibles de manière structurée.",
|
||||
}
|
||||
|
||||
def validate_parameters(self) -> List[str]:
|
||||
"""Valide les paramètres de l'action."""
|
||||
erreurs = []
|
||||
|
||||
# Vérifier qu'on a une source d'image
|
||||
if not self.ancre_visuelle and not self.region and not self.screen_capturer:
|
||||
erreurs.append("Ancre visuelle, région ou screen_capturer requis")
|
||||
|
||||
# Vérifier le prompt
|
||||
if not self.prompt and self.type_analyse not in self.prompts_predefinifs:
|
||||
erreurs.append("Prompt ou type d'analyse valide requis")
|
||||
|
||||
# Vérifier le timeout
|
||||
if self.timeout_ms < 5000:
|
||||
erreurs.append("Timeout minimum: 5000ms (5 secondes)")
|
||||
|
||||
if self.timeout_ms > 120000:
|
||||
erreurs.append("Timeout maximum: 120000ms (2 minutes)")
|
||||
|
||||
return erreurs
|
||||
|
||||
def execute_core(self, step_id: str) -> VWBActionResult:
|
||||
"""
|
||||
Exécute l'analyse IA.
|
||||
|
||||
Args:
|
||||
step_id: Identifiant de l'étape
|
||||
|
||||
Returns:
|
||||
Résultat d'exécution avec l'analyse
|
||||
"""
|
||||
start_time = datetime.now()
|
||||
|
||||
try:
|
||||
# Étape 1: Capturer l'image à analyser
|
||||
image_base64 = self._capturer_image()
|
||||
|
||||
if not image_base64:
|
||||
return self._create_error_result(
|
||||
step_id=step_id,
|
||||
start_time=start_time,
|
||||
error_type=VWBErrorType.SCREEN_CAPTURE_FAILED,
|
||||
message="Impossible de capturer l'image à analyser"
|
||||
)
|
||||
|
||||
# Étape 2: Construire le prompt
|
||||
prompt_final = self._construire_prompt()
|
||||
|
||||
# Étape 3: Appeler Ollama
|
||||
print(f"🤖 Analyse IA en cours ({self.ollama_model})...")
|
||||
resultat_analyse = self._analyser_avec_ollama(image_base64, prompt_final)
|
||||
|
||||
if resultat_analyse is None:
|
||||
return self._create_error_result(
|
||||
step_id=step_id,
|
||||
start_time=start_time,
|
||||
error_type=VWBErrorType.SYSTEM_ERROR,
|
||||
message="Échec de l'analyse IA"
|
||||
)
|
||||
|
||||
end_time = datetime.now()
|
||||
execution_time = (end_time - start_time).total_seconds() * 1000
|
||||
|
||||
print(f"✅ Analyse IA terminée en {execution_time:.0f}ms")
|
||||
print(f"📝 Résultat: {resultat_analyse[:200]}..." if len(resultat_analyse) > 200 else f"📝 Résultat: {resultat_analyse}")
|
||||
|
||||
return VWBActionResult(
|
||||
action_id=self.action_id,
|
||||
step_id=step_id,
|
||||
status=VWBActionStatus.SUCCESS,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
execution_time_ms=execution_time,
|
||||
output_data={
|
||||
'analyse': resultat_analyse,
|
||||
'variable_sortie': self.variable_sortie,
|
||||
'type_analyse': self.type_analyse,
|
||||
'model': self.ollama_model,
|
||||
'prompt_utilise': prompt_final[:100] + '...' if len(prompt_final) > 100 else prompt_final
|
||||
},
|
||||
evidence_list=self.evidence_list.copy()
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return self._create_error_result(
|
||||
step_id=step_id,
|
||||
start_time=start_time,
|
||||
error_type=VWBErrorType.SYSTEM_ERROR,
|
||||
message=f"Erreur: {str(e)}",
|
||||
technical_details={'exception': str(e)}
|
||||
)
|
||||
|
||||
def _capturer_image(self) -> Optional[str]:
|
||||
"""
|
||||
Capture l'image à analyser.
|
||||
|
||||
Returns:
|
||||
Image en base64 ou None
|
||||
"""
|
||||
try:
|
||||
# Option 1: Image depuis l'ancre visuelle
|
||||
if self.ancre_visuelle:
|
||||
if isinstance(self.ancre_visuelle, dict):
|
||||
img = self.ancre_visuelle.get('screenshot') or self.ancre_visuelle.get('image_base64')
|
||||
if img:
|
||||
return img
|
||||
elif isinstance(self.ancre_visuelle, VWBVisualAnchor):
|
||||
if self.ancre_visuelle.screenshot_base64:
|
||||
return self.ancre_visuelle.screenshot_base64
|
||||
|
||||
# Option 2: Capture d'une région spécifique
|
||||
if self.region and self.screen_capturer:
|
||||
return self._capturer_region(self.region)
|
||||
|
||||
# Option 3: Capture de l'écran entier
|
||||
if self.screen_capturer:
|
||||
return self._capturer_ecran_complet()
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Erreur capture: {e}")
|
||||
return None
|
||||
|
||||
def _capturer_region(self, region: Dict[str, int]) -> Optional[str]:
|
||||
"""Capture une région spécifique de l'écran."""
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
# Capturer l'écran entier
|
||||
img_array = self.screen_capturer.capture()
|
||||
if img_array is None:
|
||||
return None
|
||||
|
||||
# Convertir en PIL et découper
|
||||
pil_image = Image.fromarray(img_array)
|
||||
|
||||
x = region.get('x', 0)
|
||||
y = region.get('y', 0)
|
||||
width = region.get('width', 100)
|
||||
height = region.get('height', 100)
|
||||
|
||||
cropped = pil_image.crop((x, y, x + width, y + height))
|
||||
|
||||
# Convertir en base64
|
||||
buffer = io.BytesIO()
|
||||
cropped.save(buffer, format='PNG')
|
||||
return base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Erreur capture région: {e}")
|
||||
return None
|
||||
|
||||
def _capturer_ecran_complet(self) -> Optional[str]:
|
||||
"""Capture l'écran entier."""
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
img_array = self.screen_capturer.capture()
|
||||
if img_array is None:
|
||||
return None
|
||||
|
||||
pil_image = Image.fromarray(img_array)
|
||||
|
||||
buffer = io.BytesIO()
|
||||
pil_image.save(buffer, format='PNG', optimize=True)
|
||||
return base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Erreur capture écran: {e}")
|
||||
return None
|
||||
|
||||
def _construire_prompt(self) -> str:
|
||||
"""
|
||||
Construit le prompt final pour l'analyse.
|
||||
|
||||
Returns:
|
||||
Prompt complet
|
||||
"""
|
||||
# Utiliser le prompt personnalisé si fourni
|
||||
if self.prompt:
|
||||
prompt_base = self.prompt
|
||||
# Sinon utiliser le prompt prédéfini selon le type
|
||||
elif self.type_analyse in self.prompts_predefinifs:
|
||||
prompt_base = self.prompts_predefinifs[self.type_analyse]
|
||||
else:
|
||||
prompt_base = self.prompts_predefinifs['general']
|
||||
|
||||
# Ajouter le prompt système si fourni
|
||||
if self.prompt_systeme:
|
||||
return f"{self.prompt_systeme}\n\n{prompt_base}"
|
||||
|
||||
return prompt_base
|
||||
|
||||
def _analyser_avec_ollama(self, image_base64: str, prompt: str) -> Optional[str]:
|
||||
"""
|
||||
Envoie l'image à Ollama pour analyse.
|
||||
|
||||
Args:
|
||||
image_base64: Image en base64
|
||||
prompt: Prompt d'analyse
|
||||
|
||||
Returns:
|
||||
Texte de l'analyse ou None
|
||||
"""
|
||||
try:
|
||||
# Préparer la requête
|
||||
payload = {
|
||||
"model": self.ollama_model,
|
||||
"prompt": prompt,
|
||||
"images": [image_base64],
|
||||
"stream": False,
|
||||
"options": {
|
||||
"temperature": self.temperature,
|
||||
"num_predict": self.max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
# Appeler l'API Ollama
|
||||
response = requests.post(
|
||||
f"{self.ollama_url}/api/generate",
|
||||
json=payload,
|
||||
timeout=self.timeout_ms / 1000
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return result.get('response', '').strip()
|
||||
else:
|
||||
print(f"⚠️ Erreur Ollama: {response.status_code} - {response.text[:200]}")
|
||||
return None
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
print(f"⚠️ Timeout Ollama après {self.timeout_ms}ms")
|
||||
return None
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"⚠️ Ollama non accessible à {self.ollama_url}")
|
||||
return self._fallback_analyse(prompt)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Erreur Ollama: {e}")
|
||||
return None
|
||||
|
||||
def _fallback_analyse(self, prompt: str) -> Optional[str]:
|
||||
"""
|
||||
Fallback si Ollama n'est pas disponible.
|
||||
|
||||
Args:
|
||||
prompt: Prompt original
|
||||
|
||||
Returns:
|
||||
Message d'erreur informatif
|
||||
"""
|
||||
return f"[Analyse IA non disponible - Ollama non accessible]\nPrompt demandé: {prompt[:100]}..."
|
||||
|
||||
def get_action_info(self) -> Dict[str, Any]:
|
||||
"""Retourne les informations de l'action."""
|
||||
return {
|
||||
'action_id': self.action_id,
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'type': 'analyser_avec_ia',
|
||||
'parameters': {
|
||||
'type_analyse': self.type_analyse,
|
||||
'prompt': self.prompt[:50] + '...' if len(self.prompt) > 50 else self.prompt,
|
||||
'model': self.ollama_model,
|
||||
'variable_sortie': self.variable_sortie
|
||||
},
|
||||
'status': self.current_status.value
|
||||
}
|
||||
|
||||
|
||||
# Alias pour compatibilité avec le catalogue anglais
|
||||
VWBAIAnalyzeTextAction = VWBAnalyserAvecIAAction
|
||||
Reference in New Issue
Block a user