diff --git a/agent_chat/app.py b/agent_chat/app.py index 892dfdce5..13e2e0a46 100644 --- a/agent_chat/app.py +++ b/agent_chat/app.py @@ -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.""" diff --git a/agent_chat/intent_parser.py b/agent_chat/intent_parser.py index 5e4cfd6b3..85ac85bba 100644 --- a/agent_chat/intent_parser.py +++ b/agent_chat/intent_parser.py @@ -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 diff --git a/agent_chat/response_generator.py b/agent_chat/response_generator.py index 40e082f52..9cbb7b522 100644 --- a/agent_chat/response_generator.py +++ b/agent_chat/response_generator.py @@ -180,6 +180,44 @@ class ResponseGenerator: "{question}", ] }, + IntentType.DATA_IMPORT: { + "preview": [ + "J'ai trouvé le fichier **{filename}** — {total_rows} lignes, colonnes : {columns}. Je l'importe dans la table '{table_name}' ?", + "Fichier **{filename}** prêt : {total_rows} lignes avec les colonnes {columns}. On crée la table '{table_name}' ?", + ], + "imported": [ + "Table **'{table_name}'** créée avec {row_count} lignes et {col_count} colonnes ({columns}). Vous pouvez maintenant utiliser 'Pour chaque ligne' dans un workflow !", + "Import réussi ! Table **'{table_name}'** : {row_count} lignes, {col_count} colonnes ({columns}).", + ], + "list_tables": [ + "Voici les tables disponibles :\n{tables_list}", + "Tables dans la base :\n{tables_list}", + ], + "no_tables": [ + "Aucune table n'a été importée pour l'instant. Envoyez-moi un fichier Excel pour commencer !", + "La base est vide. Importez un fichier Excel pour créer votre première table.", + ], + "table_info": [ + "La table **'{table_name}'** contient {row_count} lignes et {col_count} colonnes :\n{columns_detail}", + ], + "folder_list": [ + "J'ai trouvé {count} fichiers Excel dans le dossier :\n{files_list}\n\nDites-moi lequel importer !", + ], + "folder_empty": [ + "Aucun fichier Excel trouvé dans le dossier '{folder}'. Vérifiez le chemin.", + ], + "file_not_found": [ + "Je n'ai pas trouvé le fichier '{file_path}'. Vérifiez le chemin ou envoyez-le via le bouton 📎.", + "Fichier introuvable : '{file_path}'. Vous pouvez aussi glisser un fichier dans le chat.", + ], + "error": [ + "Erreur lors de l'import : {error}", + "L'import a échoué : {error}", + ], + "uploaded": [ + "Fichier **{filename}** reçu ! Je l'analyse...", + ], + }, IntentType.UNKNOWN: { "default": [ "Je n'ai pas compris. Pouvez-vous reformuler ?", @@ -209,6 +247,15 @@ class ResponseGenerator: "facturer client X", "liste des workflows", "aide" + ], + "after_import": [ + "montre les tables", + "importer un autre fichier", + "aide" + ], + "after_table_list": [ + "importer un fichier Excel", + "aide" ] } @@ -623,6 +670,111 @@ class ResponseGenerator: action_required=False ) + def _handle_data_import( + self, + intent: ParsedIntent, + context: Dict[str, Any], + result: Dict[str, Any] + ) -> GeneratedResponse: + """Handler pour les imports de données (Excel/CSV).""" + templates = self.RESPONSE_TEMPLATES[IntentType.DATA_IMPORT] + + if result.get("file_not_found"): + template = random.choice(templates["file_not_found"]) + message = template.format(file_path=result.get("file_path", "?")) + suggestions = ["aide"] + + elif result.get("preview"): + # Aperçu avant import + template = random.choice(templates["preview"]) + preview = result["preview"] + cols_str = ", ".join(preview.get("columns", [])[:8]) + if len(preview.get("columns", [])) > 8: + cols_str += f"... (+{len(preview['columns']) - 8})" + message = template.format( + filename=result.get("filename", "?"), + total_rows=preview.get("total_rows", 0), + columns=cols_str, + table_name=result.get("table_name", "?"), + ) + suggestions = ["oui", "non"] + + elif result.get("imported"): + # Import réussi + template = random.choice(templates["imported"]) + imp = result["imported"] + cols_str = ", ".join(list(imp.get("columns", {}).keys())[:6]) + if len(imp.get("columns", {})) > 6: + cols_str += "..." + message = template.format( + table_name=imp.get("table_name", "?"), + row_count=imp.get("row_count", 0), + col_count=imp.get("column_count", 0), + columns=cols_str, + ) + suggestions = self.CONTEXTUAL_SUGGESTIONS["after_import"] + + elif result.get("tables_list") is not None: + tables = result["tables_list"] + if tables: + lines = [] + for t in tables: + lines.append(f" **{t['name']}** ({t['row_count']} lignes)") + template = random.choice(templates["list_tables"]) + message = template.format(tables_list="\n".join(lines)) + suggestions = self.CONTEXTUAL_SUGGESTIONS["after_table_list"] + else: + message = random.choice(templates["no_tables"]) + suggestions = ["importer un fichier Excel"] + + elif result.get("table_info"): + info = result["table_info"] + cols_detail = "\n".join( + f" {c['name']} ({c['type']})" for c in info.get("columns", []) + if c["name"] not in ("_rowid", "_imported_at") + ) + template = random.choice(templates["table_info"]) + message = template.format( + table_name=info.get("table_name", "?"), + row_count=info.get("row_count", 0), + col_count=len([c for c in info.get("columns", []) if c["name"] not in ("_rowid", "_imported_at")]), + columns_detail=cols_detail, + ) + suggestions = self.CONTEXTUAL_SUGGESTIONS["after_table_list"] + + elif result.get("folder_files") is not None: + files = result["folder_files"] + if files: + files_list = "\n".join(f" {f}" for f in files) + template = random.choice(templates["folder_list"]) + message = template.format(count=len(files), files_list=files_list) + else: + template = random.choice(templates["folder_empty"]) + message = template.format(folder=result.get("folder", "?")) + suggestions = ["aide"] + + elif result.get("uploaded"): + template = random.choice(templates["uploaded"]) + message = template.format(filename=result.get("filename", "?")) + suggestions = [] + + elif result.get("error"): + template = random.choice(templates["error"]) + message = template.format(error=result["error"]) + suggestions = self.CONTEXTUAL_SUGGESTIONS["after_error"] + + else: + message = "Je n'ai pas compris la demande d'import. Précisez le fichier ou dites 'montre les tables'." + suggestions = ["montre les tables", "aide"] + + return GeneratedResponse( + message=message, + suggestions=suggestions, + action_required=result.get("needs_confirmation", False), + action_type="data_import_confirm" if result.get("needs_confirmation") else None, + metadata=result, + ) + def _handle_unknown( self, intent: ParsedIntent, diff --git a/agent_chat/templates/chat.html b/agent_chat/templates/chat.html index f25ec2cdd..1673c4810 100644 --- a/agent_chat/templates/chat.html +++ b/agent_chat/templates/chat.html @@ -447,6 +447,26 @@ color: var(--text-muted); } + .attach-btn { + width: 48px; + height: 48px; + border-radius: 14px; + background: var(--bg-message-bot); + border: 1px solid var(--border); + color: var(--text-muted); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + transition: all 0.2s; + } + + .attach-btn:hover { + color: var(--primary); + border-color: var(--primary); + } + .send-btn { width: 48px; height: 48px; @@ -650,6 +670,10 @@
📋 Voir les workflows
Lister les workflows disponibles
+
+
📊 Importer des données
+
Importer un fichier Excel ou voir les tables existantes
+
@@ -657,6 +681,10 @@
+ +