1333 lines
No EOL
59 KiB
Python
1333 lines
No EOL
59 KiB
Python
"""
|
|
Manager für Workflow-Ausführung im Agentservice.
|
|
Steuert den gesamten Ablauf eines Workflow-Durchlaufs.
|
|
|
|
Implementiert die neue Workflow-Struktur und Ausführungslogik gemäß den Anforderungen.
|
|
Unterstützt sowohl neue Workflows als auch die Fortsetzung bestehender Workflows mit Benutzereingaben.
|
|
Angepasst für die verbesserte Dateibehandlung.
|
|
"""
|
|
|
|
import os
|
|
import logging
|
|
import asyncio
|
|
import uuid
|
|
import json
|
|
from datetime import datetime
|
|
from typing import List, Dict, Any, Optional, Tuple, Union
|
|
|
|
# Import von Modulen
|
|
from modules.agentservice_registry import AgentRegistry
|
|
from modules.agentservice_filemanager import prepare_file_contexts, read_file_contents, extract_files_from_message, add_file_to_message
|
|
from modules.agentservice_dataextraction import data_extraction
|
|
|
|
# Import neuer Modellklassen
|
|
try:
|
|
from modules.lucydom_model import Message, Workflow, Document, DocumentSource, DocumentContent, DataStats
|
|
except ImportError:
|
|
# Fallback-Definitionen
|
|
class Message(Dict[str, Any]): pass
|
|
class Workflow(Dict[str, Any]): pass
|
|
class Document(Dict[str, Any]): pass
|
|
class DocumentSource(Dict[str, Any]): pass
|
|
class DocumentContent(Dict[str, Any]): pass
|
|
class DataStats(Dict[str, Any]): pass
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
registry = AgentRegistry.get_instance()
|
|
agent = registry.get_agent("user_agent")
|
|
|
|
class WorkflowError(Exception):
|
|
"""Basis-Exception für Workflow-Fehler"""
|
|
pass
|
|
|
|
class WorkflowNotFoundError(WorkflowError):
|
|
"""Exception wenn ein Workflow nicht gefunden wurde"""
|
|
pass
|
|
|
|
class WorkflowExecutionError(WorkflowError):
|
|
"""Exception bei der Ausführung eines Workflows"""
|
|
pass
|
|
|
|
class WorkflowManager:
|
|
"""Manager für die Ausführung von Workflows"""
|
|
|
|
def __init__(self, mandate_id: int = None, user_id: int = None, ai_service = None, lucydom_interface = None):
|
|
"""
|
|
Initialisiert den WorkflowManager.
|
|
|
|
Args:
|
|
mandate_id: ID des Mandanten
|
|
user_id: ID des Benutzers
|
|
ai_service: Service für KI-Anfragen
|
|
lucydom_interface: Interface für Datenbankzugriffe (optional)
|
|
"""
|
|
self.mandate_id = mandate_id
|
|
self.user_id = user_id
|
|
self.ai_service = ai_service
|
|
self.lucydom_interface = lucydom_interface
|
|
|
|
# Lade Konfiguration aus config.ini
|
|
import configload
|
|
config = configload.load_config()
|
|
|
|
# Verzeichnisse für Ergebnisse und Uploads aus der Konfiguration lesen
|
|
self.results_dir = config.get('Module_AgentserviceInterface', 'RESULTS_DIR', fallback='results')
|
|
|
|
# Maximale Anzahl an Nachrichten im Verlauf
|
|
self.max_history = int(config.get('Module_AgentserviceInterface', 'MAX_HISTORY', fallback='20'))
|
|
|
|
# Stelle sicher, dass die Verzeichnisse existieren
|
|
os.makedirs(self.results_dir, exist_ok=True)
|
|
|
|
# Aktive Workflows
|
|
self.workflows = {}
|
|
|
|
# Lade aktive Workflows aus der Datenbank, falls verfügbar
|
|
if self.lucydom_interface:
|
|
self._load_active_workflows()
|
|
|
|
logger.info(f"WorkflowManager initialisiert mit Mandant {mandate_id}, Benutzer {user_id}")
|
|
|
|
|
|
async def execute_workflow(
|
|
self,
|
|
message: Dict[str, Any],
|
|
workflow_id: Optional[str] = None,
|
|
files: List[Dict[str, Any]] = None,
|
|
is_user_input: bool = False # Parameter to identify user input
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Führt einen Workflow aus, entweder durch Erstellen eines neuen oder
|
|
Fortsetzen eines bestehenden Workflows mit Benutzereingabe.
|
|
|
|
Args:
|
|
message: Die Nachricht (Prompt oder Benutzereingabe)
|
|
workflow_id: Optional ID eines bestehenden Workflows
|
|
files: Optionale Liste von Dateimetadaten
|
|
is_user_input: Flag, das anzeigt, ob es sich um eine Benutzereingabe handelt
|
|
|
|
Returns:
|
|
Dictionary mit Workflow-Status und Ergebnis
|
|
"""
|
|
# Add detailed debug logging
|
|
logger.info(f"execute_workflow called: workflow_id={workflow_id}, is_user_input={is_user_input}, message={message.get('content', '')[:50]}...")
|
|
|
|
# Detailed file logging
|
|
if files:
|
|
logger.info(f"Files provided: {len(files)} files")
|
|
for file in files:
|
|
file_id = file.get('id', 'unknown')
|
|
file_name = file.get('name', 'unnamed')
|
|
file_type = file.get('type', 'unknown')
|
|
file_content_type = file.get('content_type', 'unknown')
|
|
logger.info(f"File: {file_name} (ID: {file_id}, Type: {file_type}, Content-Type: {file_content_type})")
|
|
else:
|
|
logger.info("No files provided with the message")
|
|
|
|
# 4.1 Unterscheide zwischen neuem Workflow und bestehender Benutzereingabe
|
|
is_new_workflow = workflow_id is None
|
|
|
|
if is_new_workflow:
|
|
# Variante (A): Neuen Workflow erstellen
|
|
workflow_id = f"wf_{uuid.uuid4()}"
|
|
workflow = self._initialize_workflow(workflow_id)
|
|
workflow["name"] = message.get("content", "")[:50] # Kurzer Titel aus dem Inhalt
|
|
workflow["status"] = "running"
|
|
self._add_log(workflow, "Neuer Workflow gestartet", "info")
|
|
else:
|
|
# Variante (B): Bestehenden Workflow laden
|
|
try:
|
|
workflow = await self.load_workflow(workflow_id)
|
|
if not workflow:
|
|
raise WorkflowNotFoundError(f"Workflow {workflow_id} nicht gefunden")
|
|
|
|
# WICHTIG: Workflow-Status immer auf "running" setzen, unabhängig vom vorherigen Status
|
|
# So stellen wir sicher, dass der Workflow nach einer Benutzereingabe korrekt fortgesetzt wird
|
|
workflow["status"] = "running"
|
|
workflow["last_activity"] = datetime.now().isoformat()
|
|
self._add_log(workflow, "Workflow nach Benutzereingabe fortgesetzt", "info")
|
|
except WorkflowNotFoundError as e:
|
|
logger.error(f"Workflow nicht gefunden: {str(e)}")
|
|
return {
|
|
"workflow_id": workflow_id,
|
|
"status": "error",
|
|
"error": f"Workflow nicht gefunden: {workflow_id}"
|
|
}
|
|
except WorkflowError as e:
|
|
logger.error(f"Workflow-Fehler: {str(e)}")
|
|
return {
|
|
"workflow_id": workflow_id,
|
|
"status": "error",
|
|
"error": str(e)
|
|
}
|
|
|
|
logger.debug(f"Workflow initialisiert: {workflow_id}, Status: {workflow['status']}")
|
|
|
|
try:
|
|
# 4.2 Message-Initialisierung
|
|
# Letztes Message-Objekt abschließen (falls vorhanden)
|
|
if "messages" in workflow and workflow["messages"]:
|
|
self._finalize_last_message(workflow)
|
|
|
|
# Neues Message-Objekt erstellen
|
|
new_message = self._create_message(workflow_id, message.get("role", "user"))
|
|
new_message["content"] = message.get("content", "")
|
|
|
|
# Workflow-ID zum Message-Objekt hinzufügen für bessere Fehlerbehandlung
|
|
new_message["workflow_id"] = workflow_id
|
|
|
|
# Log the message creation
|
|
logger.info(f"Created new message with ID {new_message['id']} and content: {new_message['content'][:50]}...")
|
|
|
|
# 4.3 Dateivorbereitung
|
|
if files and len(files) > 0:
|
|
# Add detailed logging
|
|
logger.info(f"Processing {len(files)} files for message {new_message['id']}")
|
|
for f in files:
|
|
logger.info(f"Processing file: {f.get('name', 'unknown')} (ID: {f.get('id', 'unknown')})")
|
|
|
|
# Dateikontexte vorbereiten - enthält nur Metadaten
|
|
file_contexts = prepare_file_contexts(files)
|
|
self._add_log(workflow, f"{len(files)} Dateien werden verarbeitet", "info")
|
|
|
|
# Dateiinhalte lesen und zum Message-Objekt hinzufügen
|
|
# LucyDOM-Interface wird für Dateizugriffe genutzt
|
|
file_contents = await read_file_contents(
|
|
file_contexts,
|
|
self.lucydom_interface,
|
|
workflow_id,
|
|
self._add_log,
|
|
self.ai_service
|
|
)
|
|
logger.debug(f"Dateien geladen für Workflow {workflow_id}: {file_contents.keys()}")
|
|
|
|
for file_id, content in file_contents.items():
|
|
file_metadata = next((f for f in files if f.get('id') == file_id), {})
|
|
file_data = {
|
|
"id": file_id,
|
|
"name": file_metadata.get('name', next((f.get('name', 'unnamed_file') for f in file_contexts if f.get('id') == file_id), 'unnamed_file')),
|
|
"content_type": file_metadata.get('content_type', next((f.get('content_type') for f in file_contexts if f.get('id') == file_id), None)),
|
|
"type": file_metadata.get('type', next((f.get('type') for f in file_contexts if f.get('id') == file_id), "unknown")),
|
|
"content": content,
|
|
"size": file_metadata.get('size')
|
|
}
|
|
logger.info(f"Adding file {file_data['name']} (ID: {file_id}) to message {new_message['id']}")
|
|
try:
|
|
# Add file to message and check document count before and after
|
|
doc_count_before = len(new_message.get("documents", []))
|
|
new_message = add_file_to_message(new_message, file_data)
|
|
doc_count_after = len(new_message.get("documents", []))
|
|
|
|
if doc_count_after > doc_count_before:
|
|
logger.info(f"File successfully added to message. Document count: {doc_count_after}")
|
|
else:
|
|
logger.warning(f"File may not have been added to message properly. Document count unchanged: {doc_count_before}")
|
|
except Exception as e:
|
|
logger.error(f"Error adding file to message: {str(e)}")
|
|
self._add_log(workflow, f"Fehler beim Hinzufügen der Datei {file_data['name']}: {str(e)}", "error")
|
|
|
|
# Message zum Workflow hinzufügen
|
|
if "messages" not in workflow:
|
|
workflow["messages"] = []
|
|
|
|
# Log the message document count before adding to workflow
|
|
logger.info(f"Adding message with {len(new_message.get('documents', []))} documents to workflow {workflow_id}")
|
|
workflow["messages"].append(new_message)
|
|
|
|
# Immediately save workflow to persist file attachments
|
|
self._save_workflow(workflow)
|
|
logger.info(f"Saved workflow state after adding message with {len(new_message.get('documents', []))} documents")
|
|
|
|
# 4.5 Moderator-Entscheidung (mit OpenAI API)
|
|
self._add_log(workflow, "Moderator analysiert die Anfrage und wählt passende Agenten aus", "info")
|
|
|
|
# 4.4 Agent-Initialisierung
|
|
agents = registry.initialize_agents_for_workflow()
|
|
|
|
# Moderator-Entscheidung abfragen (nur System-Agenten)
|
|
system_agent_tasks = await self._decide_agent_tasks(workflow, new_message, agents)
|
|
|
|
# Speichere den aktuellen Zwischenstand
|
|
self._save_workflow(workflow)
|
|
|
|
# Nach Agenten-Entscheidung
|
|
self._add_log(workflow, f"Moderator hat die Entscheidung getroffen: {len(system_agent_tasks)} System-Agenten ausgewählt", "info")
|
|
logger.debug(f"Agent-Tasks für Workflow {workflow_id}: {[task['agent_id'] for task in system_agent_tasks]}")
|
|
|
|
for task in system_agent_tasks:
|
|
self._add_log(workflow, f"Agent {task['agent_id']} wurde ausgewählt mit Aufgabe: {task['prompt'][:50]}...", "info")
|
|
|
|
# 4.6 Agent-Ausführung
|
|
|
|
# 1. System-Agenten ausführen, falls vorhanden
|
|
agent_results = []
|
|
last_result = None
|
|
|
|
if system_agent_tasks:
|
|
self._add_log(workflow, f"{len(system_agent_tasks)} System-Agenten werden ausgeführt", "info")
|
|
|
|
for task in system_agent_tasks:
|
|
agent_id = task["agent_id"]
|
|
agent_prompt = task["prompt"]
|
|
expected_format = task.get("expected_format")
|
|
|
|
if agent_id == "moderator": # moderator answered directly in variable agent_prompt
|
|
agent_result = {
|
|
"agent_id": agent_id,
|
|
"agent_name": "moderator",
|
|
"content": agent_prompt,
|
|
"agent_type": "system",
|
|
"result_format": "Text" # Moderator liefert Text
|
|
}
|
|
agent_results.append(agent_result)
|
|
else:
|
|
# Wenn ein vorheriges Ergebnis existiert, in den Prompt einbinden
|
|
if last_result:
|
|
agent_prompt = f"{agent_prompt}\n\nVorheriges Ergebnis: {last_result}"
|
|
|
|
self._add_log(workflow, f"Agent {agent_id} wird ausgeführt", "info")
|
|
|
|
# Agenten ausführen mit erwartetem Format
|
|
agent_result = await self._execute_agent(workflow, agent_id, agent_prompt, expected_format)
|
|
|
|
if agent_result:
|
|
agent_results.append(agent_result)
|
|
last_result = agent_result.get("content", "")
|
|
|
|
# Zusätzlicher Log-Eintrag für das Frontend
|
|
self._add_log(workflow, f"Agent {agent_id} hat seine Aufgabe abgeschlossen", "success")
|
|
|
|
# 2. Immer den User-Agent aufrufen, mit einem generischen
|
|
# Prompt basierend auf den Ergebnissen der System-Agenten
|
|
|
|
# Erstelle einen benutzerfreundlichen Prompt basierend auf den System-Agent-Ergebnissen
|
|
if agent_results:
|
|
# Wenn System-Agenten ausgeführt wurden, fasse ihre Ergebnisse zusammen
|
|
summary = await self._create_summary(agent_results)
|
|
user_prompt = f"Die Agenten haben ihre Aufgaben abgeschlossen. Hier ist eine Zusammenfassung der Ergebnisse:\n\n{summary}\n\nBenötigen Sie weitere Informationen oder haben Sie Fragen dazu?"
|
|
else:
|
|
# Wenn keine System-Agenten ausgeführt wurden
|
|
user_prompt = "Ich habe Ihre Anfrage geprüft. Wie kann ich Ihnen konkret weiterhelfen?"
|
|
|
|
# 3. User-Agent-Nachricht erstellen und zum Workflow hinzufügen
|
|
workflow["status"] = "completed" # Workflow is complete, ready for new prompt
|
|
|
|
user_message = {
|
|
"role": "assistant",
|
|
"content": f"[Moderator zu User Agent] {user_prompt}",
|
|
"agent_type": "moderator",
|
|
"agent_id": "moderator",
|
|
"agent_name": "Moderator",
|
|
"workflow_complete": True # Signal completion instead of waiting
|
|
}
|
|
# Nachricht zum Workflow hinzufügen
|
|
workflow["messages"].append(user_message)
|
|
|
|
# Log-Eintrag
|
|
self._add_log(workflow, f"Workflow wartet auf Benutzereingabe: {user_prompt[:50]}...", "info")
|
|
|
|
# Workflow speichern
|
|
self._save_workflow(workflow)
|
|
|
|
# Fertig - Backend wartet jetzt auf nächsten API-Call vom Frontend
|
|
return {
|
|
"workflow_id": workflow_id,
|
|
"status": "completed",
|
|
"messages": workflow.get("messages", [])
|
|
}
|
|
|
|
except Exception as e:
|
|
# Fehlerbehandlung
|
|
workflow["status"] = "failed"
|
|
self._add_log(workflow, f"Fehler bei der Workflow-Ausführung: {str(e)}", "error")
|
|
self._save_workflow(workflow)
|
|
logger.error(f"Fehler bei der Workflow-Ausführung: {str(e)}", exc_info=True)
|
|
|
|
return {
|
|
"workflow_id": workflow_id,
|
|
"status": "failed",
|
|
"error": str(e)
|
|
}
|
|
|
|
|
|
def _load_active_workflows(self):
|
|
"""Lädt aktive Workflows aus der Datenbank"""
|
|
try:
|
|
if not self.lucydom_interface:
|
|
return
|
|
|
|
# Aktive Workflows für den aktuellen Benutzer abrufen
|
|
user_workflows = self.lucydom_interface.get_workflows_by_user(self.user_id)
|
|
active_workflows = [wf for wf in user_workflows if wf.get("status") in ["running", "completed"]]
|
|
|
|
# Aktive Workflows in den Speicher laden
|
|
for workflow_base in active_workflows:
|
|
workflow_id = workflow_base.get("id")
|
|
if not workflow_id:
|
|
continue
|
|
|
|
# Vollständigen Workflow-Zustand laden
|
|
workflow = self.lucydom_interface.load_workflow_state(workflow_id)
|
|
if workflow:
|
|
self.workflows[workflow_id] = workflow
|
|
logger.info(f"Aktiven Workflow {workflow_id} aus Datenbank geladen")
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden der aktiven Workflows: {str(e)}")
|
|
|
|
def _save_workflow(self, workflow: Dict[str, Any]) -> None:
|
|
"""
|
|
Speichert den Workflow in der Datenbank und als Datei.
|
|
|
|
Args:
|
|
workflow: Das zu speichernde Workflow-Objekt
|
|
"""
|
|
workflow_id = workflow.get("id")
|
|
|
|
# In der Datenbank speichern, falls verfügbar
|
|
if self.lucydom_interface:
|
|
try:
|
|
success = self.lucydom_interface.save_workflow_state(workflow)
|
|
if success:
|
|
logger.debug(f"Workflow {workflow_id} in Datenbank gespeichert")
|
|
else:
|
|
logger.warning(f"Workflow {workflow_id} konnte nicht in Datenbank gespeichert werden")
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Speichern des Workflows {workflow_id} in Datenbank: {str(e)}")
|
|
|
|
# Als Datei speichern (Backup/Fallback)
|
|
workflow_path = os.path.join(self.results_dir, f"workflow_{workflow_id}.json")
|
|
|
|
try:
|
|
with open(workflow_path, 'w', encoding='utf-8') as f:
|
|
json.dump(workflow, f, indent=2, ensure_ascii=False)
|
|
logger.debug(f"Workflow {workflow_id} als Datei gespeichert: {workflow_path}")
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Speichern des Workflows {workflow_id} als Datei: {str(e)}")
|
|
|
|
async def load_workflow(self, workflow_id: str) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Lädt einen Workflow aus der Datenbank oder Datei.
|
|
|
|
Args:
|
|
workflow_id: ID des Workflows
|
|
|
|
Returns:
|
|
Das geladene Workflow-Objekt oder None, wenn der Workflow nicht existiert
|
|
"""
|
|
# Prüfen, ob der Workflow bereits im Speicher ist
|
|
if workflow_id in self.workflows:
|
|
return self.workflows[workflow_id]
|
|
|
|
# Versuche, den Workflow aus der Datenbank zu laden
|
|
if self.lucydom_interface:
|
|
try:
|
|
workflow = self.lucydom_interface.load_workflow_state(workflow_id)
|
|
if workflow:
|
|
# Workflow im Speicher cachen
|
|
self.workflows[workflow_id] = workflow
|
|
logger.info(f"Workflow {workflow_id} aus Datenbank geladen")
|
|
return workflow
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden des Workflows {workflow_id} aus Datenbank: {str(e)}")
|
|
|
|
# Versuche, den Workflow aus der Datei zu laden
|
|
workflow_path = os.path.join(self.results_dir, f"workflow_{workflow_id}.json")
|
|
|
|
try:
|
|
if os.path.exists(workflow_path):
|
|
with open(workflow_path, 'r', encoding='utf-8') as f:
|
|
workflow = json.load(f)
|
|
|
|
# Workflow im Speicher cachen
|
|
self.workflows[workflow_id] = workflow
|
|
|
|
# Optional: In Datenbank speichern, falls verfügbar
|
|
if self.lucydom_interface:
|
|
try:
|
|
self.lucydom_interface.save_workflow_state(workflow)
|
|
logger.info(f"Workflow {workflow_id} in Datenbank gespeichert nach Laden aus Datei")
|
|
except Exception as e:
|
|
logger.warning(f"Fehler beim Speichern des Workflows {workflow_id} in Datenbank nach Laden aus Datei: {str(e)}")
|
|
|
|
logger.info(f"Workflow {workflow_id} aus Datei geladen: {workflow_path}")
|
|
return workflow
|
|
else:
|
|
logger.warning(f"Workflow {workflow_id} nicht gefunden: {workflow_path}")
|
|
raise WorkflowNotFoundError(f"Workflow {workflow_id} nicht gefunden")
|
|
except WorkflowNotFoundError:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden des Workflows {workflow_id} aus Datei: {str(e)}")
|
|
raise WorkflowError(f"Fehler beim Laden des Workflows: {str(e)}")
|
|
|
|
async def list_workflows(self, mandate_id: int = None, user_id: int = None) -> List[Dict[str, Any]]:
|
|
"""
|
|
Listet alle verfügbaren Workflows auf.
|
|
|
|
Args:
|
|
mandate_id: Optionale Mandanten-ID für die Filterung
|
|
user_id: Optionale Benutzer-ID für die Filterung
|
|
|
|
Returns:
|
|
Liste von Workflow-Zusammenfassungen
|
|
"""
|
|
workflows = []
|
|
|
|
# Aus Datenbank laden, falls verfügbar
|
|
if self.lucydom_interface:
|
|
try:
|
|
# Alle Workflows des Benutzers abrufen
|
|
if user_id is not None:
|
|
user_workflows = self.lucydom_interface.get_workflows_by_user(user_id)
|
|
else:
|
|
user_workflows = self.lucydom_interface.get_all_workflows()
|
|
|
|
# Nach Mandanten filtern, falls angegeben
|
|
if mandate_id is not None:
|
|
user_workflows = [wf for wf in user_workflows if wf.get("mandate_id") == mandate_id]
|
|
|
|
# Workflow-Zusammenfassungen erstellen
|
|
for workflow in user_workflows:
|
|
summary = {
|
|
"id": workflow.get("id"),
|
|
"name": workflow.get("name", f"Workflow {workflow.get('id')}"),
|
|
"status": workflow.get("status"),
|
|
"started_at": workflow.get("started_at"),
|
|
"last_activity": workflow.get("last_activity"),
|
|
"completed_at": workflow.get("completed_at")
|
|
}
|
|
|
|
# Nachrichtenanzahl hinzufügen, falls verfügbar
|
|
messages = self.lucydom_interface.get_workflow_messages(workflow.get("id"))
|
|
if messages:
|
|
summary["message_count"] = len(messages)
|
|
|
|
workflows.append(summary)
|
|
|
|
logger.info(f"Workflows aus Datenbank geladen: {len(workflows)}")
|
|
|
|
# Nach letzter Aktivität sortieren (neueste zuerst)
|
|
return sorted(workflows, key=lambda w: w.get("last_activity", ""), reverse=True)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Abrufen der Workflows aus Datenbank: {str(e)}")
|
|
|
|
# Aus Dateien laden, wenn keine Datenbank verfügbar oder ein Fehler aufgetreten ist
|
|
try:
|
|
for filename in os.listdir(self.results_dir):
|
|
if filename.startswith("workflow_") and filename.endswith(".json"):
|
|
workflow_path = os.path.join(self.results_dir, filename)
|
|
|
|
try:
|
|
with open(workflow_path, 'r', encoding='utf-8') as f:
|
|
workflow = json.load(f)
|
|
|
|
# Prüfen, ob Mandanten- und Benutzer-ID übereinstimmen
|
|
if mandate_id is not None and workflow.get("mandate_id") != mandate_id:
|
|
continue
|
|
|
|
if user_id is not None and workflow.get("user_id") != user_id:
|
|
continue
|
|
|
|
# Workflow-Zusammenfassung erstellen
|
|
summary = {
|
|
"id": workflow.get("id"),
|
|
"name": workflow.get("name", f"Workflow {workflow.get('id')}"),
|
|
"status": workflow.get("status"),
|
|
"started_at": workflow.get("started_at"),
|
|
"last_activity": workflow.get("last_activity"),
|
|
"message_count": len(workflow.get("messages", []))
|
|
}
|
|
|
|
workflows.append(summary)
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden der Workflow-Datei {filename}: {str(e)}")
|
|
|
|
logger.info(f"Workflows aus Dateien geladen: {len(workflows)}")
|
|
|
|
# Nach letzter Aktivität sortieren (neueste zuerst)
|
|
return sorted(workflows, key=lambda w: w.get("last_activity", ""), reverse=True)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Auflisten der Workflows: {str(e)}")
|
|
return []
|
|
|
|
async def delete_workflow(self, workflow_id: str) -> bool:
|
|
"""
|
|
Löscht einen Workflow.
|
|
|
|
Args:
|
|
workflow_id: ID des Workflows
|
|
|
|
Returns:
|
|
True bei Erfolg, False wenn der Workflow nicht existiert
|
|
"""
|
|
# Aus dem Speicher entfernen
|
|
if workflow_id in self.workflows:
|
|
del self.workflows[workflow_id]
|
|
|
|
# Aus der Datenbank löschen
|
|
if self.lucydom_interface:
|
|
try:
|
|
db_success = self.lucydom_interface.delete_workflow(workflow_id)
|
|
logger.info(f"Workflow {workflow_id} aus Datenbank gelöscht: {db_success}")
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Löschen des Workflows {workflow_id} aus Datenbank: {str(e)}")
|
|
|
|
# Datei löschen
|
|
workflow_path = os.path.join(self.results_dir, f"workflow_{workflow_id}.json")
|
|
|
|
try:
|
|
if os.path.exists(workflow_path):
|
|
os.remove(workflow_path)
|
|
logger.info(f"Workflow {workflow_id} aus Datei gelöscht: {workflow_path}")
|
|
return True
|
|
else:
|
|
logger.warning(f"Workflow {workflow_id} nicht gefunden: {workflow_path}")
|
|
return False
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Löschen der Workflow-Datei {workflow_id}: {str(e)}")
|
|
return False
|
|
|
|
def _initialize_workflow(self, workflow_id: str) -> Dict[str, Any]:
|
|
"""
|
|
Initialisiert einen neuen Workflow und speichert ihn in der Datenbank.
|
|
|
|
Args:
|
|
workflow_id: ID des Workflows
|
|
|
|
Returns:
|
|
Das initialisierte Workflow-Objekt
|
|
"""
|
|
current_time = datetime.now().isoformat()
|
|
|
|
# Vollständiges Workflow-Objekt gemäß dem Datenmodell erstellen
|
|
workflow = {
|
|
"id": workflow_id,
|
|
"name": f"Workflow {workflow_id}",
|
|
"mandate_id": self.mandate_id,
|
|
"user_id": self.user_id,
|
|
"status": "running",
|
|
"started_at": current_time,
|
|
"last_activity": current_time,
|
|
"current_round": 1,
|
|
|
|
# Vollständige Statistik-Struktur gemäß DataStats-Modell
|
|
"data_stats": {
|
|
"total_processing_time": 0.0,
|
|
"total_token_count": 0,
|
|
"total_bytes_sent": 0,
|
|
"total_bytes_received": 0
|
|
},
|
|
|
|
# Leere Arrays für Nachrichten und Logs
|
|
"messages": [],
|
|
"logs": []
|
|
}
|
|
|
|
print("DEBUG Init workflow")
|
|
# Log-Eintrag für den Start des Workflows
|
|
self._add_log(workflow, "Workflow gestartet", "info")
|
|
|
|
# Workflow in Datenbank speichern
|
|
if self.lucydom_interface:
|
|
try:
|
|
# Direktes Speichern des vollständigen Workflow-Objekts
|
|
self.lucydom_interface.save_workflow_state(workflow)
|
|
logger.info(f"Workflow {workflow_id} in Datenbank erstellt")
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Erstellen des Workflows {workflow_id} in Datenbank: {str(e)}")
|
|
|
|
# Workflow im Speicher cachen
|
|
self.workflows[workflow_id] = workflow
|
|
|
|
return workflow
|
|
|
|
|
|
async def stop_workflow(self, workflow_id: str) -> bool:
|
|
"""
|
|
Stoppt einen laufenden Workflow.
|
|
|
|
Args:
|
|
workflow_id: ID des zu stoppenden Workflows
|
|
|
|
Returns:
|
|
True bei Erfolg, False wenn der Workflow nicht existiert oder bereits beendet wurde
|
|
"""
|
|
try:
|
|
workflow = self.workflows.get(workflow_id)
|
|
|
|
if not workflow:
|
|
# Versuche den Workflow zu laden
|
|
workflow = await self.load_workflow(workflow_id)
|
|
if not workflow:
|
|
return False
|
|
|
|
# Wenn der Workflow nicht im Status 'running' oder 'completed' ist, beenden
|
|
if workflow.get("status") not in ["running", "completed"]:
|
|
return False
|
|
|
|
# Status auf 'stopped' setzen
|
|
workflow["status"] = "stopped"
|
|
workflow["last_activity"] = datetime.now().isoformat()
|
|
|
|
self._add_log(workflow, "Workflow wurde manuell gestoppt", "info")
|
|
|
|
# Workflow speichern
|
|
self._save_workflow(workflow)
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Stoppen des Workflows {workflow_id}: {str(e)}")
|
|
return False
|
|
|
|
async def _decide_agent_tasks(self, workflow: Dict[str, Any], message: Dict[str, Any], agents: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
"""
|
|
Entscheidet anhand der Nachricht und Agentenprofile, welche System-Agenten für welche Aufgaben
|
|
eingesetzt werden sollen. Der User-Agent wird später immer separat aufgerufen, daher nicht hier berücksichtigt.
|
|
|
|
Args:
|
|
message: Das zu verarbeitende Message-Objekt
|
|
agents: Verfügbare Agenten mit ihren Profilen
|
|
|
|
Returns:
|
|
Liste mit Aufgaben für System-Agenten (agent_id, prompt)
|
|
"""
|
|
workflow_id = message.get("workflow_id", "unknown")
|
|
|
|
try:
|
|
# Nur System-Agenten berücksichtigen, User-Agent ausfiltern
|
|
system_agents = {agent_id: agent for agent_id, agent in agents.items()
|
|
if agent.get('type') != 'user'}
|
|
|
|
# Wenn keine System-Agenten vorhanden sind, leere Liste zurückgeben
|
|
if not system_agents:
|
|
self._add_log(workflow_id, "Keine System-Agenten verfügbar", "info")
|
|
return []
|
|
|
|
# Erstelle einen Prompt für den OpenAI-Call
|
|
agent_descriptions = []
|
|
for agent_id, agent in system_agents.items():
|
|
# Informationen zum Antwortformat hinzufügen
|
|
result_format = agent.get('result_format', 'Text')
|
|
agent_descriptions.append(
|
|
f"ID: {agent_id}, Name: {agent['name']}, Typ: {agent['type']}, "
|
|
f"Beschreibung: {agent['description']}, Fähigkeiten: {agent['capabilities']}, "
|
|
f"Antwortformat: {result_format}"
|
|
)
|
|
|
|
agent_description_text = "\n".join(agent_descriptions)
|
|
|
|
# Prüfen, ob bereits ausgeführte Agenten im Kontext vorhanden sind
|
|
previous_agent_results = []
|
|
if "messages" in workflow: # Verwende workflow statt context
|
|
for prev_message in workflow.get("messages", []):
|
|
if prev_message.get("agent_type") and prev_message.get("agent_type") != "user":
|
|
previous_agent_results.append({
|
|
"agent_id": prev_message.get("agent_id", "unknown"),
|
|
"agent_type": prev_message.get("agent_type", "unknown"),
|
|
"result_format": prev_message.get("result_format", "Text"),
|
|
"sequence_no": prev_message.get("sequence_no", 0)
|
|
})
|
|
|
|
previous_results_text = ""
|
|
if previous_agent_results:
|
|
previous_results_text = "VORHERIGE AGENTEN-ERGEBNISSE:\n"
|
|
for result in previous_agent_results:
|
|
previous_results_text += (
|
|
f"Agent: {result['agent_id']}, Typ: {result['agent_type']}, "
|
|
f"Antwortformat: {result['result_format']}, Sequenz: {result['sequence_no']}\n"
|
|
)
|
|
|
|
# Nachrichteninhalt extrahieren
|
|
content = message.get("content", "")
|
|
|
|
# Dateien aus der Nachricht extrahieren
|
|
files = extract_files_from_message(message)
|
|
file_descriptions = []
|
|
|
|
for file in files:
|
|
file_desc = f"Name: {file.get('name', '')}, Typ: {file.get('content_type', '')}"
|
|
# Check if content exists and is not too large to include
|
|
if 'content' in file and isinstance(file.get('content'), str) and len(file.get('content', '')) <= 5000:
|
|
file_desc += f", Inhalt: {file.get('content', '')[:200]}..."
|
|
file_descriptions.append(file_desc)
|
|
|
|
file_description_text = "\n".join(file_descriptions) if file_descriptions else "Keine Dateien"
|
|
|
|
# Add log for the agent selection process
|
|
self._add_log(workflow_id, "Moderator analysiert die Anfrage und entscheidet über System-Agenten...", "info")
|
|
|
|
# Prompt für den OpenAI-Call erstellen
|
|
decision_prompt = f"""
|
|
Du bist der Workflow-Manager, der entscheidet, welche System-Agenten für eine Anfrage eingesetzt werden sollen.
|
|
|
|
VERFÜGBARE SYSTEM-AGENTEN:
|
|
{agent_description_text}
|
|
|
|
{previous_results_text}
|
|
|
|
BENUTZERANFRAGE:
|
|
{content}
|
|
|
|
DATEIEN:
|
|
{file_description_text}
|
|
|
|
ANWEISUNGEN:
|
|
1. Analysiere die Benutzeranfrage und die Dateien
|
|
2. Entscheide, welche System-Agenten benötigt werden
|
|
3. Berücksichtige die Antwortformate der Agenten bei der Auswahl
|
|
4. Wenn Du in der Lage bist, die BENUTZERANFRAGE direkt selbst zu beantworten, so kannst Du direkt die Antwort senden, wähle dazu FORMAT1. Andernfalls wähle die passenden System-Agenten (mindestens einen) aus und definiere für jeden seine spezifische Aufgabe, die Angaben gemäss FORMAT2.
|
|
5. Gib das Ergebnis als JSON-Array von Objekten zurück
|
|
6. Berücksichtige bei deiner Entscheidung den Kontext und Verlauf der Konversation
|
|
7. Wenn möglich, wähle Agenten so, dass das Ausgabeformat eines Agenten zum erwarteten Eingabeformat des nächsten Agenten passt
|
|
|
|
Antwortformat FORMAT1:
|
|
[
|
|
{{"agent_id": "moderator", "prompt": "Die direkte Beantwortung der BENUTZERANFRAGE"}},
|
|
]
|
|
|
|
Antwortformat FORMAT2:
|
|
[
|
|
{{"agent_id": "agent_id_1", "prompt": "Aufgabenbeschreibung für Agent 1", "expected_format": "Name des erwarteten Ausgabeformats"}},
|
|
{{"agent_id": "agent_id_2", "prompt": "Aufgabenbeschreibung für Agent 2", "expected_format": "Name des erwarteten Ausgabeformats"}}
|
|
]
|
|
|
|
WICHTIG:
|
|
- Füge keine weiteren Erklärungen hinzu, antworte nur mit dem JSON-Array
|
|
"""
|
|
|
|
# OpenAI-Call durchführen
|
|
content = await self.ai_service.call_api([{"role": "user", "content": decision_prompt}])
|
|
|
|
# Versuche, JSON zu parsen
|
|
import json
|
|
import re
|
|
|
|
# Suche nach JSON-Objekten in der Antwort
|
|
json_match = re.search(r'\[\s*{.*}\s*\]', content, re.DOTALL)
|
|
|
|
# Auch leere Arrays erkennen
|
|
if not json_match and "[]" in content:
|
|
# Leeres Array erkannt - keine System-Agenten ausgewählt
|
|
self._add_log(workflow_id, "Moderator hat entschieden, keine System-Agenten zu verwenden", "info")
|
|
return []
|
|
|
|
if json_match:
|
|
json_str = json_match.group(0)
|
|
try:
|
|
agent_tasks = json.loads(json_str)
|
|
|
|
# Validiere die Struktur und filtere User-Agent heraus (falls irrtümlich enthalten)
|
|
valid_tasks = []
|
|
for task in agent_tasks:
|
|
if "agent_id" not in task or "prompt" not in task:
|
|
self._add_log(workflow_id, f"Ungültiges Task-Format ignoriert: {task}", "warning")
|
|
continue
|
|
|
|
# Prüfe, ob der Agent existiert und kein User-Agent ist
|
|
if task["agent_id"] not in system_agents:
|
|
self._add_log(workflow_id, f"Agent '{task['agent_id']}' liefert eine direkte Antwort", "info")
|
|
|
|
# Füge expected_format hinzu, falls vorhanden
|
|
if "expected_format" in task:
|
|
# Logge das erwartete Format
|
|
self._add_log(workflow_id, f"Agent '{task['agent_id']}' erwartet Format: {task['expected_format']}", "info")
|
|
else:
|
|
# Default expected_format basierend auf dem Agent-Typ setzen
|
|
agent_info = system_agents.get(task["agent_id"], {})
|
|
task["expected_format"] = agent_info.get("result_format", "Text")
|
|
|
|
valid_tasks.append(task)
|
|
|
|
# Logge die Anzahl der ausgewählten Agenten
|
|
if valid_tasks:
|
|
self._add_log(workflow_id, f"Moderator hat {len(valid_tasks)} System-Agenten ausgewählt", "info")
|
|
else:
|
|
self._add_log(workflow_id, "Moderator hat keine passenden System-Agenten gefunden", "info")
|
|
|
|
logger.debug(f"Ausgewählte System-Agenten-Tasks: {valid_tasks}")
|
|
return valid_tasks
|
|
|
|
except json.JSONDecodeError as json_error:
|
|
self._add_log(workflow_id, f"Fehler beim Parsen des JSON: {str(json_error)}", "error")
|
|
logger.error(f"JSON Parse-Fehler: {str(json_error)}")
|
|
logger.error(f"Problematischer JSON-String: {json_str}")
|
|
|
|
# Keine Agenten zurückgeben bei Parsing-Fehler
|
|
return []
|
|
else:
|
|
# Kein JSON gefunden
|
|
self._add_log(workflow_id, "Moderator konnte keine Agenten-Auswahl treffen", "warning")
|
|
logger.warning("Kein gültiges JSON in der Moderator-Antwort gefunden")
|
|
return []
|
|
|
|
except Exception as e:
|
|
# Bei Fehlern keine Agenten zurückgeben, mit Logging
|
|
self._add_log(workflow_id, f"Fehler bei der Agent-Auswahl: {str(e)}", "error")
|
|
logger.error(f"Fehler bei der Agent-Auswahl: {str(e)}", exc_info=True)
|
|
return []
|
|
|
|
|
|
async def _execute_agent(self, workflow: Dict[str, Any], agent_id: str, prompt: str, expected_format: str = None) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Führt einen Agenten mit einem spezifischen Prompt aus.
|
|
|
|
Args:
|
|
workflow: Das Workflow-Objekt
|
|
agent_id: ID des auszuführenden Agenten
|
|
prompt: Prompt für den Agenten
|
|
expected_format: Erwartetes Format der Antwort (optional)
|
|
|
|
Returns:
|
|
Das Ergebnis des Agenten oder None bei Fehlern
|
|
"""
|
|
try:
|
|
# Agenten-Instanz holen
|
|
registry = AgentRegistry.get_instance()
|
|
agent = registry.get_agent(agent_id)
|
|
|
|
if not agent:
|
|
self._add_log(workflow, f"Agent '{agent_id}' nicht gefunden", "error")
|
|
return None
|
|
|
|
# Message-Objekt für den Agenten erstellen
|
|
agent_message = {
|
|
"role": "user",
|
|
"content": prompt,
|
|
"workflow_id": workflow["id"]
|
|
}
|
|
|
|
# Kontext mit erwartetem Format erstellen
|
|
context = {"expected_format": expected_format} if expected_format else {}
|
|
|
|
# Agenten ausführen
|
|
self._add_log(workflow, f"Agent '{agent_id}' wird ausgeführt", "info")
|
|
result = await agent.process_message(agent_message, context)
|
|
|
|
# Prüfen, ob das Ergebnis das erwartete Format hat
|
|
result_format = result.get("result_format")
|
|
if expected_format and result_format and expected_format != result_format:
|
|
self._add_log(
|
|
workflow,
|
|
f"Warnung: Agent '{agent_id}' hat Format '{result_format}' geliefert, aber '{expected_format}' wurde erwartet",
|
|
"warning"
|
|
)
|
|
|
|
# Agenten-Antwort als neue Nachricht zum Workflow hinzufügen
|
|
agent_response_message = self._create_message(workflow["id"], "assistant")
|
|
agent_response_message["content"] = result.get("content", "")
|
|
agent_response_message["agent_type"] = agent.type
|
|
agent_response_message["agent_id"] = agent_id
|
|
agent_response_message["agent_name"] = agent.name
|
|
agent_response_message["result_format"] = result.get("result_format", agent.result_format)
|
|
|
|
# Nachricht zum Workflow hinzufügen
|
|
workflow["messages"].append(agent_response_message)
|
|
|
|
|
|
# Nachricht abschließen und in der Datenbank speichern
|
|
self._finalize_last_message(workflow)
|
|
|
|
# Workflow-Zustand speichern
|
|
self._save_workflow(workflow)
|
|
|
|
# Ergebnis formatieren und zurückgeben
|
|
agent_result = {
|
|
"agent_id": agent_id,
|
|
"agent_name": agent.name,
|
|
"content": result.get("content", ""),
|
|
"agent_type": agent.type
|
|
}
|
|
|
|
self._add_log(workflow, f"Agent '{agent_id}' hat geantwortet", "info")
|
|
|
|
return agent_result
|
|
|
|
except Exception as e:
|
|
self._add_log(workflow, f"Fehler bei der Ausführung von Agent '{agent_id}': {str(e)}", "error")
|
|
return None
|
|
|
|
|
|
async def _create_summary(self, agent_results: List[Dict[str, Any]]) -> str:
|
|
"""
|
|
Erstellt eine Zusammenfassung der Agentenergebnisse.
|
|
|
|
Args:
|
|
agent_results: Liste der Agentenergebnisse
|
|
|
|
Returns:
|
|
Zusammenfassung als Text
|
|
"""
|
|
if not agent_results:
|
|
return "Keine Agentenergebnisse verfügbar."
|
|
|
|
# Kombiniere die Ergebnisse in einen Kontext
|
|
context = ""
|
|
for result in agent_results:
|
|
agent_name = result.get("agent_name", "Unbekannter Agent")
|
|
content = result.get("content", "")
|
|
|
|
context += f"--- {agent_name} ---\n{content}\n\n"
|
|
|
|
# Prompt für die Zusammenfassung
|
|
summary_prompt = f"""
|
|
Erstelle eine aussagekräftige Zusammenfassung der folgenden Agentenergebnisse.
|
|
Organisiere die Informationen strukturiert und vermeide Redundanzen.
|
|
Behalte alle wichtigen Erkenntnisse und Empfehlungen bei.
|
|
|
|
{context}
|
|
"""
|
|
|
|
# OpenAI-Call für die Zusammenfassung
|
|
try:
|
|
summary = await self.ai_service.call_api([{"role": "user", "content": summary_prompt}])
|
|
return summary
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei der Erstellung der Zusammenfassung: {str(e)}")
|
|
return "Fehler bei der Erstellung der Zusammenfassung. Bitte die individuellen Agentenergebnisse beachten."
|
|
|
|
def _add_log(self, workflow: Dict[str, Any], message: str, log_type: str, agent_id: Optional[str] = None, agent_name: Optional[str] = None) -> None:
|
|
"""
|
|
Fügt einen Log-Eintrag zum Workflow hinzu und speichert ihn in der Datenbank.
|
|
"""
|
|
# First, check if workflow is a string (ID) instead of dictionary
|
|
if isinstance(workflow, str):
|
|
# Try to load the workflow by ID
|
|
workflow_id = workflow
|
|
workflow = self.workflows.get(workflow_id)
|
|
if not workflow:
|
|
# Just log to the logger and return
|
|
logger.info(f"Log (couldn't add to workflow {workflow_id}): {log_type} - {message}")
|
|
return
|
|
# Check if workflow is a dictionary
|
|
if not isinstance(workflow, dict):
|
|
logger.error(f"Invalid workflow type: {type(workflow)}. Expected dictionary.")
|
|
# Just log to the logger and return
|
|
logger.info(f"Log (couldn't add to workflow): {log_type} - {message}")
|
|
return
|
|
|
|
# Continue with the rest of the function if workflow is a dictionary
|
|
|
|
log_entry = {
|
|
"id": f"log_{uuid.uuid4()}",
|
|
"message": message,
|
|
"type": log_type,
|
|
"timestamp": datetime.now().isoformat(),
|
|
"agent_id": agent_id,
|
|
"agent_name": agent_name
|
|
}
|
|
|
|
# Log-Eintrag zum Workflow hinzufügen
|
|
if "logs" not in workflow:
|
|
workflow["logs"] = []
|
|
|
|
workflow["logs"].append(log_entry)
|
|
|
|
# Letzte Aktivität aktualisieren
|
|
workflow["last_activity"] = log_entry["timestamp"]
|
|
# Log-Eintrag in Datenbank speichern, falls verfügbar
|
|
if self.lucydom_interface:
|
|
try:
|
|
# Workflow-ID zum Log-Eintrag hinzufügen
|
|
log_data = log_entry.copy()
|
|
log_data["workflow_id"] = workflow["id"]
|
|
|
|
self.lucydom_interface.create_workflow_log(log_data)
|
|
logger.debug(f"Log-Eintrag für Workflow {workflow['id']} in Datenbank gespeichert")
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Speichern des Log-Eintrags für Workflow {workflow['id']} in Datenbank: {str(e)}")
|
|
logger.info(f"Workflow {workflow['id']}: {message}")
|
|
|
|
|
|
def _create_message(self, workflow_id: str, role: str = "system", parent_message_id: str = None) -> Dict[str, Any]:
|
|
"""
|
|
Erstellt ein neues Message-Objekt und speichert es in der Datenbank.
|
|
|
|
Args:
|
|
workflow_id: ID des Workflows
|
|
role: Rolle der Nachricht ('system', 'user', 'assistant')
|
|
parent_message_id: ID der Elternnachricht (optional)
|
|
|
|
Returns:
|
|
Das erstellte Message-Objekt
|
|
"""
|
|
workflow = self.workflows.get(workflow_id)
|
|
|
|
# Sequence-Nummer bestimmen
|
|
sequence_no = 1
|
|
if workflow and workflow.get("messages"):
|
|
sequence_no = len(workflow["messages"]) + 1
|
|
|
|
# Aktuelle Zeit
|
|
current_time = datetime.now().isoformat()
|
|
|
|
# Ensure a unique ID for the message
|
|
message_id = f"msg_{uuid.uuid4()}"
|
|
|
|
# Message-Objekt erstellen
|
|
message = {
|
|
"id": message_id,
|
|
"workflow_id": workflow_id,
|
|
"parent_message_id": parent_message_id,
|
|
"started_at": current_time,
|
|
"finished_at": None,
|
|
"sequence_no": sequence_no,
|
|
|
|
"status": "pending",
|
|
"role": role,
|
|
|
|
"data_stats": {
|
|
"processing_time": 0.0,
|
|
"token_count": 0,
|
|
"bytes_sent": 0,
|
|
"bytes_received": 0
|
|
},
|
|
|
|
"documents": [], # Initialize empty documents array
|
|
"content": None,
|
|
"agent_type": None
|
|
}
|
|
|
|
# In Datenbank speichern, falls verfügbar
|
|
if self.lucydom_interface:
|
|
try:
|
|
# Include all fields in the database version
|
|
message_data = {
|
|
"id": message_id,
|
|
"workflow_id": workflow_id,
|
|
"sequence_no": sequence_no,
|
|
"role": role,
|
|
"content": None,
|
|
"agent_type": None,
|
|
"created_at": current_time,
|
|
# IMPORTANT: Include documents field
|
|
"documents": []
|
|
}
|
|
|
|
# Log the message creation
|
|
logger.debug(f"Creating new message in database: {message_data}")
|
|
|
|
result = self.lucydom_interface.create_workflow_message(message_data)
|
|
if result:
|
|
logger.debug(f"Nachricht für Workflow {workflow_id} in Datenbank erstellt mit ID: {message_id}")
|
|
else:
|
|
logger.warning(f"Fehler beim Erstellen der Nachricht für Workflow {workflow_id} in Datenbank")
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Erstellen der Nachricht für Workflow {workflow_id} in Datenbank: {str(e)}")
|
|
|
|
return message
|
|
|
|
|
|
def _finalize_last_message(self, workflow: Dict[str, Any]) -> None:
|
|
"""
|
|
Schließt die letzte Nachricht im Workflow ab und aktualisiert sie in der Datenbank.
|
|
|
|
Args:
|
|
workflow: Das Workflow-Objekt
|
|
"""
|
|
if not workflow.get("messages"):
|
|
return
|
|
|
|
last_message = workflow["messages"][-1]
|
|
if last_message.get("finished_at") is None:
|
|
last_message["finished_at"] = datetime.now().isoformat()
|
|
last_message["status"] = "completed"
|
|
|
|
# In Datenbank aktualisieren, falls verfügbar
|
|
if self.lucydom_interface:
|
|
try:
|
|
message_id = last_message.get("id")
|
|
if not message_id:
|
|
logger.warning(f"Keine ID für letzte Nachricht in Workflow {workflow['id']} gefunden")
|
|
return
|
|
|
|
# Only extract fields that are expected in the database model
|
|
# Make sure all required fields have values with proper defaults
|
|
message_data = {
|
|
"id": message_id,
|
|
"workflow_id": workflow.get("id", ""),
|
|
"sequence_no": last_message.get("sequence_no", 0),
|
|
"role": last_message.get("role", "unknown"),
|
|
"content": last_message.get("content", ""),
|
|
"agent_type": last_message.get("agent_type", ""),
|
|
"created_at": last_message.get("started_at", datetime.now().isoformat()),
|
|
# IMPORTANT: Include the documents array
|
|
"documents": last_message.get("documents", [])
|
|
}
|
|
|
|
# Log the message data for debugging
|
|
logger.debug(f"Updating message in database with data: {message_data}")
|
|
|
|
# Nachricht in Datenbank aktualisieren
|
|
self.lucydom_interface.update_workflow_message(message_id, message_data)
|
|
logger.debug(f"Nachricht {message_id} für Workflow {workflow['id']} in Datenbank aktualisiert (mit Dokumenten)")
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Aktualisieren der Nachricht für Workflow {workflow['id']} in Datenbank: {str(e)}")
|
|
|
|
|
|
def get_workflow_status(self, workflow_id: str) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Gibt den Status eines Workflows zurück.
|
|
|
|
Args:
|
|
workflow_id: ID des Workflows
|
|
|
|
Returns:
|
|
Dictionary mit Status-Informationen oder None, wenn der Workflow nicht existiert
|
|
"""
|
|
# Aus dem Speicher abrufen
|
|
workflow = self.workflows.get(workflow_id)
|
|
|
|
# Falls nicht im Speicher, aus der Datenbank oder Datei laden
|
|
if not workflow:
|
|
# Aus Datenbank laden, falls verfügbar
|
|
if self.lucydom_interface:
|
|
try:
|
|
workflow_data = self.lucydom_interface.get_workflow(workflow_id)
|
|
if workflow_data:
|
|
workflow = workflow_data
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden des Workflow-Status aus Datenbank: {str(e)}")
|
|
|
|
# Falls nicht in der Datenbank, aus Datei laden
|
|
if not workflow:
|
|
try:
|
|
workflow_path = os.path.join(self.results_dir, f"workflow_{workflow_id}.json")
|
|
if os.path.exists(workflow_path):
|
|
with open(workflow_path, 'r', encoding='utf-8') as f:
|
|
workflow = json.load(f)
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden des Workflow-Status aus Datei: {str(e)}")
|
|
return None
|
|
|
|
if not workflow:
|
|
return None
|
|
|
|
# Status-Informationen extrahieren
|
|
status_info = {
|
|
"id": workflow.get("id"),
|
|
"name": workflow.get("name", f"Workflow {workflow_id}"),
|
|
"status": workflow.get("status"),
|
|
"progress": 1.0 if workflow.get("status") in ["completed", "failed", "stopped"] else 0.5,
|
|
"started_at": workflow.get("started_at"),
|
|
"last_activity": workflow.get("last_activity"),
|
|
"workflow_complete": workflow.get("status") == "completed", # Add this instead
|
|
"current_round": workflow.get("current_round", 1),
|
|
"data_stats": workflow.get("data_stats", {
|
|
"total_processing_time": 0.0,
|
|
"total_token_count": 0,
|
|
"total_bytes_sent": 0,
|
|
"total_bytes_received": 0
|
|
})
|
|
}
|
|
|
|
return status_info
|
|
|
|
def get_workflow_logs(self, workflow_id: str) -> Optional[List[Dict[str, Any]]]:
|
|
"""
|
|
Gibt die Logs eines Workflows zurück.
|
|
|
|
Args:
|
|
workflow_id: ID des Workflows
|
|
|
|
Returns:
|
|
Liste der Logs oder None, wenn der Workflow nicht existiert
|
|
"""
|
|
# Aus dem Speicher abrufen
|
|
workflow = self.workflows.get(workflow_id)
|
|
|
|
# Falls nicht im Speicher, aus der Datenbank laden
|
|
if not workflow and self.lucydom_interface:
|
|
try:
|
|
logs = self.lucydom_interface.get_workflow_logs(workflow_id)
|
|
return logs
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden der Workflow-Logs aus Datenbank: {str(e)}")
|
|
|
|
# Falls nicht in der Datenbank oder kein Interface verfügbar, aus Datei laden
|
|
if not workflow:
|
|
try:
|
|
workflow_path = os.path.join(self.results_dir, f"workflow_{workflow_id}.json")
|
|
if os.path.exists(workflow_path):
|
|
with open(workflow_path, 'r', encoding='utf-8') as f:
|
|
workflow = json.load(f)
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden der Workflow-Logs aus Datei: {str(e)}")
|
|
return None
|
|
|
|
return workflow.get("logs", []) if workflow else None
|
|
|
|
def get_workflow_messages(self, workflow_id: str) -> Optional[List[Dict[str, Any]]]:
|
|
"""
|
|
Gibt die Nachrichten eines Workflows zurück.
|
|
|
|
Args:
|
|
workflow_id: ID des Workflows
|
|
|
|
Returns:
|
|
Liste der Nachrichten oder None, wenn der Workflow nicht existiert
|
|
"""
|
|
# Aus dem Speicher abrufen
|
|
workflow = self.workflows.get(workflow_id)
|
|
|
|
# Falls nicht im Speicher, aus der Datenbank laden
|
|
if not workflow and self.lucydom_interface:
|
|
try:
|
|
messages = self.lucydom_interface.get_workflow_messages(workflow_id)
|
|
return messages
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden der Workflow-Nachrichten aus Datenbank: {str(e)}")
|
|
|
|
# Falls nicht in der Datenbank oder kein Interface verfügbar, aus Datei laden
|
|
if not workflow:
|
|
try:
|
|
workflow_path = os.path.join(self.results_dir, f"workflow_{workflow_id}.json")
|
|
if os.path.exists(workflow_path):
|
|
with open(workflow_path, 'r', encoding='utf-8') as f:
|
|
workflow = json.load(f)
|
|
except Exception as e:
|
|
logger.error(f"Fehler beim Laden der Workflow-Nachrichten aus Datei: {str(e)}")
|
|
return None
|
|
|
|
return workflow.get("messages", []) if workflow else None
|
|
|
|
# Anpassen der Factory-Funktion für den WorkflowManager
|
|
def get_workflow_manager(mandate_id: int = None, user_id: int = None, ai_service = None):
|
|
"""
|
|
Gibt eine WorkflowManager-Instanz für den angegebenen Kontext zurück.
|
|
Wiederverwendet bestehende Instanzen.
|
|
|
|
Args:
|
|
mandate_id: ID des Mandanten
|
|
user_id: ID des Benutzers
|
|
ai_service: Service für KI-Anfragen
|
|
|
|
Returns:
|
|
Eine WorkflowManager-Instanz
|
|
"""
|
|
from modules.lucydom_interface import get_lucydom_interface
|
|
|
|
context_key = f"{mandate_id}_{user_id}"
|
|
|
|
# LucyDOM-Interface für Datenbankzugriffe
|
|
lucydom_interface = get_lucydom_interface(mandate_id, user_id)
|
|
|
|
if context_key not in _workflow_managers:
|
|
_workflow_managers[context_key] = WorkflowManager(
|
|
mandate_id,
|
|
user_id,
|
|
ai_service,
|
|
lucydom_interface
|
|
)
|
|
|
|
# Aktualisiere die Services, falls sie geändert wurden
|
|
if ai_service is not None:
|
|
_workflow_managers[context_key].ai_service = ai_service
|
|
|
|
return _workflow_managers[context_key]
|
|
|
|
|
|
# Singleton-Factory für WorkflowManager-Instanzen pro Kontext
|
|
_workflow_managers = {} |