""" ChatManager Modul zur Verwaltung von AI-Chat-Workflows. Implementiert eine kompakte und modulare Architektur für die Verarbeitung von Benutzeranfragen, Agentenausführung und Ergebnisformatierung. """ import logging import json import uuid from datetime import datetime from typing import Dict, Any, List, Optional, Union # Notwendige Importe from connectors.connector_aichat_openai import ChatService from modules.chat_registry import get_agent_registry from modules.lucydom_interface import get_lucydom_interface # Logger konfigurieren logger = logging.getLogger(__name__) class ChatManager: """ Verwaltet die Verarbeitung von Chat-Anfragen, Agentenausführung und die Integration von Ergebnissen in den Workflow. """ def __init__(self, mandate_id: int, user_id: int): """ Initialisiert den ChatManager mit Mandanten- und Benutzerkontext. Args: mandate_id: ID des aktuellen Mandanten user_id: ID des aktuellen Benutzers """ self.mandate_id = mandate_id self.user_id = user_id self.ai_service = ChatService() self.lucy_interface = get_lucydom_interface(mandate_id, user_id) self.agent_registry = get_agent_registry() ### Chat Management async def chat_run(self, user_input: Dict[str, Any], workflow_id: Optional[str] = None) -> Dict[str, Any]: """ Hauptfunktion zur Integration von Benutzeranfragen in den Workflow. Args: user_input, which will be parsed to message_user: Message-Objekt mit Benutzeranfrage und Dokumenten workflow_id: Optional - ID des Workflows (None für neue Workflows) Returns: Workflow-Objekt mit aktualisiertem Zustand """ logger.info(f"User message object: {self.parse_json2text(message_user)}") # 0. User-Input mit file id's in Message User als message object transformieren und alle contents vorbereiten message_user = self.chat_user_message_integration(user_input) # 1. Workflow initialisieren oder bestehenden laden workflow = self.workflow_init(workflow_id) # 2. Benutzer-Message im Workflow speichern self.message_add(workflow, message_user) # 3. Projektleiter-Prompt erstellen und Antwort analysieren project_manager_response = await self.chat_prompt(message_user, workflow) # 3.1. Extrahiere die benötigten Informationen aus der Antwort obj_answer = project_manager_response.get("obj_answer", []) obj_workplan = project_manager_response.get("obj_workplan", []) user_response = project_manager_response.get("user_response", "") # 3.2. Speichere die Antwort als Message im Workflow und füge Log-Einträge hinzu response_message = { "role": "assistant", "agent_type": "project_manager", "content": user_response } self.message_add(workflow, response_message) # 3.3. Log-Eintrag für den Workplan und die geplanten Ergebnisse self.log_add(workflow, f"Arbeitsplan: {self.parse_json2text(obj_workplan)}") self.log_add(workflow, f"Geplante Ergebnisse: {self.parse_json2text(obj_answer)}") # 4. Agenten gemäss Workplan ausführen obj_results = [] if obj_workplan: for task in obj_workplan: # Informiere Benutzer über aktuellen Schritt agent_name = task.get("agent") step_info = f"Führe Agent '{agent_name}' aus um {', '.join([d.get('label') for d in task.get('doc_output', [])])} zu erstellen" self.log_add(workflow, step_info) # Bereite Eingabedokumente für den Agenten vor input_docs = self.agent_input_documents(task.get('doc_input', []), workflow) # Führe den Agenten aus agent_results = await self.agent_execute( agent_name=agent_name, prompt=task.get("prompt", ""), input_docs=input_docs, output_format=task.get("doc_output", []) ) # Sammle Ergebnisse obj_results.extend(agent_results) # Speichere Zwischenergebnisse for result in agent_results: self.log_add(workflow, f"Ergebnis erstellt: {result.get('label')}") # 5. Erstelle die finale Antwort mit den gesammelten Dokumenten final_message = self.chat_final_message(user_response, obj_results, obj_answer) self.message_add(workflow, final_message) # 6. Finalisiere den Workflow self.workflow_finish(workflow) return workflow async def chat_prompt(self, message_user: Dict[str, Any], workflow: Dict[str, Any]) -> Dict[str, Any]: """ Erstellt den Prompt für den Projektleiter und verarbeitet seine Antwort. Args: message_user: Message-Objekt mit Benutzeranfrage workflow: Aktuelles Workflow-Objekt Returns: Antwort des Projektleiters mit obj_answer, obj_workplan und user_response """ # Verfügbare Dokumenttypen aus der Funktion holen doc_types = self.document_types_accepted() doc_types_str = ", ".join(doc_types) # Verfügbare Agenten mit ihren Fähigkeiten abrufen available_agents = self.agent_profiles() # Erstelle eine Zusammenfassung des Workflows workflow_summary = await self.workflow_summarize(workflow, "Fasse den bisherigen Verlauf kurz und prägnant zusammen") # Erstelle eine Zusammenfassung der vom Benutzer bereitgestellten Dokumente user_docs_summary = await self.message_summarize_documents(message_user, "Fasse den Inhalt des Dokuments kurz zusammen") # Liste der aktuell verfügbaren Dokumente aus User-Input oder bereits generierten Dokumenten erstellen available_documents = self.available_documents_get(message_user, workflow) available_docs_str = self.available_documents_format(available_documents) # Erstelle den Prompt für den Projektleiter prompt = f""" Basierend auf der Benutzeranfrage: "{message_user.get('content')}" und den bereitgestellten Dokumenten, analysiere bitte die Anforderungen und erstelle einen Plan zur Bearbeitung. # Bisheriger Konversationsverlauf: {workflow_summary} # Vom Benutzer bereitgestellte Dokumente: {user_docs_summary} # Verfügbare Dokumente (aktuell im Workflow): {available_docs_str} # Verfügbare Dokumenttypen: {doc_types_str} # Verfügbare Agenten und ihre Fähigkeiten: {self.parse_json2text(available_agents)} Bitte analysiere die Anfrage und erstelle: 1. Eine Liste benötigter Ergebnisdokumente (obj_answer) 2. Einen Plan für die Ausführung von Agenten (obj_workplan) 3. Eine verständliche Antwort an den Benutzer ## WICHTIGE REGELN FÜR DEN ARBEITSPLAN: 1. Jedes Eingabedokument muss entweder bereits vorhanden sein (vom Benutzer bereitgestellt oder vorher von einem Agenten erzeugt) oder von einem Agenten erstellt werden, bevor es verwendet wird. 2. Wenn nötig, konvertiere Eingabedokumente durch Agenten in ein passendes Format, wenn der Typ nicht übereinstimmt. 3. Definiere keine Dokument-Inputs, die nicht existieren oder nicht vorab generiert werden. 4. Erstelle eine logische Reihenfolge - frühere Agenten können Dokumente erzeugen, die später als Eingaben verwendet werden. 5. Wenn der Benutzer Dokumente bereitgestellt hat, nutze diese kreativ, auch wenn sie nicht exakt dem gewünschten Typ entsprechen. Antworte in folgendem JSON-Format: {{ "obj_answer": [ {{ "label": "eindeutiger_dokumentname", "doc_type": "{doc_types[0]}", # Einer der verfügbaren Dokumenttypen: {doc_types_str} "summary": "Beschreibung des Dokumentinhalts" }} ], "obj_workplan": [ {{ "agent": "agent_name", # Name eines verfügbaren Agenten "doc_output": [ {{ "label": "eindeutiger_dokumentname", "doc_type": "{doc_types[0]}" # Einer der verfügbaren Dokumenttypen: {doc_types_str} }} ], "prompt": "Anweisungen für den Agenten", "doc_input": [ {{ "label": "eindeutiger_dokumentname", "doc_type": "{doc_types[0]}" # Einer der verfügbaren Dokumenttypen: {doc_types_str} }} ] # Falls keine Eingabedokumente benötigt werden, kann "doc_input" leer bleiben oder weggelassen werden }} # Mehrere Agent-Tasks können hier hinzugefügt werden und sollten logisch aufeinander aufbauen ], "user_response": "Klare Erklärung für den Benutzer, was als nächstes passiert" }} """ # Rufe den AI-Service auf, um die Antwort des Projektleiters zu erhalten project_manager_output = await self.ai_service.call_api([ {"role": "system", "content": "Du bist ein erfahrener Projektleiter, der Benutzeranfragen analysiert und Arbeitspläne erstellt. Du achtest sehr sorgfältig darauf, dass alle Dokument-Abhängigkeiten korrekt sind und keine nicht existierenden Dokumente als Eingaben definiert werden."}, {"role": "user", "content": prompt} ]) # Parsen der JSON-Antwort return self.parse_json_response(project_manager_output) def chat_user_message_integration(self, user_input: Dict[str, Any]) -> Dict[str, Any]: # Nachrichteninhalt überprüfen message_content = user_input.get("message", "") if isinstance(message_content, dict) and "content" in message_content: message_content = message_content["content"] # Wenn Nachrichteninhalt leer ist, kein Chat if message_content is None or message_content.strip() == "": logger.warning(f"Leere Nachricht, kein Chat") message_content = "(No user input received)" # Zusätzliche Dateien verarbeiten additional_fileids = user_input.get("additional_fileids", []) additional_files = self.process_file_ids(additional_fileids) # Nachrichtenobjekt erstellen message_object = { "role": "user", "content": message_content, "documents": additional_files } return message_object def chat_final_message(self, user_response: str, obj_results: List[Dict[str, Any]], obj_answer: List[Dict[str, Any]]) -> Dict[str, Any]: """ Erstellt die finale Antwortnachricht mit Dokumenten. Args: user_response: Textantwort an den Benutzer obj_results: Liste der erzeugten Ergebnisdokumente obj_answer: Liste der erwarteten Antwortdokumente Returns: Vollständiges Message-Objekt mit Inhalt und Dokumenten """ # Grundlegende Nachrichtenstruktur erstellen final_message = { "role": "assistant", "agent_type": "final_responder", "content": user_response, "documents": [] } # Dokumente vom Typ "text" in die Antwort integrieren text_parts = [user_response] for doc in obj_results: doc_label = None doc_contents = [] for content in doc.get("contents", []): if content.get("label"): doc_label = content.get("label") # Suche nach dem passenden doc_type in obj_answer target_type = None for answer_spec in obj_answer: if answer_spec.get("label") == doc_label: target_type = answer_spec.get("doc_type") break # Wenn der Content vom Typ "text" ist und integriert werden soll if content.get("type") == "text" and content.get("text"): text = content.get("text") text_parts.append(f"\n\n--- {doc_label} ---\n{text}") doc_contents.append(content) # Füge das Dokument zur finalen Nachricht hinzu if doc_contents: final_message["documents"].append({ "id": doc.get("id", f"doc_{str(uuid.uuid4())}"), "source": doc.get("source", {"type": "agent", "name": "Response Generator"}), "contents": doc_contents }) # Aktualisiere den Nachrichteninhalt mit integrierten Texten final_message["content"] = "\n".join(text_parts) return final_message ### Workflow def workflow_init(self, workflow_id: Optional[str] = None) -> Dict[str, Any]: """ Initialisiert einen Workflow oder lädt einen bestehenden. Args: workflow_id: Optional - ID des zu ladenden Workflows Returns: Initialisiertes Workflow-Objekt """ current_time = datetime.now().isoformat() if workflow_id is None or not self.lucy_interface.get_workflow(workflow_id): # Neuen Workflow erstellen new_workflow_id = str(uuid.uuid4()) if workflow_id is None else workflow_id workflow = { "id": new_workflow_id, "mandate_id": self.mandate_id, "user_id": self.user_id, "name": f"Workflow {new_workflow_id[:8]}", "status": "running", "started_at": current_time, "last_activity": current_time, "current_round": 1, "waiting_for_user": False, "messages": [], "logs": [], "data_stats": {} } # In Datenbank speichern self.lucy_interface.create_workflow(workflow) return workflow else: # Bestehenden Workflow laden workflow = self.lucy_interface.load_workflow_state(workflow_id) # Status aktualisieren workflow["status"] = "running" workflow["last_activity"] = current_time workflow["waiting_for_user"] = False # In Datenbank aktualisieren self.lucy_interface.save_workflow_state(workflow) return workflow async def workflow_summarize(self, workflow: Dict[str, Any], prompt: str) -> str: """ Erstellt eine Zusammenfassung des Workflows. Args: workflow: Workflow-Objekt prompt: Anweisungen zur Erstellung der Zusammenfassung Returns: Zusammenfassung des Workflows """ if not workflow or "messages" not in workflow or not workflow["messages"]: return "Keine vorherigen Nachrichten im Workflow vorhanden." # Nachrichten in umgekehrter Reihenfolge durchgehen (neueste zuerst) messages = sorted(workflow["messages"], key=lambda m: m.get("sequence_no", 0), reverse=True) summary_parts = [] for message in messages: message_summary = await self.message_summarize(message, prompt) summary_parts.append(message_summary) return "\n\n".join(summary_parts) def workflow_finish(self, workflow: Dict[str, Any]) -> Dict[str, Any]: """ Finalisiert einen Workflow und setzt den Status auf 'stopped'. Args: workflow: Workflow-Objekt Returns: Aktualisiertes Workflow-Objekt """ workflow["status"] = "completed" workflow["last_activity"] = datetime.now().isoformat() workflow["waiting_for_user"] = True # In Datenbank speichern self.lucy_interface.save_workflow_state(workflow) return workflow ### Agents def agent_profiles(self) -> List[Dict[str, Any]]: """ Ruft Informationen über alle verfügbaren Agenten ab. Returns: Liste mit Informationen über alle verfügbaren Agenten """ return self.agent_registry.get_agent_infos() def agent_input_documents(self, doc_input_list: List[Dict[str, Any]], workflow: Dict[str, Any]) -> List[Dict[str, Any]]: """ Bereitet Eingabedokumente für einen Agenten vor. Args: doc_input_list: Liste der benötigten Eingabedokumente workflow: Workflow-Objekt Returns: Aufbereitete Eingabedokumente für den Agenten """ prepared_inputs = [] for doc_spec in doc_input_list: doc_label = doc_spec.get("label") doc_type = doc_spec.get("doc_type") found_doc = None # Durchsuche alle Nachrichten nach dem gesuchten Dokument for message in workflow.get("messages", []): for doc in message.get("documents", []): # Dokument anhand des Labels identifizieren if any(content.get("label") == doc_label for content in doc.get("contents", [])): found_doc = doc break if found_doc: break if found_doc: prepared_inputs.append(found_doc) return prepared_inputs async def agent_execute(self, agent_name: str, prompt: str, input_docs: List[Dict[str, Any]], output_format: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """ Führt einen Agenten mit den angegebenen Parametern aus. Args: agent_name: Name des auszuführenden Agenten prompt: Prompt für den Agenten input_docs: Eingabedokumente output_format: Erwartetes Ausgabeformat Returns: Liste der vom Agenten erzeugten Ergebnisdokumente """ # Hole den Agenten aus der Registry agent = self.agent_registry.get_agent(agent_name) if not agent: logger.error(f"Agent '{agent_name}' nicht gefunden") return [] try: # Erstelle die Agenten-Anfrage agent_request = { "role": "user", "content": prompt, "documents": input_docs } # Führe den Agenten aus agent_response = await agent.process_message(agent_request, {"expected_format": output_format}) # Extrahiere die erzeugten Dokumente results = agent_response.get("documents", []) # Benenne die Dokumente gemäß des angegebenen Ausgabeformats for i, format_spec in enumerate(output_format): if i < len(results): for content in results[i].get("contents", []): content["label"] = format_spec.get("label") content["type"] = format_spec.get("doc_type") return results except Exception as e: logger.error(f"Fehler bei Ausführung von Agent '{agent_name}': {str(e)}") return [] ### Messages def message_add(self, workflow: Dict[str, Any], message: Dict[str, Any]) -> str: """ Fügt eine Nachricht zum Workflow hinzu und aktualisiert last_activity. Args: workflow: Workflow-Objekt message: Zu speichernde Nachricht Returns: ID der hinzugefügten Nachricht """ current_time = datetime.now().isoformat() # Sicherstellen, dass Messages-Liste existiert if "messages" not in workflow: workflow["messages"] = [] # Neue Nachrichten-ID generieren, falls nicht vorhanden if "id" not in message: message["id"] = f"msg_{str(uuid.uuid4())}" # Workflow-ID und Zeitstempel hinzufügen message["workflow_id"] = workflow["id"] message["started_at"] = current_time message["finished_at"] = current_time # Sequenznummer setzen message["sequence_no"] = len(workflow["messages"]) + 1 # Status setzen message["status"] = "completed" # Message zum Workflow hinzufügen workflow["messages"].append(message) # Workflow-Status aktualisieren workflow["last_activity"] = current_time workflow["last_message_id"] = message["id"] # In Datenbank speichern self.lucy_interface.create_workflow_message(message) return message["id"] async def message_summarize(self, message: Dict[str, Any], prompt: str) -> str: """ Erstellt eine Zusammenfassung einer Nachricht. Args: message: Zu summarisierende Nachricht prompt: Anweisungen zur Erstellung der Zusammenfassung Returns: Zusammenfassung der Nachricht """ agent_type = message.get("agent_type", "Unbekannt") role = message.get("role", "Unbekannt") content = message.get("content", "") # Kurze Nachrichten direkt übernehmen if len(content) < 200: content_summary = content else: # Für längere Nachrichten AI verwenden content_summary = await self.ai_service.call_api([ {"role": "system", "content": f"Fasse den folgenden Text kurz zusammen. {prompt}"}, {"role": "user", "content": content} ]) # Dokumente zusammenfassen docs_summary = "" if "documents" in message and message["documents"]: docs_list = [] for i, doc in enumerate(message["documents"]): doc_source = doc.get("source", {}) doc_name = doc_source.get("name", f"Dokument {i+1}") docs_list.append(f"- {doc_name}") if docs_list: docs_summary = f"\nDokumente: {', '.join(docs_list)}" return f"[{role}/{agent_type}]: {content_summary}{docs_summary}" async def message_summarize_documents(self, message: Dict[str, Any], prompt: str) -> str: """ Erstellt eine Zusammenfassung der Dokumente in einer Nachricht. Args: message: Nachricht mit Dokumenten prompt: Anweisungen zur Erstellung der Zusammenfassung Returns: Zusammenfassung der Dokumente """ if "documents" not in message or not message["documents"]: return "Keine Dokumente vorhanden." summaries = [] for i, doc in enumerate(message["documents"]): doc_source = doc.get("source", {}) doc_name = doc_source.get("name", f"Dokument {i+1}") content_summary = "Keine Inhalte verfügbar" if "contents" in doc and doc["contents"]: text_contents = [] for content in doc["contents"]: if content.get("is_text", False) and "text" in content: text = content["text"] # Für kurze Texte keine Zusammenfassung notwendig if len(text) < 200: text_contents.append(text) else: # AI für Zusammenfassung verwenden summary = self.ai_service.call_api([ {"role": "system", "content": f"Fasse den folgenden Text kurz zusammen. {prompt}"}, {"role": "user", "content": text} ]) text_contents.append(summary) if text_contents: content_summary = "\n".join(text_contents) summaries.append(f"Dokument: {doc_name}\n{content_summary}") return "\n\n".join(summaries) ### Documents def process_file_ids(self, file_ids: List[int]) -> List[Dict[str, Any]]: """ Verarbeitet eine Liste von File-IDs und gibt die entsprechenden Dateiobjekte als List of Document zurück Args: file_ids: Liste von Datei-IDs Returns: Liste von Dateiobjekten """ files = [] logger.info(f"Verarbeite {len(file_ids)} Dateien") for file_id in file_ids: try: # Existiert die Datei? file = self.lucy_interface.get_file(file_id) if not file: logger.warning(f"Datei mit ID {file_id} nicht gefunden") continue # Prüfen, ob Datei zum aktuellen Mandanten gehört if file.get("mandate_id") != self.mandate_id: logger.warning(f"Datei {file_id} gehört nicht zum Mandanten {self.mandate_id}") continue document = {} files.append(document) logger.info(f"Datei {file.get('name', 'unbenannt')} (ID: {file_id}) hinzugefügt") except Exception as e: logger.error(f"Fehler bei der Verarbeitung der Datei {file_id}: {str(e)}") # Mit restlichen Dateien fortfahren statt zu scheitern continue return files def available_documents_get(self, message_user: Dict[str, Any], workflow: Dict[str, Any]) -> List[Dict[str, Any]]: """ Ermittelt alle aktuell verfügbaren Dokumente aus User-Input und bereits generierten Dokumenten. Args: message_user: Aktuelle Nachricht vom Benutzer workflow: Aktuelles Workflow-Objekt Returns: Liste mit Informationen über alle verfügbaren Dokumente """ available_docs = [] # TODO ****** user input object --> spearate routine to read content from user documents (files: to store mime-type or extension? - to analyse) // separate routine to get content from messages # Dokumente aus der aktuellen Benutzer-Nachricht if "documents" in message_user and message_user["documents"]: for doc in message_user["documents"]: source = doc.get("source", {}) doc_info = { "label": source.get("name", "unbenannt"), "doc_type": source.get("content_type", "text"), "source": "user", "message_id": message_user.get("id", "current") } available_docs.append(doc_info) # Dokumente aus vorherigen Nachrichten im Workflow if "messages" in workflow and workflow["messages"]: for message in workflow["messages"]: if "documents" in message and message["documents"]: for doc in message["documents"]: # Dokumente aus Inhalten extrahieren for content in doc.get("contents", []): if "label" in content: doc_info = { "label": content.get("label"), "doc_type": content.get("type", "text"), "source": "agent" if message.get("role") == "assistant" else "user", "message_id": message.get("id", "unknown") } available_docs.append(doc_info) logger.info(f"Available documents: {available_docs}") return available_docs def available_documents_format(self, documents: List[Dict[str, Any]]) -> str: """ Formatiert die Liste der verfügbaren Dokumente als lesbaren Text. Args: documents: Liste mit Dokumentinformationen Returns: Formatierter Text mit Dokumentinformationen """ if not documents: return "Keine Dokumente verfügbar." formatted_text = "" for i, doc in enumerate(documents, 1): source_info = f"vom Benutzer" if doc.get("source") == "user" else "von einem Agenten" formatted_text += f"{i}. '{doc.get('label')}' (Typ: {doc.get('doc_type')}, Quelle: {source_info})\n" return formatted_text def document_types_accepted(self) -> List[str]: """ Gibt eine Liste aller verfügbaren Dokumenttypen zurück. Returns: Liste der Dokumenttypen """ return ['text', 'csv', 'png', 'html'] ### Tools def log_add(self, workflow: Dict[str, Any], message: str, level: str = "info", agent_id: Optional[str] = None, agent_name: Optional[str] = None) -> str: """ Fügt einen Log-Eintrag zum Workflow hinzu und loggt diesen auch im Logger. Args: workflow: Workflow-Objekt message: Log-Nachricht level: Log-Level (info, warning, error) agent_id: Optional - ID des Agenten agent_name: Optional - Name des Agenten Returns: ID des erstellten Log-Eintrags """ # Sicherstellen, dass Logs-Liste existiert if "logs" not in workflow: workflow["logs"] = [] # Log-ID generieren log_id = f"log_{str(uuid.uuid4())}" # Log-Eintrag erstellen log_entry = { "id": log_id, "workflow_id": workflow["id"], "message": message, "type": level, "timestamp": datetime.now().isoformat(), "agent_id": agent_id, "agent_name": agent_name } # Log zum Workflow hinzufügen workflow["logs"].append(log_entry) # In Datenbank speichern self.lucy_interface.create_workflow_log(log_entry) # Auch im Logger loggen if level == "info": logger.info(f"Workflow {workflow['id']}: {message}") elif level == "warning": logger.warning(f"Workflow {workflow['id']}: {message}") elif level == "error": logger.error(f"Workflow {workflow['id']}: {message}") return log_id def parse_json2text(self, json_obj: Any) -> str: """ Konvertiert ein JSON-Objekt in eine lesbare Textdarstellung. Args: json_obj: Zu konvertierendes JSON-Objekt Returns: Formatierte Textdarstellung """ if not json_obj: return "Keine Daten vorhanden" try: # Formatieren mit Einrückung für bessere Lesbarkeit return json.dumps(json_obj, indent=2, ensure_ascii=False) except Exception as e: logger.error(f"Fehler bei JSON-Konvertierung: {str(e)}") return str(json_obj) def parse_json_response(self, response_text: str) -> Dict[str, Any]: """ Parst die JSON-Antwort aus einem Text. Args: response_text: Text mit JSON-Inhalt Returns: Geparste JSON-Daten """ try: # Extrahiere JSON aus dem Text (falls mit anderen Inhalten vermischt) json_start = response_text.find('{') json_end = response_text.rfind('}') + 1 if json_start >= 0 and json_end > json_start: json_str = response_text[json_start:json_end] return json.loads(json_str) else: # Versuche den gesamten Text zu parsen return json.loads(response_text) except json.JSONDecodeError as e: logger.error(f"JSON-Parse-Fehler: {str(e)}") # Fallback: Leere Struktur zurückgeben return { "obj_answer": [], "obj_workplan": [], "user_response": "Entschuldigung, ich konnte Ihre Anfrage nicht verarbeiten. Bitte versuchen Sie es erneut." } # Singleton-Factory für den ChatManager _chat_managers = {} def get_chat_manager(mandate_id: int = 0, user_id: int = 0) -> ChatManager: """ Gibt einen ChatManager für den angegebenen Kontext zurück. Wiederverwendet bestehende Instanzen. Args: mandate_id: ID des Mandanten user_id: ID des Benutzers Returns: ChatManager-Instanz """ context_key = f"{mandate_id}_{user_id}" if context_key not in _chat_managers: _chat_managers[context_key] = ChatManager(mandate_id, user_id) return _chat_managers[context_key]