feat: import Excel via chat Léa, suppression nœuds VWB, fix temperature 0.1

- Chat Léa : "importe patients.xlsx" → preview → confirmation → table SQLite
  Bouton 📎 pour upload fichier, "montre les tables", "info table X"
- VWB : suppression nœuds via touche Suppr/Backspace + bouton croix rouge
- Fix : toutes les températures VLM à 0.1 (qwen3-vl bloque à 0.0)
- Fix : capture VWB avec DISPLAY=:1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-03-17 07:18:51 +01:00
parent 97cb2957d5
commit 928b9e1065
8 changed files with 820 additions and 58 deletions

View File

@@ -65,8 +65,13 @@ class OllamaClient:
max_tokens: int = 500,
force_json: bool = False) -> Dict[str, Any]:
"""
Générer une réponse du VLM
Générer une réponse du VLM via l'API chat d'Ollama.
Note: On utilise /api/chat au lieu de /api/generate car qwen3-vl
avec /api/generate consomme tous les tokens en thinking interne
et retourne une réponse vide. L'API chat gère correctement
le mode /no_think et sépare thinking/réponse.
Args:
prompt: Prompt textuel
image_path: Chemin vers une image (optionnel)
@@ -74,7 +79,8 @@ class OllamaClient:
system_prompt: Prompt système (optionnel)
temperature: Température de génération
max_tokens: Nombre max de tokens
force_json: Forcer la sortie JSON (non recommandé pour qwen3-vl)
Returns:
Dict avec 'response', 'success', 'error'
"""
@@ -85,46 +91,52 @@ class OllamaClient:
image_data = self._encode_image_from_path(image_path)
elif image:
image_data = self._encode_image_from_pil(image)
# Construire la requête avec thinking mode désactivé
# Pour Qwen3, utiliser /nothink au début du prompt
# Construire le prompt avec /no_think pour désactiver le thinking
effective_prompt = prompt
if "qwen" in self.model.lower():
effective_prompt = f"/nothink {prompt}"
# S'assurer que /no_think est présent (pas de doublon)
if "/no_think" not in prompt and "/nothink" not in prompt:
effective_prompt = f"/no_think\n{prompt}"
# Construire le message utilisateur
user_message = {"role": "user", "content": effective_prompt}
if image_data:
user_message["images"] = [image_data]
# Construire les messages
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append(user_message)
payload = {
"model": self.model,
"prompt": effective_prompt,
"messages": messages,
"stream": False,
"options": {
"temperature": temperature,
"num_predict": max_tokens,
"num_ctx": 2048, # Contexte réduit pour plus de vitesse
"top_k": 1 # Plus rapide pour les tâches de classification
"num_ctx": 2048,
"top_k": 1
}
}
# Forcer la sortie JSON si demandé (réduit drastiquement les erreurs de parsing)
if force_json:
payload["format"] = "json"
if system_prompt:
payload["system"] = system_prompt
if image_data:
payload["images"] = [image_data]
# Envoyer la requête
# Envoyer la requête via l'API chat
response = requests.post(
f"{self.endpoint}/api/generate",
f"{self.endpoint}/api/chat",
json=payload,
timeout=self.timeout
)
if response.status_code == 200:
result = response.json()
content = result.get("message", {}).get("content", "")
return {
"response": result.get("response", ""),
"response": content,
"success": True,
"error": None
}
@@ -134,7 +146,7 @@ class OllamaClient:
"success": False,
"error": f"HTTP {response.status_code}: {response.text}"
}
except Exception as e:
return {
"response": "",
@@ -197,7 +209,7 @@ Respond with just the type name, nothing else."""
if context:
prompt += f"\n\nContext: {context}"
result = self.generate(prompt, image=element_image, temperature=0.0)
result = self.generate(prompt, image=element_image, temperature=0.1)
if result["success"]:
element_type = result["response"].strip().lower()
@@ -238,7 +250,7 @@ Respond with just the role name, nothing else."""
if context:
prompt += f"\n\nContext: {context}"
result = self.generate(prompt, image=element_image, temperature=0.0)
result = self.generate(prompt, image=element_image, temperature=0.1)
if result["success"]:
role = result["response"].strip().lower()
@@ -266,7 +278,7 @@ Respond with just the role name, nothing else."""
"""
prompt = "Extract all visible text from this image. Return only the text, nothing else."
result = self.generate(prompt, image=image, temperature=0.0)
result = self.generate(prompt, image=image, temperature=0.1)
if result["success"]:
return {"text": result["response"].strip(), "success": True}
@@ -288,30 +300,26 @@ Respond with just the role name, nothing else."""
Returns:
Dict avec 'type', 'role', 'text', 'confidence', 'success'
"""
# System prompt direct — pas de thinking, JSON uniquement
system_prompt = "You are a JSON-only UI classifier. No thinking. No explanation. Output raw JSON only."
# User prompt avec exemples explicites pour guider le modèle
# Prompt concis sans system prompt — le system prompt avec qwen3-vl
# augmente considérablement le nombre de tokens de thinking interne,
# causant des réponses vides quand le budget tokens est trop bas.
prompt = """/no_think
Look at this UI element image and classify it. Reply with ONLY a JSON object, nothing else.
Classify this UI element. Reply with ONLY a JSON object, nothing else.
Types: button, text_input, checkbox, radio, dropdown, tab, link, icon, table_row, menu_item
Roles: primary_action, cancel, submit, form_input, search_field, navigation, settings, close, delete, edit, save
Example 1: {"type": "button", "role": "submit", "text": "OK"}
Example 2: {"type": "text_input", "role": "form_input", "text": ""}
Example 3: {"type": "icon", "role": "close", "text": "X"}
Example: {"type": "button", "role": "submit", "text": "OK"}
Your answer:"""
# Note: force_json=False car qwen3-vl ne supporte pas format:json
# temperature=0.1 car qwen3-vl bloque à 0.0 avec des images
# max_tokens=800 car qwen3-vl consomme 300-700 tokens en thinking
# interne même avec /no_think — les images complexes nécessitent
# plus de budget pour que la réponse JSON visible soit complète
result = self.generate(
prompt,
image=element_image,
system_prompt=system_prompt,
temperature=0.1,
max_tokens=200,
max_tokens=800,
force_json=False
)