""" Zentrales Filehandling-Modul für den Agentservice. Enthält alle Funktionen für das Verarbeiten von Dateien. Angepasst, um mit LucyDOMInterface als zentrale Datei-Autorität zu arbeiten. """ import os import logging import base64 import json import uuid from datetime import datetime from typing import Dict, Any, List, Optional, Tuple, Union, BinaryIO from io import BytesIO # Import BytesIO at the top level # Bibliotheken für Dateiverarbeitung try: import pandas as pd except ImportError: pd = None logger = logging.getLogger(__name__) # Custom exception für das File-Handling class FileProcessingError(Exception): """Basisklasse für Fehler bei der Dateiverarbeitung im AgentService.""" pass class FileExtractionError(FileProcessingError): """Fehler bei der Textextraktion aus Dateien.""" pass class FileAnalysisError(FileProcessingError): """Fehler bei der Analyse von Dateien.""" pass def encode_to_base64(content: bytes, mime_type: str = None) -> str: """ Kodiert Binärdaten als Base64-String. Args: content: Die zu kodierenden Binärdaten mime_type: Optionaler MIME-Typ für das Encoding Returns: Base64-kodierter String """ base64_data = base64.b64encode(content).decode('utf-8') return base64_data def prepare_file_contexts(files: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """ Bereitet die Dateikontexte basierend auf Metadaten vor. Akzeptiert keine Pfade mehr, sondern nur Metadaten aus der Datenbank. Args: files: Liste von Dateien mit Metadaten (Dict mit id, name, type, content_type) Returns: Liste von Dateikontexten für die Verarbeitung """ file_contexts = [] logger.info(f"Preparing file contexts for {len(files)} files") for file in files: file_id = file.get("id") file_name = file.get("name") file_type = file.get("type") # Create a comprehensive context with all available metadata context = { "id": file_id, "name": file_name, "type": file_type, "size": file.get("size", "Unbekannt"), "content_type": file.get("content_type"), "path": file.get("path"), "upload_date": file.get("upload_date"), "hash": file.get("hash"), "mandate_id": file.get("mandate_id"), "user_id": file.get("user_id") } # Log for debugging logger.info(f"Created file context: {file_name} (ID: {file_id}, Type: {file_type})") file_contexts.append(context) return file_contexts def extract_text_from_file_content(file_content: bytes, file_name: str, content_type: str = None) -> str: """ Extrahiert Text aus verschiedenen Dateiformaten basierend auf dem Binärinhalt. Args: file_content: Binärinhalt der Datei file_name: Name der Datei für die Erkennung des Formats content_type: Optional MIME-Typ der Datei Returns: Extrahierter Text oder Fehlermeldung """ try: # Einfache Textdateien if file_name.endswith(('.txt', '.md', '.json', '.xml', '.html', '.htm', '.css', '.js', '.py')): try: return file_content.decode('utf-8') except UnicodeDecodeError: try: return file_content.decode('latin1') except: return file_content.decode('cp1252', errors='replace') # Excel-Dateien elif file_name.endswith(('.xlsx', '.xls')): if pd is not None: # Temporäre Datei im Speicher erstellen file_obj = BytesIO(file_content) df = pd.read_excel(file_obj) result = f"Excel file with {len(df)} rows and {len(df.columns)} columns.\n" result += f"Columns: {', '.join(df.columns.tolist())}\n\n" result += df.to_string(index=False) return result else: return f"[Excel-Datei: {file_name} - pandas nicht installiert]" # CSV-Dateien elif file_name.endswith('.csv'): if pd is not None: try: # Temporäre Datei im Speicher erstellen file_obj = BytesIO(file_content) df = pd.read_csv(file_obj, encoding='utf-8') except UnicodeDecodeError: file_obj = BytesIO(file_content) try: df = pd.read_csv(file_obj, encoding='latin1') except: file_obj = BytesIO(file_content) df = pd.read_csv(file_obj, encoding='cp1252') result = f"CSV file with {len(df)} rows and {len(df.columns)} columns.\n" result += f"Columns: {', '.join(df.columns.tolist())}\n\n" result += df.to_string(index=False) return result else: return f"[CSV-Datei: {file_name} - pandas nicht installiert]" # PDF-Dateien elif file_name.endswith('.pdf'): try: try: from PyPDF2 import PdfReader # BytesIO is already imported at the top level reader = PdfReader(BytesIO(file_content)) text = "" for page in reader.pages: text += page.extract_text() + "\n\n" return text except ImportError: try: import fitz # PyMuPDF # BytesIO is already imported at the top level doc = fitz.open(stream=file_content, filetype="pdf") text = "" for page in doc: text += page.get_text() + "\n\n" return text except ImportError: return f"[PDF: {file_name} - Keine PDF-Bibliothek installiert]" except Exception as e: raise FileExtractionError(f"Fehler beim Lesen der PDF-Datei {file_name}: {str(e)}") # Sonstige Dateien else: return f"[Datei: {file_name} - Textextraktion nicht unterstützt]" except Exception as e: logger.error(f"Fehler beim Extrahieren von Text aus {file_name}: {str(e)}") raise FileExtractionError(f"Fehler beim Extrahieren von Text aus {file_name}: {str(e)}") async def extract_and_analyze_pdf_images( pdf_content: bytes, prompt: str, ai_service ) -> List[Dict[str, Any]]: """ Extrahiert Bilder aus einer PDF-Datei und analysiert sie. Arbeitet mit Binärdaten statt Dateipfaden. Args: pdf_content: Binärdaten der PDF-Datei prompt: Prompt für die Bildanalyse ai_service: AI-Service für die Bildanalyse Returns: Liste mit Analyseergebnissen für jedes Bild """ image_responses = [] temp_files = [] # Liste der temporären Dateien zur Bereinigung try: # PDF mit PyMuPDF öffnen import fitz # PyMuPDF # BytesIO is already imported at the top level import tempfile # PDF im Speicher öffnen doc = fitz.open(stream=pdf_content, filetype="pdf") logger.info(f"PDF geöffnet mit {len(doc)} Seiten") for page_num, page in enumerate(doc, 1): # Alle Bilder auf der Seite finden image_list = page.get_images(full=True) if image_list: logger.info(f"Seite {page_num}: {len(image_list)} Bilder gefunden") for img_index, img in enumerate(image_list): try: # Bild-Referenz xref = img[0] # Bild und Metadaten extrahieren base_image = doc.extract_image(xref) image_bytes = base_image["image"] # Tatsächliche Bilddaten image_ext = base_image["ext"] # Dateiendung (jpg, png, etc.) # Erstelle temporäre Datei fd, temp_img_path = tempfile.mkstemp(suffix=f".{image_ext}") temp_files.append(temp_img_path) # Zur Bereinigungsliste hinzufügen with os.fdopen(fd, 'wb') as img_file: img_file.write(image_bytes) logger.debug(f"Bild temporär gespeichert: {temp_img_path}") # Analysiere mit AI-Service try: analysis_result = await ai_service.analyze_image( image_data=image_bytes, # Direktes Übergeben der Bilddaten prompt=prompt, mime_type=f"image/{image_ext}" ) logger.debug(f"Bildanalyse für Bild {img_index} auf Seite {page_num} abgeschlossen") except Exception as analyze_error: logger.error(f"Fehler bei der Bildanalyse: {str(analyze_error)}") analysis_result = f"[Fehler bei der Bildanalyse: {str(analyze_error)}]" # Ergebnis speichern try: # Versuche zuerst, die Größe aus base_image zu bekommen if 'width' in base_image and 'height' in base_image: image_size = f"{base_image['width']}x{base_image['height']}" else: # Alternative: Öffne das temporäre Bild, um die Größe zu bestimmen from PIL import Image with Image.open(temp_img_path) as img: width, height = img.size image_size = f"{width}x{height}" except Exception as e: logger.warning(f"Konnte Bildgröße nicht ermitteln: {str(e)}") image_size = "unbekannt" image_responses.append({ "page": page_num, "image_index": img_index, "format": image_ext, "image_size": image_size, "response": analysis_result }) except Exception as e: logger.warning(f"Fehler bei der Extraktion von Bild {img_index} auf Seite {page_num}: {str(e)}") continue logger.info(f"Extrahiert und analysiert: {len(image_responses)} Bilder aus PDF") except ImportError: logger.error("PyMuPDF (fitz) ist nicht installiert. Installiere es mit 'pip install pymupdf'") raise FileExtractionError("PyMuPDF (fitz) ist nicht installiert") except Exception as e: logger.error(f"Fehler beim Extrahieren von PDF-Bildern: {str(e)}") raise FileExtractionError(f"Fehler beim Extrahieren von PDF-Bildern: {str(e)}") finally: # Bereinige alle temporären Dateien for temp_file in temp_files: try: if os.path.exists(temp_file): os.remove(temp_file) except Exception as e: logger.warning(f"Konnte temporäre Datei nicht entfernen: {temp_file} - {str(e)}") return image_responses def add_file_to_message(message: Dict[str, Any], file_data: Dict[str, Any]) -> Dict[str, Any]: """ Fügt eine Datei zu einer Nachricht hinzu. Funktion für Workflow-Manager und interne Verwendung. Args: message: Die zu erweiternde Nachricht file_data: Dateimetadaten und Inhalt Returns: Die aktualisierte Nachricht mit der Datei """ # Detailed logging for debugging logger.info(f"Adding file to message: {file_data.get('name', 'unnamed_file')} (ID: {file_data.get('id', 'unknown')})") # Initialize documents array if needed if "documents" not in message: message["documents"] = [] logger.debug("Initialized empty documents array in message") # Create a unique ID for the document if not provided doc_id = file_data.get("id", f"file_{uuid.uuid4()}") # Extract file size if available file_size = file_data.get("size") if isinstance(file_size, str) and file_size.isdigit(): file_size = int(file_size) elif file_size is None and file_data.get("content"): # Estimate size from content if not provided file_size = len(file_data.get("content", "")) # Create standard document structure that matches the data model document = { "id": doc_id, # Add an ID to the document itself "source": { "type": "file", "id": file_data.get("id", doc_id), "name": file_data.get("name", "unnamed_file"), "content_type": file_data.get("content_type"), "size": file_size, "upload_date": file_data.get("upload_date", datetime.now().isoformat()) }, "contents": [ { "type": "text", "text": file_data.get("content", "No content available") } ] } # Log document structure for debugging logger.debug(f"Created document structure: {json.dumps({k: v for k, v in document.items() if k != 'contents'})}") # Check if file is already in the message to avoid duplicates file_already_added = any( doc.get("source", {}).get("id") == file_data.get("id") for doc in message.get("documents", []) ) if not file_already_added: message["documents"].append(document) logger.info(f"File {file_data.get('name')} successfully added to message (total: {len(message.get('documents', []))} files)") else: logger.info(f"File {file_data.get('name')} already exists in message, skipping") return message def extract_files_from_message(message: Dict[str, Any]) -> List[Dict[str, Any]]: """ Extrahiert Dateiinformationen aus einer Nachricht. Funktion für Workflow-Manager und interne Verwendung. Args: message: Die Nachricht, aus der Dateien extrahiert werden sollen Returns: Liste der extrahierten Dateiinformationen """ files = [] if "documents" not in message: logger.debug("No documents found in message") return files # Log for debugging logger.debug(f"Extracting files from message with {len(message.get('documents', []))} documents") for doc in message.get("documents", []): doc_source = doc.get("source", {}) # Nur Dateien extrahieren if doc_source.get("type") == "file": file_info = { "id": doc_source.get("id", f"file_{uuid.uuid4()}"), "name": doc_source.get("name", "unnamed_file"), "content_type": doc_source.get("content_type"), "size": doc_source.get("size") } # Inhalt extrahieren, falls vorhanden doc_contents = doc.get("contents", []) for content in doc_contents: if content.get("type") == "text": file_info["content"] = content.get("text", "") break logger.debug(f"Extracted file: {file_info.get('name')} (ID: {file_info.get('id')})") files.append(file_info) else: logger.debug(f"Skipping non-file document of type: {doc_source.get('type')}") logger.info(f"Extracted {len(files)} files from message") return files async def read_file_contents( file_contexts: List[Dict[str, Any]], lucydom_interface, workflow_id: str = None, add_log_func = None, ai_service = None # AI service parameter for image analysis ) -> Dict[str, str]: """ Liest den Inhalt aller Dateien und führt bei Bildern und Dokumenten Analysen durch. Verwendet LucyDOM-Interface statt direkter Dateizugriffe. Args: file_contexts: Liste der Dateikontexte mit Metadaten lucydom_interface: LucyDOM-Interface für Dateizugriffe workflow_id: Optionale ID des Workflows für Logging add_log_func: Optionale Funktion für das Hinzufügen von Logs ai_service: Optionaler AI-Service für die Bildanalyse Returns: Dictionary mit Dateiinhalten (file_id -> content) """ file_contents = {} # Add debug logging logger.info(f"Reading contents of {len(file_contexts)} files for workflow {workflow_id}") for file in file_contexts: file_id = file["id"] file_name = file["name"] file_type = file.get("type", "unknown") try: # Dateiinhalt über LucyDOM-Interface abrufen file_data = await lucydom_interface.read_file_content(file_id) if not file_data: _log(add_log_func, workflow_id, f"Datei {file_name} nicht gefunden", "warning") file_contents[file_id] = f"File content not available (File not found)" continue logger.info(f"Successfully read file: {file_name} (ID: {file_id}, Type: {file_type})") # Image files - always perform image analysis if AI service is available if file_type == "image" or file_name.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')): if ai_service: try: #_log(add_log_func, workflow_id, f"Analyzing image {file_name} {len(file_data)}B...", "info") logger.info(f"ai_service type: {type(ai_service)}") logger.info(f"ai_service methods: {dir(ai_service)}") logger.info(f"ai_service has analyze_image method: {'analyze_image' in dir(ai_service)}") image_analysis = await ai_service.analyze_image( image_data=file_data, prompt="Describe this image in detail", mime_type=file.get("content_type") ) logger.debug(f"Image analysis successfully generated for {file_name}") file_contents[file_id] = f"Image Analysis:\n{image_analysis}" _log(add_log_func, workflow_id, f"Image {file_name} analyzed successfully", "info") except Exception as e: logger.error(f"Error analyzing image {file_name}: {str(e)}") _log(add_log_func, workflow_id, f"Error analyzing image {file_name}: {str(e)}", "error") file_contents[file_id] = f"Image file: {file_name} (Analysis failed: {str(e)})" else: file_contents[file_id] = f"Image file: {file_name} (AI analysis not available)" # Document files elif file_type == "document" or not file_type: # Verwende die zentrale Textextraktionsfunktion mit Dateiinhalt content = extract_text_from_file_content(file_data, file_name, file.get("content_type")) file_contents[file_id] = content _log(add_log_func, workflow_id, f"File {file_name} read successfully", "info") # Other file types - just store metadata else: file_contents[file_id] = f"File: {file_name} (Type: {file_type}, content not available)" _log(add_log_func, workflow_id, f"Unsupported file type: {file_type} for {file_name}", "warning") except Exception as e: logger.error(f"Error reading file {file_name}: {str(e)}") _log(add_log_func, workflow_id, f"Error reading file {file_name}: {str(e)}", "error") file_contents[file_id] = f"File content not available (Error: {str(e)})" return file_contents def _log(add_log_func, workflow_id, message, log_type, agent_id=None, agent_name=None): """Hilfsfunktion zum Loggen mit unterschiedlichen Log-Funktionen""" # Log über die Logger-Instanz if log_type == "error": logger.error(message) elif log_type == "warning": logger.warning(message) else: logger.info(message) # Log über die bereitgestellte Log-Funktion (falls vorhanden) if add_log_func and workflow_id: add_log_func(workflow_id, message, log_type, agent_id, agent_name)