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

@@ -36,6 +36,7 @@ class IntentType(Enum):
CONFIRM = "confirm" # Confirmer une action
DENY = "deny" # Refuser une action
CLARIFY = "clarify" # Demander une clarification
DATA_IMPORT = "data_import" # Importer des données (Excel, CSV)
UNKNOWN = "unknown" # Intention non reconnue
@@ -74,6 +75,21 @@ class IntentParser:
# Patterns pour la détection d'intentions par règles
INTENT_PATTERNS = {
IntentType.DATA_IMPORT: [
# Import de fichiers Excel/CSV
r"(?:importe|charge|lis|lire)\s+(?:le\s+fichier\s+|les\s+(?:données|feuilles)\s+(?:de|du|excel\s+du)\s+)?(.+\.xlsx?)\b",
r"(?:importe|charge|lis|lire)\s+(?:le\s+fichier\s+|les\s+(?:données|feuilles)\s+(?:de|du|excel\s+du)\s+)?(.+\.csv)\b",
r"(?:importe|charge|lis|lire)\s+(?:le\s+fichier\s+)?excel\s+(.+)",
r"(?:importe|charge|lis|lire)\s+(?:les\s+)?(?:feuilles?\s+)?excel\s+(?:du\s+dossier\s+|de\s+)(.+)",
r"(?:crée?|créer?)\s+une?\s+table\s+(?:à\s+partir\s+d[eu]'?\s*)(.+\.xlsx?)\b",
# Lister les tables
r"(?:montre|liste|affiche|voir)\s*(?:-moi\s+)?(?:les\s+)?tables?\b",
r"(?:quelles?\s+)?tables?\s+(?:sont\s+)?(?:disponibles?|dans\s+la\s+base)",
r"liste\s+(?:des?\s+)?tables?\s+(?:de\s+)?(?:la\s+)?(?:base)?",
# Infos sur une table
r"(?:combien\s+de\s+lignes?\s+(?:dans|pour)\s+(?:la\s+)?table\s+)(\w+)",
r"(?:info|détails?|describe?|colonnes?|structure)\s+(?:de\s+)?(?:la\s+)?table\s+(\w+)",
],
IntentType.EXECUTE: [
# Verbes d'action explicites
r"(?:lance|exécute|démarre|fai[st]|run|start|execute)\s+(.+)",
@@ -210,6 +226,25 @@ class IntentParser:
# Expressions mathématiques : 5+2, 100*3, 12/4, 7-3, 2.5+3.1
r"(\d+(?:[.,]\d+)?\s*[+\-*/x×÷]\s*\d+(?:[.,]\d+)?)",
],
"file_path": [
# Chemins Windows : C:\data\fichier.xlsx
r"([A-Za-z]:\\[^\s,]+\.(?:xlsx?|csv))",
# Chemins Unix : /data/fichier.xlsx
r"(/[^\s,]+\.(?:xlsx?|csv))",
# Noms de fichier simples : patients.xlsx
r"(?:^|\s)([\w\-\.]+\.(?:xlsx?|csv))(?:\s|$)",
],
"folder_path": [
# Dossiers Windows : C:\data\imports
r"(?:dossier|répertoire|dir|directory)\s+([A-Za-z]:\\[^\s,]+)",
r"([A-Za-z]:\\[^\s,]+)(?:\s|$)",
# Dossiers Unix : /data/imports
r"(?:dossier|répertoire|dir|directory)\s+(/[^\s,]+)",
],
"table_name": [
# Noms de table (exclure les mots courants comme "à", "de", "la")
r"(?:table|la\s+table)\s+['\"]?(\w{2,})['\"]?",
],
}
def __init__(
@@ -294,6 +329,10 @@ class IntentParser:
# 4. Construire les paramètres depuis les entités
parameters = self._entities_to_parameters(entities)
# 4b. Enrichir les paramètres DATA_IMPORT avec l'action et le chemin
if intent_type == IntentType.DATA_IMPORT:
parameters = self._enrich_data_import_params(normalized, query, parameters, entities)
# 5. Si le LLM est disponible et la confiance est basse, utiliser le LLM
if self.use_llm and self.llm_available and rule_confidence < 0.7:
llm_result = self._parse_with_llm(query, context)
@@ -316,6 +355,82 @@ class IntentParser:
clarification_question=clarification_question
)
def _enrich_data_import_params(
self,
normalized: str,
raw_query: str,
parameters: Dict[str, Any],
entities: List[Dict[str, Any]],
) -> Dict[str, Any]:
"""Enrichir les paramètres pour une intention DATA_IMPORT.
Détermine l'action (import_file, import_folder, list_tables, table_info)
et extrait le chemin de fichier / nom de table.
"""
# Déterminer l'action
list_patterns = [
r"(?:montre|liste|affiche|voir)\s*(?:-moi\s+)?(?:les\s+)?tables?",
r"(?:quelles?\s+)?tables?\s+(?:sont\s+)?(?:disponibles?|dans)",
r"liste\s+(?:des?\s+)?tables?",
]
info_patterns = [
r"combien\s+de\s+lignes?",
r"(?:info|détails?|describe?|colonnes?|structure)\s+(?:de\s+)?(?:la\s+)?table",
]
folder_patterns = [
r"(?:feuilles?\s+excel|fichiers?\s+excel)\s+(?:du|de)\s+(?:dossier|répertoire)",
r"(?:importe|charge|lis)\s+(?:les\s+)?(?:feuilles?\s+)?excel\s+(?:du\s+dossier|de\s+)",
]
action = "import_file" # Par défaut
for pat in list_patterns:
if re.search(pat, normalized, re.IGNORECASE):
action = "list_tables"
break
if action == "import_file":
for pat in info_patterns:
if re.search(pat, normalized, re.IGNORECASE):
action = "table_info"
break
if action == "import_file":
for pat in folder_patterns:
if re.search(pat, normalized, re.IGNORECASE):
action = "import_folder"
break
parameters["action"] = action
# Extraire le chemin de fichier depuis les entités
for entity in entities:
if entity["type"] == "file_path" and "file_path" not in parameters:
parameters["file_path"] = entity["value"]
elif entity["type"] == "folder_path" and "folder_path" not in parameters:
parameters["folder_path"] = entity["value"]
elif entity["type"] == "table_name" and "table_name" not in parameters:
parameters["table_name"] = entity["value"]
# Fallback : extraire un chemin de fichier depuis la requête brute
if "file_path" not in parameters and action == "import_file":
# Chercher un .xlsx/.xls/.csv dans la requête brute (supporte les chemins Windows)
fp_match = re.search(
r'([A-Za-z]:\\[^\s,]+\.(?:xlsx?|csv)|/[^\s,]+\.(?:xlsx?|csv)|[\w\-\.]+\.(?:xlsx?|csv))',
raw_query,
re.IGNORECASE,
)
if fp_match:
parameters["file_path"] = fp_match.group(1)
# Extraire table_name pour table_info depuis la requête
if action == "table_info" and "table_name" not in parameters:
tm = re.search(r"table\s+['\"]?(\w+)['\"]?", normalized, re.IGNORECASE)
if tm:
parameters["table_name"] = tm.group(1)
return parameters
def _normalize_query(self, query: str) -> str:
"""Normaliser une requête pour le matching."""
# Convertir en minuscules