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:
@@ -30,8 +30,9 @@ from typing import Dict, Any, List, Optional
|
||||
|
||||
import requests as http_requests # Pour les appels au streaming server
|
||||
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
from flask import Flask, render_template, request, jsonify, send_from_directory
|
||||
from flask_socketio import SocketIO, emit
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
@@ -53,6 +54,14 @@ try:
|
||||
except ImportError:
|
||||
GPU_AVAILABLE = False
|
||||
|
||||
# Import de données Excel/CSV
|
||||
try:
|
||||
from core.data.excel_importer import ExcelImporter
|
||||
from core.data.db_iterator import DBIterator
|
||||
DATA_IMPORT_AVAILABLE = True
|
||||
except ImportError:
|
||||
DATA_IMPORT_AVAILABLE = False
|
||||
|
||||
# Execution components (optional - pour exécution réelle)
|
||||
try:
|
||||
from core.execution import ActionExecutor, TargetResolver, ErrorHandler
|
||||
@@ -101,6 +110,18 @@ execution_status = {
|
||||
}
|
||||
command_history: List[Dict[str, Any]] = []
|
||||
|
||||
# Répertoire d'upload et chemin de la base de données
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
UPLOAD_DIR = PROJECT_ROOT / "data" / "uploads"
|
||||
DEFAULT_DB_PATH = PROJECT_ROOT / "data" / "databases" / "rpa_data.db"
|
||||
|
||||
# Import de données — composants globaux
|
||||
excel_importer: Optional['ExcelImporter'] = None
|
||||
db_iterator: Optional['DBIterator'] = None
|
||||
|
||||
# État d'import en attente de confirmation (session_id → infos d'import)
|
||||
_pending_imports: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
# Copilot state — suivi du mode pas-à-pas
|
||||
_copilot_sessions: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
@@ -246,6 +267,21 @@ def init_system():
|
||||
logger.warning(f"⚠ GestureCatalog: {e}")
|
||||
gesture_catalog = None
|
||||
|
||||
# 7. Import de données Excel/CSV
|
||||
global excel_importer, db_iterator
|
||||
if DATA_IMPORT_AVAILABLE:
|
||||
try:
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
excel_importer = ExcelImporter(db_path=str(DEFAULT_DB_PATH))
|
||||
db_iterator = DBIterator(db_path=str(DEFAULT_DB_PATH))
|
||||
logger.info(f"✓ ExcelImporter + DBIterator (db: {DEFAULT_DB_PATH})")
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠ Data import: {e}")
|
||||
excel_importer = None
|
||||
db_iterator = None
|
||||
else:
|
||||
logger.info("ℹ Import Excel non disponible (openpyxl manquant ?)")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Routes Web
|
||||
@@ -489,22 +525,30 @@ def api_chat():
|
||||
action_taken = None
|
||||
|
||||
if intent.intent_type == IntentType.CONFIRM:
|
||||
# Confirmer une action en attente
|
||||
pending = conversation_manager.get_pending_confirmation(session)
|
||||
if pending:
|
||||
confirmation_loop.confirm(pending.id)
|
||||
conversation_manager.clear_pending_confirmation(session)
|
||||
result = {"confirmed": True, "workflow": pending.workflow_name}
|
||||
action_taken = "confirmed"
|
||||
|
||||
# Lancer l'exécution
|
||||
socketio.start_background_task(
|
||||
execute_workflow_from_confirmation, pending, session.session_id
|
||||
)
|
||||
# Vérifier s'il y a un import en attente de confirmation
|
||||
pending_import = _pending_imports.pop(session.session_id, None)
|
||||
if pending_import:
|
||||
result = _execute_pending_import(pending_import)
|
||||
action_taken = "data_import"
|
||||
else:
|
||||
result = {"confirmed": False}
|
||||
# Confirmer une action en attente (workflow)
|
||||
pending = conversation_manager.get_pending_confirmation(session)
|
||||
if pending:
|
||||
confirmation_loop.confirm(pending.id)
|
||||
conversation_manager.clear_pending_confirmation(session)
|
||||
result = {"confirmed": True, "workflow": pending.workflow_name}
|
||||
action_taken = "confirmed"
|
||||
|
||||
# Lancer l'exécution
|
||||
socketio.start_background_task(
|
||||
execute_workflow_from_confirmation, pending, session.session_id
|
||||
)
|
||||
else:
|
||||
result = {"confirmed": False}
|
||||
|
||||
elif intent.intent_type == IntentType.DENY:
|
||||
# Annuler un import en attente
|
||||
_pending_imports.pop(session.session_id, None)
|
||||
# Refuser une action en attente
|
||||
pending = conversation_manager.get_pending_confirmation(session)
|
||||
if pending:
|
||||
@@ -594,6 +638,11 @@ def api_chat():
|
||||
# Niveau 3 : Pas de query exploitable
|
||||
result = {"not_found": True, "query": query or "", "teach_me": True}
|
||||
|
||||
elif intent.intent_type == IntentType.DATA_IMPORT:
|
||||
# Import de données Excel/CSV
|
||||
result = _handle_data_import(intent, session.session_id)
|
||||
action_taken = "data_import"
|
||||
|
||||
elif intent.intent_type == IntentType.LIST:
|
||||
# Lister les workflows avec métadonnées enrichies
|
||||
if matcher:
|
||||
@@ -669,7 +718,14 @@ def api_chat():
|
||||
|
||||
# 6. Générer la réponse (si pas déjà fait pour confirmation)
|
||||
if action_taken != "confirmation_requested":
|
||||
response = response_generator.generate(intent, context, result)
|
||||
# Si c'est un import de données (même via CONFIRM/DENY), forcer le handler DATA_IMPORT
|
||||
if action_taken == "data_import":
|
||||
# Créer un intent fictif pour le dispatch vers _handle_data_import
|
||||
from dataclasses import replace as _dc_replace
|
||||
data_intent = _dc_replace(intent, intent_type=IntentType.DATA_IMPORT)
|
||||
response = response_generator.generate(data_intent, context, result)
|
||||
else:
|
||||
response = response_generator.generate(intent, context, result)
|
||||
|
||||
# 7. Enregistrer le tour dans la conversation
|
||||
conversation_manager.add_turn(
|
||||
@@ -893,6 +949,303 @@ def _execute_gesture(gesture):
|
||||
})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Import de données Excel/CSV
|
||||
# =============================================================================
|
||||
|
||||
def _handle_data_import(intent, session_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Traiter une intention DATA_IMPORT.
|
||||
|
||||
Gère les actions : import_file, import_folder, list_tables, table_info.
|
||||
"""
|
||||
if not DATA_IMPORT_AVAILABLE or not excel_importer:
|
||||
return {"error": "L'import Excel n'est pas disponible (openpyxl manquant)."}
|
||||
|
||||
action = intent.parameters.get("action", "import_file")
|
||||
params = intent.parameters
|
||||
|
||||
if action == "list_tables":
|
||||
return _data_list_tables()
|
||||
|
||||
elif action == "table_info":
|
||||
table_name = params.get("table_name")
|
||||
if not table_name:
|
||||
return {"error": "Précisez le nom de la table (ex: 'info table patients')."}
|
||||
return _data_table_info(table_name)
|
||||
|
||||
elif action == "import_folder":
|
||||
folder_path = params.get("folder_path") or params.get("file_path", "")
|
||||
if not folder_path:
|
||||
return {"error": "Précisez le chemin du dossier."}
|
||||
return _data_import_folder(folder_path)
|
||||
|
||||
elif action == "import_file":
|
||||
file_path = params.get("file_path")
|
||||
if not file_path:
|
||||
return {"error": "Précisez le fichier à importer (ex: 'importe patients.xlsx')."}
|
||||
table_name = params.get("table_name")
|
||||
return _data_preview_file(file_path, table_name, session_id)
|
||||
|
||||
return {"error": "Action d'import non reconnue."}
|
||||
|
||||
|
||||
def _resolve_file_path(file_path: str) -> Optional[Path]:
|
||||
"""
|
||||
Résoudre un chemin de fichier.
|
||||
|
||||
Cherche dans l'ordre :
|
||||
1. Chemin absolu tel quel
|
||||
2. Relatif au dossier d'upload
|
||||
3. Relatif à la racine du projet
|
||||
"""
|
||||
p = Path(file_path)
|
||||
|
||||
# Chemin absolu
|
||||
if p.is_absolute() and p.exists():
|
||||
return p
|
||||
|
||||
# Relatif au dossier d'upload
|
||||
in_uploads = UPLOAD_DIR / p.name
|
||||
if in_uploads.exists():
|
||||
return in_uploads
|
||||
|
||||
# Relatif à la racine du projet
|
||||
in_project = PROJECT_ROOT / file_path
|
||||
if in_project.exists():
|
||||
return in_project
|
||||
|
||||
# Juste le nom du fichier dans uploads
|
||||
if not p.is_absolute():
|
||||
in_uploads2 = UPLOAD_DIR / file_path
|
||||
if in_uploads2.exists():
|
||||
return in_uploads2
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _data_preview_file(file_path: str, table_name: Optional[str], session_id: str) -> Dict[str, Any]:
|
||||
"""Prévisualiser un fichier Excel avant import."""
|
||||
resolved = _resolve_file_path(file_path)
|
||||
if not resolved:
|
||||
return {"file_not_found": True, "file_path": file_path}
|
||||
|
||||
try:
|
||||
preview = excel_importer.preview(str(resolved))
|
||||
# Déterminer le nom de la table
|
||||
if not table_name:
|
||||
table_name = resolved.stem.lower().replace(" ", "_").replace("-", "_")
|
||||
|
||||
# Stocker l'import en attente pour cette session
|
||||
_pending_imports[session_id] = {
|
||||
"file_path": str(resolved),
|
||||
"table_name": table_name,
|
||||
"sheet_name": preview.sheet_name,
|
||||
}
|
||||
|
||||
return {
|
||||
"preview": {
|
||||
"columns": preview.headers,
|
||||
"total_rows": preview.total_rows,
|
||||
"sheet_name": preview.sheet_name,
|
||||
"detected_types": preview.detected_types,
|
||||
"sample_rows": [
|
||||
[str(v) if v is not None else "" for v in row]
|
||||
for row in preview.rows[:3]
|
||||
],
|
||||
},
|
||||
"filename": resolved.name,
|
||||
"table_name": table_name,
|
||||
"needs_confirmation": True,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur prévisualisation {file_path}: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def _execute_pending_import(pending: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Exécuter un import après confirmation utilisateur."""
|
||||
if not excel_importer:
|
||||
return {"error": "ExcelImporter non disponible."}
|
||||
|
||||
try:
|
||||
result = excel_importer.import_file(
|
||||
excel_path=pending["file_path"],
|
||||
table_name=pending.get("table_name"),
|
||||
sheet_name=pending.get("sheet_name"),
|
||||
)
|
||||
|
||||
if result.success:
|
||||
return {
|
||||
"imported": {
|
||||
"table_name": result.table_name,
|
||||
"row_count": result.row_count,
|
||||
"column_count": result.column_count,
|
||||
"columns": result.columns,
|
||||
"sheet_name": result.sheet_name,
|
||||
"db_path": result.db_path,
|
||||
}
|
||||
}
|
||||
else:
|
||||
return {"error": "; ".join(result.errors) or "Import échoué (0 lignes)."}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur import: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def _data_list_tables() -> Dict[str, Any]:
|
||||
"""Lister les tables disponibles dans la base."""
|
||||
if not db_iterator:
|
||||
return {"tables_list": []}
|
||||
|
||||
try:
|
||||
tables = db_iterator.list_tables()
|
||||
tables_info = []
|
||||
for t in tables:
|
||||
try:
|
||||
count = db_iterator.count(t)
|
||||
except Exception:
|
||||
count = -1
|
||||
tables_info.append({"name": t, "row_count": count})
|
||||
|
||||
return {"tables_list": tables_info}
|
||||
except FileNotFoundError:
|
||||
# Base pas encore créée — aucune table
|
||||
return {"tables_list": []}
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur list_tables: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def _data_table_info(table_name: str) -> Dict[str, Any]:
|
||||
"""Récupérer les infos d'une table."""
|
||||
if not db_iterator:
|
||||
return {"error": "DBIterator non disponible."}
|
||||
|
||||
try:
|
||||
columns = db_iterator.get_columns(table_name)
|
||||
count = db_iterator.count(table_name)
|
||||
|
||||
return {
|
||||
"table_info": {
|
||||
"table_name": table_name,
|
||||
"columns": columns,
|
||||
"row_count": count,
|
||||
}
|
||||
}
|
||||
except FileNotFoundError:
|
||||
return {"error": f"Aucune base de données trouvée."}
|
||||
except Exception as e:
|
||||
# Table n'existe probablement pas
|
||||
return {"error": f"Table '{table_name}' introuvable : {e}"}
|
||||
|
||||
|
||||
def _data_import_folder(folder_path: str) -> Dict[str, Any]:
|
||||
"""Lister les fichiers Excel dans un dossier."""
|
||||
p = Path(folder_path)
|
||||
if not p.exists() or not p.is_dir():
|
||||
return {"folder_files": [], "folder": folder_path}
|
||||
|
||||
excel_files = sorted(
|
||||
[f.name for f in p.iterdir() if f.suffix.lower() in (".xlsx", ".xls", ".csv")]
|
||||
)
|
||||
|
||||
return {
|
||||
"folder_files": excel_files,
|
||||
"folder": folder_path,
|
||||
}
|
||||
|
||||
|
||||
@app.route('/api/chat/upload', methods=['POST'])
|
||||
def api_chat_upload():
|
||||
"""
|
||||
Upload d'un fichier Excel/CSV via le chat.
|
||||
|
||||
Le fichier est stocké dans data/uploads/ et un aperçu est retourné.
|
||||
"""
|
||||
if 'file' not in request.files:
|
||||
return jsonify({"error": "Aucun fichier reçu."}), 400
|
||||
|
||||
file = request.files['file']
|
||||
if not file.filename:
|
||||
return jsonify({"error": "Nom de fichier vide."}), 400
|
||||
|
||||
# Vérifier l'extension
|
||||
allowed_ext = {'.xlsx', '.xls', '.csv'}
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
if ext not in allowed_ext:
|
||||
return jsonify({
|
||||
"error": f"Format '{ext}' non supporté. Formats acceptés : {', '.join(allowed_ext)}"
|
||||
}), 400
|
||||
|
||||
# Sauvegarder le fichier
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
filename = secure_filename(file.filename)
|
||||
# Ajouter un timestamp pour éviter les collisions
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
safe_name = f"{Path(filename).stem}_{ts}{ext}"
|
||||
save_path = UPLOAD_DIR / safe_name
|
||||
file.save(str(save_path))
|
||||
|
||||
logger.info(f"Fichier uploadé : {save_path}")
|
||||
|
||||
# Retourner les infos pour que le frontend envoie un message d'import
|
||||
session_id = request.form.get('session_id', '')
|
||||
|
||||
# Prévisualiser le fichier
|
||||
if DATA_IMPORT_AVAILABLE and excel_importer and ext in ('.xlsx', '.xls'):
|
||||
try:
|
||||
preview = excel_importer.preview(str(save_path))
|
||||
table_name = Path(filename).stem.lower().replace(" ", "_").replace("-", "_")
|
||||
|
||||
# Stocker l'import en attente
|
||||
_pending_imports[session_id] = {
|
||||
"file_path": str(save_path),
|
||||
"table_name": table_name,
|
||||
"sheet_name": preview.sheet_name,
|
||||
}
|
||||
|
||||
cols_str = ", ".join(preview.headers[:8])
|
||||
if len(preview.headers) > 8:
|
||||
cols_str += f"... (+{len(preview.headers) - 8})"
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"filename": file.filename,
|
||||
"saved_as": safe_name,
|
||||
"path": str(save_path),
|
||||
"preview": {
|
||||
"columns": preview.headers,
|
||||
"total_rows": preview.total_rows,
|
||||
"sheet_name": preview.sheet_name,
|
||||
},
|
||||
"message": (
|
||||
f"Fichier **{file.filename}** recu ! "
|
||||
f"{preview.total_rows} lignes, colonnes : {cols_str}. "
|
||||
f"Je cree la table '{table_name}' ?"
|
||||
),
|
||||
"needs_confirmation": True,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur preview upload: {e}")
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"filename": file.filename,
|
||||
"saved_as": safe_name,
|
||||
"path": str(save_path),
|
||||
"error": f"Fichier sauvegardé mais erreur de lecture : {e}",
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"filename": file.filename,
|
||||
"saved_as": safe_name,
|
||||
"path": str(save_path),
|
||||
"message": f"Fichier **{file.filename}** sauvegardé dans {UPLOAD_DIR}.",
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/help')
|
||||
def api_help():
|
||||
"""Aide et mode d'emploi."""
|
||||
|
||||
Reference in New Issue
Block a user