1176 lines
47 KiB
Python
1176 lines
47 KiB
Python
import os
|
|
import logging
|
|
import uuid
|
|
from datetime import datetime, timedelta
|
|
import mimetypes
|
|
from typing import Dict, Any, List, Optional, Union, BinaryIO, Tuple
|
|
import importlib
|
|
import hashlib
|
|
from pathlib import Path
|
|
|
|
from connectors.connector_db_json import DatabaseConnector
|
|
from modules.utility import APP_CONFIG
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Custom exceptions for file handling
|
|
class FileError(Exception):
|
|
"""Base class for file handling exceptions."""
|
|
pass
|
|
|
|
class FileNotFoundError(FileError):
|
|
"""Exception raised when a file is not found."""
|
|
pass
|
|
|
|
class FileStorageError(FileError):
|
|
"""Exception raised when there's an error storing a file."""
|
|
pass
|
|
|
|
class FilePermissionError(FileError):
|
|
"""Exception raised when there's a permission issue with a file."""
|
|
pass
|
|
|
|
class FileDeletionError(FileError):
|
|
"""Exception raised when there's an error deleting a file."""
|
|
pass
|
|
|
|
|
|
class LucyDOMInterface:
|
|
"""
|
|
Interface zur LucyDOM-Datenbank.
|
|
Verwendet den JSON-Konnektor für den Datenzugriff.
|
|
"""
|
|
|
|
def __init__(self, mandate_id: int, user_id: int):
|
|
"""
|
|
Initialisiert das LucyDOM-Interface mit Mandanten- und Benutzerkontext.
|
|
|
|
Args:
|
|
mandate_id: ID des aktuellen Mandanten
|
|
user_id: ID des aktuellen Benutzers
|
|
"""
|
|
self.mandate_id = mandate_id
|
|
self.user_id = user_id
|
|
|
|
# Upload Verzeichnis aus config.ini lesen
|
|
self.upload_dir = APP_CONFIG.get('Module_AgentserviceInterface_UPLOAD_DIR')
|
|
os.makedirs(self.upload_dir, exist_ok=True)
|
|
|
|
# Datenmodell-Modul importieren
|
|
try:
|
|
self.model_module = importlib.import_module("modules.lucydom_model")
|
|
logger.info("lucydom_model erfolgreich importiert")
|
|
except ImportError as e:
|
|
logger.error(f"Fehler beim Importieren von lucydom_model: {e}")
|
|
raise
|
|
|
|
# Datenbank initialisieren, falls nötig
|
|
self._initialize_database()
|
|
|
|
|
|
def _initialize_database(self):
|
|
"""
|
|
Initialisiert die Datenbank mit minimalen Objekten für den angemeldeten Benutzer im Mandanten, falls sie noch nicht existiert.
|
|
Ohne gültigen Benutzer keine Initialisierung.
|
|
Erstellt für jede im Datenmodell definierte Tabelle einen initialen Datensatz.
|
|
"""
|
|
effective_mandate_id = self.mandate_id
|
|
effective_user_id = self.user_id
|
|
if effective_mandate_id is None or effective_user_id is None:
|
|
#data available
|
|
return
|
|
|
|
self.db = DatabaseConnector(
|
|
db_host=APP_CONFIG.get("DB_LUCYDOM_HOST"),
|
|
db_database=APP_CONFIG.get("DB_LUCYDOM_DATABASE"),
|
|
db_user=APP_CONFIG.get("DB_LUCYDOM_USER"),
|
|
db_password=APP_CONFIG.get("DB_LUCYDOM_PASSWORD_SECRET"),
|
|
mandate_id=self.mandate_id,
|
|
user_id=self.user_id
|
|
)
|
|
|
|
# Initialisierung von Standard-Prompts für verschiedene Bereiche
|
|
prompts = self.db.get_recordset("prompts")
|
|
if not prompts:
|
|
logger.info("Erstelle Standard-Prompts")
|
|
|
|
# Standard-Prompts definieren
|
|
standard_prompts = [
|
|
{
|
|
"mandate_id": effective_mandate_id,
|
|
"user_id": effective_user_id,
|
|
"content": "Recherchiere die aktuellen Markttrends und Entwicklungen im Bereich [THEMA]. Sammle Informationen zu führenden Unternehmen, innovativen Produkten oder Dienstleistungen und aktuellen Herausforderungen. Präsentiere die Ergebnisse in einer strukturierten Übersicht mit relevanten Daten und Quellen.",
|
|
"name": "Web Research: Marktforschung"
|
|
},
|
|
{
|
|
"mandate_id": effective_mandate_id,
|
|
"user_id": effective_user_id,
|
|
"content": "Analysiere den beigefügten Datensatz zu [THEMA] und identifiziere die wichtigsten Trends, Muster und Auffälligkeiten. Führe statistische Berechnungen durch, um deine Erkenntnisse zu untermauern. Stelle die Ergebnisse in einer klar strukturierten Analyse dar und ziehe relevante Schlussfolgerungen.",
|
|
"name": "Analyse: Datenanalyse"
|
|
},
|
|
{
|
|
"mandate_id": effective_mandate_id,
|
|
"user_id": effective_user_id,
|
|
"content": "Erstelle ein detailliertes Protokoll unserer Besprechung zum Thema [THEMA]. Erfasse alle besprochenen Punkte, getroffenen Entscheidungen und vereinbarten Maßnahmen. Strukturiere das Protokoll übersichtlich mit Tagesordnungspunkten, Teilnehmerliste und klaren Verantwortlichkeiten für die Follow-up-Aktionen.",
|
|
"name": "Protokoll: Besprechungsprotokoll"
|
|
},
|
|
{
|
|
"mandate_id": effective_mandate_id,
|
|
"user_id": effective_user_id,
|
|
"content": "Entwickle ein UI/UX-Designkonzept für [ANWENDUNG/WEBSITE]. Berücksichtige die Zielgruppe, Hauptfunktionen und die Markenidentität. Beschreibe die visuelle Gestaltung, Navigation, Interaktionsmuster und Informationsarchitektur. Erläutere, wie das Design die Benutzerfreundlichkeit und das Nutzererlebnis optimiert.",
|
|
"name": "Design: UI/UX Design"
|
|
}
|
|
]
|
|
|
|
# Prompts erstellen
|
|
for prompt_data in standard_prompts:
|
|
created_prompt = self.db.record_create("prompts", prompt_data)
|
|
logger.info(f"Prompt '{prompt_data.get('name', 'Standard')}' wurde erstellt mit ID {created_prompt['id']}")
|
|
|
|
|
|
# File utilities
|
|
|
|
def get_mime_type(self, file_path: str) -> str:
|
|
"""
|
|
Bestimmt den MIME-Typ einer Datei.
|
|
|
|
Args:
|
|
file_path: Pfad zur Datei
|
|
|
|
Returns:
|
|
Der erkannte MIME-Typ
|
|
"""
|
|
# Versuche, den MIME-Typ über den Dateipfad zu erkennen
|
|
mime_type, _ = mimetypes.guess_type(file_path)
|
|
|
|
# Wenn kein MIME-Typ erkannt wurde, versuche es über die Dateiendung
|
|
if not mime_type:
|
|
ext = os.path.splitext(file_path)[1].lower()[1:]
|
|
mime_type = self.get_mime_type_from_extension(ext)
|
|
|
|
return mime_type
|
|
|
|
def get_mime_type_from_extension(self, extension: str) -> str:
|
|
"""
|
|
Bestimmt den MIME-Typ basierend auf der Dateiendung.
|
|
|
|
Args:
|
|
extension: Die Dateiendung ohne Punkt
|
|
|
|
Returns:
|
|
Der entsprechende MIME-Typ
|
|
"""
|
|
extension_to_mime = {
|
|
"pdf": "application/pdf",
|
|
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
"doc": "application/msword",
|
|
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
"xls": "application/vnd.ms-excel",
|
|
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
"ppt": "application/vnd.ms-powerpoint",
|
|
"csv": "text/csv",
|
|
"txt": "text/plain",
|
|
"json": "application/json",
|
|
"xml": "application/xml",
|
|
"html": "text/html",
|
|
"htm": "text/html",
|
|
"jpg": "image/jpeg",
|
|
"jpeg": "image/jpeg",
|
|
"png": "image/png",
|
|
"gif": "image/gif",
|
|
"webp": "image/webp",
|
|
"svg": "image/svg+xml",
|
|
"py": "text/x-python",
|
|
"js": "application/javascript",
|
|
"css": "text/css"
|
|
}
|
|
|
|
return extension_to_mime.get(extension.lower(), "application/octet-stream")
|
|
|
|
def determine_file_type(self, file_path: str) -> str:
|
|
"""
|
|
Bestimmt den Typ einer Datei basierend auf dem MIME-Typ.
|
|
|
|
Args:
|
|
file_path: Pfad zur Datei
|
|
|
|
Returns:
|
|
Art der Datei: "image", "document" oder "file"
|
|
"""
|
|
mime_type = self.get_mime_type(file_path)
|
|
|
|
# Bildtypen
|
|
if mime_type.startswith("image/"):
|
|
return "image"
|
|
|
|
# Dokumenttypen
|
|
document_types = [
|
|
"application/pdf",
|
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", # docx
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", # xlsx
|
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation", # pptx
|
|
"application/vnd.ms-excel",
|
|
"application/vnd.ms-powerpoint",
|
|
"application/msword",
|
|
"text/csv",
|
|
"text/plain",
|
|
"application/json",
|
|
"application/xml",
|
|
"text/html",
|
|
"text/x-python",
|
|
"application/javascript",
|
|
"text/css"
|
|
]
|
|
|
|
if any(mime_type.startswith(dt) for dt in document_types) or mime_type in document_types:
|
|
return "document"
|
|
|
|
# Fallback für unbekannte Typen
|
|
return "file"
|
|
|
|
def calculate_file_hash(self, file_content: bytes) -> str:
|
|
"""
|
|
Calculate SHA-256 hash of file content for deduplication
|
|
|
|
Args:
|
|
file_content: Binary content of the file
|
|
|
|
Returns:
|
|
SHA-256 hash as a hexadecimal string
|
|
"""
|
|
return hashlib.sha256(file_content).hexdigest()
|
|
|
|
def _get_current_timestamp(self) -> str:
|
|
"""Gibt den aktuellen Zeitstempel im ISO-Format zurück"""
|
|
return datetime.now().isoformat()
|
|
|
|
def get_initial_id(self, table: str) -> Optional[int]:
|
|
"""
|
|
Gibt die initiale ID für eine Tabelle zurück.
|
|
|
|
Args:
|
|
table: Name der Tabelle
|
|
|
|
Returns:
|
|
Die initiale ID oder None, wenn nicht vorhanden
|
|
"""
|
|
return self.db.get_initial_id(table)
|
|
|
|
|
|
# Datei-Methoden
|
|
|
|
def get_all_files(self) -> List[Dict[str, Any]]:
|
|
"""Gibt alle Dateien des aktuellen Mandanten zurück"""
|
|
return self.db.get_recordset("files")
|
|
|
|
def get_file(self, file_id: int) -> Optional[Dict[str, Any]]:
|
|
"""Gibt eine Datei anhand ihrer ID zurück"""
|
|
files = self.db.get_recordset("files", record_filter={"id": file_id})
|
|
if files:
|
|
return files[0]
|
|
return None
|
|
|
|
def create_file(self, name: str, file_type: str, content_type: str = None,
|
|
size: int = None, path: str = None, file_hash: str = None) -> Dict[str, Any]:
|
|
"""Erstellt einen neuen Dateieintrag"""
|
|
file_data = {
|
|
"mandate_id": self.mandate_id,
|
|
"user_id": self.user_id,
|
|
"name": name,
|
|
"type": file_type,
|
|
"content_type": content_type,
|
|
"size": size,
|
|
"path": path,
|
|
"hash": file_hash,
|
|
"upload_date": self._get_current_timestamp()
|
|
}
|
|
|
|
return self.db.record_create("files", file_data)
|
|
|
|
def update_file(self, file_id: int, update_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Aktualisiert eine vorhandene Datei
|
|
|
|
Args:
|
|
file_id: ID der zu aktualisierenden Datei
|
|
update_data: Dictionary mit zu aktualisierenden Feldern
|
|
|
|
Returns:
|
|
Das aktualisierte Datei-Objekt
|
|
"""
|
|
# Prüfen, ob die Datei existiert
|
|
file = self.get_file(file_id)
|
|
if not file:
|
|
raise FileNotFoundError(f"Datei mit ID {file_id} nicht gefunden")
|
|
|
|
# Datei aktualisieren
|
|
return self.db.record_modify("files", file_id, update_data)
|
|
|
|
def check_for_duplicate_file(self, file_hash: str) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Check if a file with the same hash already exists
|
|
|
|
Args:
|
|
file_hash: SHA-256 hash of the file content
|
|
|
|
Returns:
|
|
File record if a duplicate exists, None otherwise
|
|
"""
|
|
files = self.db.get_recordset("files", record_filter={"hash": file_hash})
|
|
if files:
|
|
return files[0]
|
|
return None
|
|
|
|
def save_uploaded_file(self, file_content: bytes, file_name: str) -> Dict[str, Any]:
|
|
"""
|
|
Speichert eine hochgeladene Datei und erstellt einen Datenbankeintrag.
|
|
|
|
Args:
|
|
file_content: Binärdaten der Datei
|
|
file_name: Name der Datei
|
|
|
|
Returns:
|
|
Dictionary mit Metadaten der gespeicherten Datei
|
|
"""
|
|
try:
|
|
# Debug: Log the start of the file upload process
|
|
logger.info(f"Starting upload process for file: {file_name}")
|
|
logger.info(f"Upload directory: {self.upload_dir}, Mandate ID: {self.mandate_id}")
|
|
|
|
# Debug: Check if file_content is valid bytes
|
|
if not isinstance(file_content, bytes):
|
|
logger.error(f"Invalid file_content type: {type(file_content)}")
|
|
raise ValueError(f"file_content must be bytes, got {type(file_content)}")
|
|
|
|
# Calculate file hash for deduplication
|
|
file_hash = self.calculate_file_hash(file_content)
|
|
logger.debug(f"Calculated file hash: {file_hash}")
|
|
|
|
# Check for duplicate
|
|
existing_file = self.check_for_duplicate_file(file_hash)
|
|
if existing_file:
|
|
# Simply return the existing file metadata
|
|
logger.info(f"Duplikat gefunden für {file_name}: {existing_file['id']}")
|
|
return existing_file
|
|
|
|
# Generiere eindeutige ID
|
|
file_id = f"file_{uuid.uuid4()}"
|
|
logger.debug(f"Generated file ID: {file_id}")
|
|
|
|
# Sanitize filename
|
|
safe_filename = Path(file_name).name # Get only the filename part
|
|
logger.debug(f"Sanitized filename: {safe_filename}")
|
|
|
|
# Create parent directories if needed
|
|
mandate_upload_dir = os.path.join(self.upload_dir, str(self.mandate_id))
|
|
logger.debug(f"Mandate upload directory: {mandate_upload_dir}")
|
|
|
|
# Debug: Check if mandate upload directory exists
|
|
if not os.path.exists(mandate_upload_dir):
|
|
logger.info(f"Creating mandate upload directory: {mandate_upload_dir}")
|
|
|
|
os.makedirs(mandate_upload_dir, exist_ok=True)
|
|
|
|
# Dateipfad erstellen mit Mandant als Unterverzeichnis
|
|
file_path = os.path.join(mandate_upload_dir, f"{file_id}_{safe_filename}")
|
|
logger.debug(f"Full file path: {file_path}")
|
|
|
|
# Datei speichern
|
|
logger.info(f"Writing file content to: {file_path}")
|
|
with open(file_path, "wb") as f:
|
|
f.write(file_content)
|
|
|
|
# Verify file was created
|
|
if not os.path.exists(file_path):
|
|
logger.error(f"File was not created at path: {file_path}")
|
|
raise FileStorageError(f"File could not be created at {file_path}")
|
|
else:
|
|
logger.info(f"File successfully saved to: {file_path}")
|
|
|
|
# Dateigröße bestimmen
|
|
file_size = len(file_content)
|
|
|
|
# MIME-Typ und Dateityp bestimmen
|
|
mime_type = self.get_mime_type(file_path)
|
|
file_type = self.determine_file_type(file_path)
|
|
|
|
# Erstelle Metadaten
|
|
file_meta = {
|
|
"id": file_id,
|
|
"name": file_name,
|
|
"path": file_path,
|
|
"size": file_size,
|
|
"type": file_type,
|
|
"content_type": mime_type,
|
|
"hash": file_hash,
|
|
"upload_date": datetime.now().isoformat(),
|
|
"mandate_id": self.mandate_id,
|
|
"user_id": self.user_id
|
|
}
|
|
|
|
logger.debug(f"File metadata: {file_meta}")
|
|
|
|
# Speichere in der Datenbank
|
|
logger.info(f"Saving file metadata to database for file: {file_id}")
|
|
db_file = self.create_file(
|
|
name=file_name,
|
|
file_type=file_type,
|
|
content_type=mime_type,
|
|
size=file_size,
|
|
path=file_path,
|
|
file_hash=file_hash
|
|
)
|
|
|
|
# Debug: Verify database record was created
|
|
if not db_file:
|
|
logger.warning(f"Database record for file {file_id} was not created properly")
|
|
else:
|
|
logger.info(f"Database record created for file {file_id}")
|
|
|
|
# Wenn Datenbank-ID vorhanden ist, übernehme sie
|
|
if db_file and "id" in db_file:
|
|
file_meta["id"] = db_file["id"]
|
|
|
|
logger.info(f"File upload process completed for: {file_name}")
|
|
return file_meta
|
|
|
|
except Exception as e:
|
|
# If an error occurs, clean up any partial file
|
|
if 'file_path' in locals() and os.path.exists(file_path):
|
|
try:
|
|
logger.warning(f"Cleaning up partial file: {file_path}")
|
|
os.remove(file_path)
|
|
except Exception as cleanup_error:
|
|
logger.error(f"Error cleaning up partial file: {cleanup_error}")
|
|
|
|
logger.error(f"Error in save_uploaded_file for {file_name}: {str(e)}", exc_info=True)
|
|
raise FileStorageError(f"Fehler beim Speichern der Datei: {str(e)}")
|
|
|
|
|
|
|
|
async def read_file_content(self, file_id: int) -> Optional[bytes]:
|
|
"""
|
|
Reads the content of a file by ID
|
|
|
|
Args:
|
|
file_id: ID of the file
|
|
|
|
Returns:
|
|
File content as bytes or None if not found
|
|
"""
|
|
try:
|
|
# Get file metadata
|
|
file = self.get_file(file_id)
|
|
|
|
if not file or "path" not in file:
|
|
raise FileNotFoundError(f"Datei mit ID {file_id} nicht gefunden")
|
|
|
|
file_path = file["path"]
|
|
|
|
# Check if file exists
|
|
if not os.path.exists(file_path):
|
|
raise FileNotFoundError(f"Datei nicht gefunden: {file_path}")
|
|
|
|
# Read file content
|
|
with open(file_path, "rb") as f:
|
|
content = f.read()
|
|
|
|
return content
|
|
except FileNotFoundError as e:
|
|
# Re-raise FileNotFoundError as is
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Lesen der Datei {file_id}: {str(e)}")
|
|
raise FileError(f"Fehler beim Lesen der Datei: {str(e)}")
|
|
|
|
def download_file(self, file_id: int) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Gibt eine Datei zum Download zurück.
|
|
|
|
Args:
|
|
file_id: ID der Datei
|
|
|
|
Returns:
|
|
Dictionary mit Dateidaten und -metadaten oder None, wenn nicht gefunden
|
|
"""
|
|
try:
|
|
# Suche die Datei in der Datenbank
|
|
file = self.get_file(file_id)
|
|
|
|
if not file or "path" not in file:
|
|
raise FileNotFoundError(f"Datei mit ID {file_id} nicht gefunden")
|
|
|
|
file_path = file["path"]
|
|
|
|
# Prüfe, ob die Datei existiert
|
|
if not os.path.exists(file_path):
|
|
raise FileNotFoundError(f"Datei nicht gefunden: {file_path}")
|
|
|
|
# Lese die Datei
|
|
with open(file_path, "rb") as f:
|
|
file_content = f.read()
|
|
|
|
return {
|
|
"id": file_id,
|
|
"name": file.get("name", os.path.basename(file_path)),
|
|
"content_type": file.get("content_type", self.get_mime_type(file_path)),
|
|
"size": file.get("size", len(file_content)),
|
|
"path": file_path,
|
|
"content": file_content
|
|
}
|
|
except FileNotFoundError as e:
|
|
# Re-raise FileNotFoundError as is
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Herunterladen der Datei {file_id}: {str(e)}")
|
|
raise FileError(f"Fehler beim Herunterladen der Datei: {str(e)}")
|
|
|
|
def delete_file(self, file_id: int) -> bool:
|
|
"""
|
|
Löscht eine Datei aus der Datenbank und dem Dateisystem.
|
|
|
|
Args:
|
|
file_id: ID der Datei
|
|
|
|
Returns:
|
|
True bei Erfolg, False bei Fehler
|
|
"""
|
|
try:
|
|
# Suche die Datei in der Datenbank
|
|
file = self.get_file(file_id)
|
|
|
|
if not file:
|
|
raise FileNotFoundError(f"Datei mit ID {file_id} nicht gefunden")
|
|
|
|
# Prüfe, ob die Datei zum aktuellen Mandanten gehört
|
|
if file.get("mandate_id") != self.mandate_id:
|
|
raise FilePermissionError(f"Keine Berechtigung zum Löschen der Datei {file_id}")
|
|
|
|
# Speichere den Dateipfad
|
|
file_path = file.get("path")
|
|
|
|
# Check for other references to this file (by hash)
|
|
file_hash = file.get("hash")
|
|
if file_hash:
|
|
other_references = [f for f in self.db.get_recordset("files", record_filter={"hash": file_hash})
|
|
if f.get("id") != file_id]
|
|
|
|
# If other files reference this content, only delete the database entry
|
|
if other_references:
|
|
logger.info(f"Andere Referenzen auf den Dateiinhalt gefunden, nur DB-Eintrag wird gelöscht: {file_id}")
|
|
return self.db.record_delete("files", file_id)
|
|
|
|
# Lösche den Datenbankeintrag
|
|
db_success = self.db.record_delete("files", file_id)
|
|
|
|
# Wenn der Datenbankeintrag erfolgreich gelöscht wurde und ein Dateipfad vorhanden ist,
|
|
# lösche auch die Datei
|
|
if db_success and file_path and os.path.exists(file_path):
|
|
try:
|
|
os.remove(file_path)
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim physischen Löschen der Datei {file_path}: {str(e)}")
|
|
# Datenbankdatei wurde gelöscht, physische Datei nicht - trotzdem Erfolg melden
|
|
return True
|
|
|
|
return db_success
|
|
except FileNotFoundError as e:
|
|
# Pass through FileNotFoundError
|
|
raise
|
|
except FilePermissionError as e:
|
|
# Pass through FilePermissionError
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Löschen der Datei {file_id}: {str(e)}")
|
|
raise FileDeletionError(f"Fehler beim Löschen der Datei: {str(e)}")
|
|
|
|
|
|
# Prompt-Methoden
|
|
|
|
def get_all_prompts(self) -> List[Dict[str, Any]]:
|
|
"""Gibt alle Prompts des aktuellen Mandanten zurück"""
|
|
return self.db.get_recordset("prompts")
|
|
|
|
def get_prompt(self, prompt_id: int) -> Optional[Dict[str, Any]]:
|
|
"""Gibt einen Prompt anhand seiner ID zurück"""
|
|
prompts = self.db.get_recordset("prompts", record_filter={"id": prompt_id})
|
|
if prompts:
|
|
return prompts[0]
|
|
return None
|
|
|
|
def create_prompt(self, content: str, name: str) -> Dict[str, Any]:
|
|
"""Erstellt einen neuen Prompt"""
|
|
prompt_data = {
|
|
"mandate_id": self.mandate_id,
|
|
"user_id": self.user_id,
|
|
"content": content,
|
|
"name": name,
|
|
"created_at": self._get_current_timestamp()
|
|
}
|
|
|
|
return self.db.record_create("prompts", prompt_data)
|
|
|
|
def update_prompt(self, prompt_id: int, content: str = None, name: str = None) -> Dict[str, Any]:
|
|
"""
|
|
Aktualisiert einen vorhandenen Prompt
|
|
|
|
Args:
|
|
prompt_id: ID des zu aktualisierenden Prompts
|
|
content: Neuer Inhalt des Prompts
|
|
|
|
Returns:
|
|
Das aktualisierte Prompt-Objekt
|
|
"""
|
|
# Prüfen, ob der Prompt existiert
|
|
prompt = self.get_prompt(prompt_id)
|
|
if not prompt:
|
|
return None
|
|
|
|
# Daten für die Aktualisierung vorbereiten
|
|
prompt_data = {}
|
|
|
|
if content is not None:
|
|
prompt_data["content"] = content
|
|
if name is not None:
|
|
prompt_data["name"] = name
|
|
|
|
# Prompt aktualisieren
|
|
return self.db.record_modify("prompts", prompt_id, prompt_data)
|
|
|
|
def delete_prompt(self, prompt_id: int) -> bool:
|
|
"""
|
|
Löscht einen Prompt aus der Datenbank
|
|
|
|
Args:
|
|
prompt_id: ID des zu löschenden Prompts
|
|
|
|
Returns:
|
|
True, wenn der Prompt erfolgreich gelöscht wurde, sonst False
|
|
"""
|
|
return self.db.record_delete("prompts", prompt_id)
|
|
|
|
|
|
# Workflow Methoden
|
|
|
|
def get_all_workflows(self) -> List[Dict[str, Any]]:
|
|
"""Gibt alle Workflows des aktuellen Mandanten zurück"""
|
|
return self.db.get_recordset("workflows")
|
|
|
|
def get_workflows_by_user(self, user_id: int) -> List[Dict[str, Any]]:
|
|
"""Gibt alle Workflows eines Benutzers zurück"""
|
|
return self.db.get_recordset("workflows", record_filter={"user_id": user_id})
|
|
|
|
def get_workflow(self, workflow_id: str) -> Optional[Dict[str, Any]]:
|
|
"""Gibt einen Workflow anhand seiner ID zurück"""
|
|
workflows = self.db.get_recordset("workflows", record_filter={"id": workflow_id})
|
|
if workflows:
|
|
return workflows[0]
|
|
return None
|
|
|
|
def create_workflow(self, workflow_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Erstellt einen neuen Workflow in der Datenbank"""
|
|
# Stellen Sie sicher, dass mandate_id und user_id gesetzt sind
|
|
if "mandate_id" not in workflow_data:
|
|
workflow_data["mandate_id"] = self.mandate_id
|
|
|
|
if "user_id" not in workflow_data:
|
|
workflow_data["user_id"] = self.user_id
|
|
|
|
# Zeitstempel setzen, falls nicht vorhanden
|
|
current_time = self._get_current_timestamp()
|
|
if "started_at" not in workflow_data:
|
|
workflow_data["started_at"] = current_time
|
|
|
|
if "last_activity" not in workflow_data:
|
|
workflow_data["last_activity"] = current_time
|
|
|
|
return self.db.record_create("workflows", workflow_data)
|
|
|
|
def update_workflow(self, workflow_id: str, workflow_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Aktualisiert einen vorhandenen Workflow.
|
|
|
|
Args:
|
|
workflow_id: ID des zu aktualisierenden Workflows
|
|
workflow_data: Neue Daten für den Workflow
|
|
|
|
Returns:
|
|
Das aktualisierte Workflow-Objekt
|
|
"""
|
|
# Prüfen, ob der Workflow existiert
|
|
workflow = self.get_workflow(workflow_id)
|
|
if not workflow:
|
|
return None
|
|
|
|
# Aktualisierungszeit setzen
|
|
workflow_data["last_activity"] = self._get_current_timestamp()
|
|
|
|
# Workflow aktualisieren
|
|
return self.db.record_modify("workflows", workflow_id, workflow_data)
|
|
|
|
def delete_workflow(self, workflow_id: str) -> bool:
|
|
"""
|
|
Löscht einen Workflow aus der Datenbank.
|
|
|
|
Args:
|
|
workflow_id: ID des zu löschenden Workflows
|
|
|
|
Returns:
|
|
True bei Erfolg, False wenn der Workflow nicht existiert
|
|
"""
|
|
# Prüfen, ob der Workflow existiert
|
|
workflow = self.get_workflow(workflow_id)
|
|
if not workflow:
|
|
return False
|
|
|
|
# Prüfen, ob der Benutzer der Eigentümer ist oder Admin-Rechte hat
|
|
if workflow.get("user_id") != self.user_id:
|
|
# Hier könnte eine Prüfung auf Admin-Rechte erfolgen
|
|
return False
|
|
|
|
# Workflow löschen
|
|
return self.db.record_delete("workflows", workflow_id)
|
|
|
|
def get_workflow_messages(self, workflow_id: str) -> List[Dict[str, Any]]:
|
|
"""Gibt alle Nachrichten eines Workflows zurück"""
|
|
return self.db.get_recordset("workflow_messages", record_filter={"workflow_id": workflow_id})
|
|
|
|
def create_workflow_message(self, message_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Erstellt eine neue Nachricht für einen Workflow
|
|
|
|
Args:
|
|
message_data: Die Nachrichtendaten
|
|
|
|
Returns:
|
|
Die erstellte Nachricht oder None bei Fehler
|
|
"""
|
|
try:
|
|
# Check if required fields are present
|
|
required_fields = ["id", "workflow_id"]
|
|
for field in required_fields:
|
|
if field not in message_data:
|
|
logger.error(f"Pflichtfeld '{field}' fehlt in message_data")
|
|
raise ValueError(f"Pflichtfeld '{field}' fehlt in den Nachrichtendaten")
|
|
|
|
# Validate that ID is not None
|
|
if message_data["id"] is None:
|
|
message_data["id"] = f"msg_{uuid.uuid4()}"
|
|
logger.warning(f"Automatisch generierte ID für Workflow-Nachricht: {message_data['id']}")
|
|
|
|
# Stellen Sie sicher, dass die benötigten Felder vorhanden sind
|
|
if "created_at" not in message_data:
|
|
message_data["created_at"] = self._get_current_timestamp()
|
|
|
|
# Debug-Log für die zu erstellenden Daten
|
|
logger.debug(f"Erstelle Workflow-Nachricht mit Daten: {message_data}")
|
|
|
|
return self.db.record_create("workflow_messages", message_data)
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Erstellen der Workflow-Nachricht: {str(e)}")
|
|
# Return None instead of raising to avoid cascading failures
|
|
return None
|
|
|
|
def update_workflow_message(self, message_id: str, message_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Aktualisiert eine bestehende Workflow-Nachricht in der Datenbank
|
|
with improved document handling.
|
|
|
|
Args:
|
|
message_id: ID der Nachricht
|
|
message_data: Zu aktualisierende Daten
|
|
|
|
Returns:
|
|
Das aktualisierte Nachrichtenobjekt oder None bei Fehler
|
|
"""
|
|
try:
|
|
# Print debug info
|
|
print(f"Updating message {message_id} in database")
|
|
|
|
# Ensure message_id is provided
|
|
if not message_id:
|
|
logger.error("No message_id provided for update_workflow_message")
|
|
raise ValueError("message_id cannot be empty")
|
|
|
|
# Check if message exists in database
|
|
messages = self.db.get_recordset("workflow_messages", record_filter={"id": message_id})
|
|
if not messages:
|
|
logger.warning(f"Message with ID {message_id} does not exist in database")
|
|
|
|
# If message doesn't exist but we have workflow_id, create it
|
|
if "workflow_id" in message_data:
|
|
logger.info(f"Creating new message with ID {message_id} for workflow {message_data.get('workflow_id')}")
|
|
return self.db.record_create("workflow_messages", message_data)
|
|
else:
|
|
logger.error(f"Workflow ID missing for new message {message_id}")
|
|
return None
|
|
|
|
# Ensure documents array is handled properly
|
|
if "documents" in message_data:
|
|
logger.info(f"Message {message_id} has {len(message_data['documents'])} documents")
|
|
|
|
# Make sure we're not storing huge content in the database
|
|
# For each document, ensure content size is reasonable
|
|
documents_to_store = []
|
|
for doc in message_data["documents"]:
|
|
doc_copy = doc.copy()
|
|
|
|
# Process contents array if it exists
|
|
if "contents" in doc_copy:
|
|
# Ensure contents is not too large - limit text size
|
|
for content in doc_copy["contents"]:
|
|
if content.get("type") == "text" and "text" in content:
|
|
text = content["text"]
|
|
if len(text) > 1000: # Limit text preview to 1000 chars
|
|
content["text"] = text[:1000] + "... [truncated]"
|
|
|
|
documents_to_store.append(doc_copy)
|
|
|
|
# Replace with the processed documents
|
|
message_data["documents"] = documents_to_store
|
|
|
|
# Log the update data size for debugging
|
|
update_data_size = len(str(message_data))
|
|
logger.debug(f"Update data size: {update_data_size} bytes")
|
|
|
|
# Ensure ID is in the dataset
|
|
if 'id' not in message_data:
|
|
message_data['id'] = message_id
|
|
|
|
# Update the message
|
|
updated_message = self.db.record_modify("workflow_messages", message_id, message_data)
|
|
if updated_message:
|
|
logger.info(f"Message {message_id} updated successfully")
|
|
else:
|
|
logger.warning(f"Failed to update message {message_id}")
|
|
|
|
return updated_message
|
|
except Exception as e:
|
|
logger.error(f"Error updating message {message_id}: {str(e)}", exc_info=True)
|
|
# Re-raise with full information
|
|
raise ValueError(f"Error updating message {message_id}: {str(e)}")
|
|
|
|
|
|
|
|
def get_workflow_logs(self, workflow_id: str) -> List[Dict[str, Any]]:
|
|
"""Gibt alle Log-Einträge eines Workflows zurück"""
|
|
return self.db.get_recordset("workflow_logs", record_filter={"workflow_id": workflow_id})
|
|
|
|
def create_workflow_log(self, log_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Erstellt einen neuen Log-Eintrag für einen Workflow"""
|
|
# Stellen Sie sicher, dass die benötigten Felder vorhanden sind
|
|
if "timestamp" not in log_data:
|
|
log_data["timestamp"] = self._get_current_timestamp()
|
|
|
|
return self.db.record_create("workflow_logs", log_data)
|
|
|
|
def save_workflow_state(self, workflow: Dict[str, Any], save_messages: bool = True, save_logs: bool = True) -> bool:
|
|
"""
|
|
Speichert den kompletten Zustand eines Workflows in der Datenbank.
|
|
Dies umfasst den Workflow selbst, Nachrichten und Logs.
|
|
|
|
Args:
|
|
workflow: Das vollständige Workflow-Objekt
|
|
save_messages: Flag, ob Nachrichten gespeichert werden sollen
|
|
save_logs: Flag, ob Logs gespeichert werden sollen
|
|
|
|
Returns:
|
|
True bei Erfolg, False bei Fehler
|
|
"""
|
|
try:
|
|
workflow_id = workflow.get("id")
|
|
if not workflow_id:
|
|
return False
|
|
|
|
# Extrahiere nur die für die Datenbank relevanten Workflow-Felder
|
|
workflow_db_data = {
|
|
"id": workflow_id,
|
|
"mandate_id": workflow.get("mandate_id", self.mandate_id),
|
|
"user_id": workflow.get("user_id", self.user_id),
|
|
"name": workflow.get("name", f"Workflow {workflow_id}"),
|
|
"status": workflow.get("status", "unknown"),
|
|
"started_at": workflow.get("started_at", self._get_current_timestamp()),
|
|
"last_activity": workflow.get("last_activity", self._get_current_timestamp()),
|
|
"completed_at": workflow.get("completed_at"),
|
|
"data_stats": workflow.get("data_stats", {})
|
|
}
|
|
|
|
# Prüfen, ob der Workflow bereits existiert
|
|
existing_workflow = self.get_workflow(workflow_id)
|
|
if existing_workflow:
|
|
self.update_workflow(workflow_id, workflow_db_data)
|
|
else:
|
|
self.create_workflow(workflow_db_data)
|
|
|
|
|
|
# Nachrichten speichern
|
|
if save_messages and "messages" in workflow:
|
|
# Bestehende Nachrichten abrufen
|
|
existing_messages = {msg["id"]: msg for msg in self.get_workflow_messages(workflow_id)}
|
|
|
|
for message in workflow["messages"]:
|
|
message_id = message.get("id")
|
|
if not message_id:
|
|
continue
|
|
|
|
# Nur relevante Daten für die Datenbank extrahieren
|
|
message_data = {
|
|
"id": message_id,
|
|
"workflow_id": workflow_id,
|
|
"sequence_no": message.get("sequence_no", 0),
|
|
"role": message.get("role", "unknown"),
|
|
"content": message.get("content"),
|
|
"agent_type": message.get("agent_type"),
|
|
"created_at": message.get("started_at", self._get_current_timestamp()),
|
|
# IMPORTANT: Include documents field to persist file attachments
|
|
"documents": message.get("documents", [])
|
|
}
|
|
|
|
# Debug logging for documents
|
|
doc_count = len(message.get("documents", []))
|
|
if doc_count > 0:
|
|
logger.info(f"Message {message_id} has {doc_count} documents to save")
|
|
|
|
# Nachricht erstellen oder aktualisieren
|
|
if message_id in existing_messages:
|
|
self.db.record_modify("workflow_messages", message_id, message_data)
|
|
else:
|
|
self.db.record_create("workflow_messages", message_data)
|
|
|
|
# Logs speichern
|
|
if save_logs and "logs" in workflow:
|
|
# Bestehende Logs abrufen
|
|
existing_logs = {log["id"]: log for log in self.get_workflow_logs(workflow_id)}
|
|
|
|
for log in workflow["logs"]:
|
|
log_id = log.get("id")
|
|
if not log_id:
|
|
continue
|
|
|
|
# Nur relevante Daten für die Datenbank extrahieren
|
|
log_data = {
|
|
"id": log_id,
|
|
"workflow_id": workflow_id,
|
|
"message": log.get("message", ""),
|
|
"type": log.get("type", "info"),
|
|
"timestamp": log.get("timestamp", self._get_current_timestamp()),
|
|
"agent_id": log.get("agent_id"),
|
|
"agent_name": log.get("agent_name")
|
|
}
|
|
|
|
# Log erstellen oder aktualisieren
|
|
if log_id in existing_logs:
|
|
self.db.record_modify("workflow_logs", log_id, log_data)
|
|
else:
|
|
self.db.record_create("workflow_logs", log_data)
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Speichern des Workflow-Zustands: {str(e)}")
|
|
return False
|
|
|
|
|
|
def load_workflow_state(self, workflow_id: str) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Lädt den kompletten Zustand eines Workflows aus der Datenbank.
|
|
Dies umfasst den Workflow selbst, Nachrichten und Logs.
|
|
|
|
Args:
|
|
workflow_id: ID des zu ladenden Workflows
|
|
|
|
Returns:
|
|
Das vollständige Workflow-Objekt oder None bei Fehler
|
|
"""
|
|
try:
|
|
# Basis-Workflow laden
|
|
workflow = self.get_workflow(workflow_id)
|
|
if not workflow:
|
|
return None
|
|
|
|
# Log the workflow base retrieval
|
|
logger.debug(f"Loaded base workflow {workflow_id} from database")
|
|
|
|
# Nachrichten laden
|
|
messages = self.get_workflow_messages(workflow_id)
|
|
# Nach Sequenznummer sortieren
|
|
messages.sort(key=lambda x: x.get("sequence_no", 0))
|
|
|
|
# Debug log for messages and document counts
|
|
message_count = len(messages)
|
|
logger.debug(f"Loaded {message_count} messages for workflow {workflow_id}")
|
|
|
|
# Log document counts for each message
|
|
for msg in messages:
|
|
doc_count = len(msg.get("documents", []))
|
|
if doc_count > 0:
|
|
logger.info(f"Message {msg.get('id')} has {doc_count} documents loaded from database")
|
|
# Log document details for debugging
|
|
for i, doc in enumerate(msg.get("documents", [])):
|
|
source = doc.get("source", {})
|
|
logger.debug(f"Document {i+1}: {source.get('name', 'unnamed')} (ID: {source.get('id', 'unknown')})")
|
|
|
|
# Logs laden
|
|
logs = self.get_workflow_logs(workflow_id)
|
|
# Nach Zeitstempel sortieren
|
|
logs.sort(key=lambda x: x.get("timestamp", ""))
|
|
|
|
# Vollständiges Workflow-Objekt zusammenbauen
|
|
complete_workflow = workflow.copy()
|
|
complete_workflow["messages"] = messages
|
|
complete_workflow["logs"] = logs
|
|
|
|
return complete_workflow
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden des Workflow-Zustands: {str(e)}")
|
|
return None
|
|
|
|
|
|
# DELETE Workflow message elements
|
|
|
|
def delete_workflow_message(self, workflow_id: str, message_id: str) -> bool:
|
|
"""
|
|
Löscht eine Nachricht aus einem Workflow in der Datenbank.
|
|
|
|
Args:
|
|
workflow_id: ID des zugehörigen Workflows
|
|
message_id: ID der zu löschenden Nachricht
|
|
|
|
Returns:
|
|
True bei Erfolg, False bei Fehler
|
|
"""
|
|
try:
|
|
# Prüfen, ob die Nachricht existiert
|
|
messages = self.get_workflow_messages(workflow_id)
|
|
message = next((m for m in messages if m.get("id") == message_id), None)
|
|
|
|
if not message:
|
|
logger.warning(f"Nachricht {message_id} für Workflow {workflow_id} nicht gefunden")
|
|
return False
|
|
|
|
# Nachricht aus der Datenbank löschen
|
|
return self.db.record_delete("workflow_messages", message_id)
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Löschen der Nachricht {message_id}: {str(e)}")
|
|
return False
|
|
|
|
def delete_file_from_message(self, workflow_id: str, message_id: str, file_id: int) -> bool:
|
|
"""
|
|
Entfernt eine Dateireferenz aus einer Nachricht.
|
|
Die Datei selbst wird nicht gelöscht, nur die Referenz in der Nachricht.
|
|
Enhanced version with improved file matching.
|
|
|
|
Args:
|
|
workflow_id: ID des zugehörigen Workflows
|
|
message_id: ID der Nachricht
|
|
file_id: ID der zu entfernenden Datei
|
|
|
|
Returns:
|
|
True bei Erfolg, False bei Fehler
|
|
"""
|
|
try:
|
|
# Log operation
|
|
logger.info(f"Removing file {file_id} from message {message_id} in workflow {workflow_id}")
|
|
|
|
# Get all workflow messages
|
|
all_messages = self.get_workflow_messages(workflow_id)
|
|
logger.debug(f"Workflow {workflow_id} has {len(all_messages)} messages")
|
|
|
|
# Try different approaches to find the message
|
|
message = None
|
|
|
|
# Exact match
|
|
message = next((m for m in all_messages if m.get("id") == message_id), None)
|
|
|
|
# Case-insensitive match
|
|
if not message and isinstance(message_id, str):
|
|
message = next((m for m in all_messages
|
|
if isinstance(m.get("id"), str) and m.get("id").lower() == message_id.lower()), None)
|
|
|
|
# Partial match (starts with)
|
|
if not message and isinstance(message_id, str):
|
|
message = next((m for m in all_messages
|
|
if isinstance(m.get("id"), str) and m.get("id").startswith(message_id)), None)
|
|
|
|
if not message:
|
|
logger.warning(f"Message {message_id} not found in workflow {workflow_id}")
|
|
return False
|
|
|
|
# Log the found message
|
|
logger.info(f"Found message: {message.get('id')}")
|
|
|
|
# Check if message has documents
|
|
if "documents" not in message or not message["documents"]:
|
|
logger.warning(f"No documents in message {message_id}")
|
|
return False
|
|
|
|
# Log existing documents
|
|
documents = message.get("documents", [])
|
|
logger.debug(f"Message has {len(documents)} documents")
|
|
for i, doc in enumerate(documents):
|
|
doc_id = doc.get("id", "unknown")
|
|
source = doc.get("source", {})
|
|
source_id = source.get("id", "unknown")
|
|
logger.debug(f"Document {i}: doc_id={doc_id}, source_id={source_id}")
|
|
|
|
# Create a new list of documents without the one to delete
|
|
updated_documents = []
|
|
removed = False
|
|
|
|
for doc in documents:
|
|
doc_id = doc.get("id")
|
|
source = doc.get("source", {})
|
|
source_id = source.get("id")
|
|
|
|
# Flexible matching approach
|
|
should_remove = (
|
|
(doc_id == file_id) or
|
|
(source_id == file_id) or
|
|
(isinstance(doc_id, str) and file_id in doc_id) or
|
|
(isinstance(source_id, str) and file_id in source_id)
|
|
)
|
|
|
|
if should_remove:
|
|
removed = True
|
|
logger.info(f"Found file to remove: doc_id={doc_id}, source_id={source_id}")
|
|
else:
|
|
updated_documents.append(doc)
|
|
|
|
if not removed:
|
|
logger.warning(f"No matching file {file_id} found in message {message_id}")
|
|
return False
|
|
|
|
# Update message with modified documents array
|
|
message_update = {
|
|
"documents": updated_documents
|
|
}
|
|
|
|
# Apply the update directly to the database
|
|
updated = self.db.record_modify("workflow_messages", message["id"], message_update)
|
|
|
|
if updated:
|
|
logger.info(f"Successfully removed file {file_id} from message {message_id}")
|
|
return True
|
|
else:
|
|
logger.warning(f"Failed to update message {message_id} in database")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error removing file {file_id} from message {message_id}: {str(e)}")
|
|
return False
|
|
|
|
|
|
# Singleton-Factory für LucyDOMInterface-Instanzen pro Kontext
|
|
_lucydom_interfaces = {}
|
|
|
|
def get_lucydom_interface(mandate_id: int = 0, user_id: int = 0) -> LucyDOMInterface:
|
|
"""
|
|
Gibt eine LucyDOMInterface-Instanz für den angegebenen Kontext zurück.
|
|
Wiederverwendet bestehende Instanzen.
|
|
"""
|
|
context_key = f"{mandate_id}_{user_id}"
|
|
if context_key not in _lucydom_interfaces:
|
|
_lucydom_interfaces[context_key] = LucyDOMInterface(mandate_id, user_id)
|
|
return _lucydom_interfaces[context_key]
|
|
|
|
# Init
|
|
get_lucydom_interface()
|