gateway/gwserver/_old_bk_modules/agentservice_workflow_manager.py
2025-04-11 23:39:10 +02:00

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 = {}