import asyncio import uuid import os import logging from typing import List, Dict, Any, Optional from datetime import datetime from fastapi import HTTPException import configload as configload # Import der Teilmodule import modules.agentservice_part_filehandling as file_handling import modules.agentservice_part_agents as agents import modules.agentservice_part_results as results # Logger konfigurieren logger = logging.getLogger(__name__) # Konfigurationsdaten laden def load_config_data(): config = configload.load_config() result = { "application": { "debug": config.get('Module_AgentserviceInterface', 'DEBUG'), "upload_dir": config.get('Module_AgentserviceInterface', 'UPLOAD_DIR'), "results_dir": config.get('Module_AgentserviceInterface', 'RESULTS_DIR'), "max_history": config.get('Module_AgentserviceInterface', 'MAX_HISTORY'), "ai_provider": config.get('Module_AgentserviceInterface', 'AI_PROVIDER'), } } # Debug-Modus einstellen if result["application"]["debug"].lower() == "true": logging.basicConfig(level=logging.DEBUG) logger.setLevel(logging.DEBUG) logger.debug("Debug-Modus aktiviert") return result class AgentService: """ Service für die Verwaltung und Ausführung von Multi-Agent-Workflows mit verschiedenen Modellen. """ def __init__(self, mandate_id: int = None, user_id: int = None): """ Initialisiert den AgentService. Args: mandate_id: ID des aktuellen Mandanten (optional) user_id: ID des aktuellen Benutzers (optional) """ # Mandanten- und Benutzerkontext self.mandate_id = mandate_id self.user_id = user_id # Konfiguration laden self.config = load_config_data() # Verzeichnisse aus der Konfiguration übernehmen self.results_dir = self.config["application"]["results_dir"] self.upload_dir = self.config["application"]["upload_dir"] self.max_history = int(self.config["application"]["max_history"]) # AI Provider aus der Konfiguration übernehmen self.ai_provider = self.config["application"]["ai_provider"].lower() # Verzeichnisse erstellen os.makedirs(self.results_dir, exist_ok=True) os.makedirs(self.upload_dir, exist_ok=True) # Connector-Instanzen initialisieren if self.ai_provider == "anthropic": import connector_aichat_anthropic as service_aichat self.service_aichat = service_aichat.ChatService() logger.info("Anthropic AI Provider wird verwendet") else: import connector_aichat_openai as service_aichat self.service_aichat = service_aichat.ChatService() logger.info("OpenAI AI Provider wird verwendet") import connector_aiweb_webscraping as service_aiscrap self.service_aiscrap = service_aiscrap.WebScrapingService() logger.info(f"AgentService initialisiert mit:") logger.info(f" - AI Provider: {self.ai_provider}") logger.info(f" - Ergebnisverzeichnis: {self.results_dir}") logger.info(f" - Upload-Verzeichnis: {self.upload_dir}") # Workflow-Speicher self.workflows = {} async def execute_workflow( self, workflow_id: str, prompt: str, agents_list: List[Dict[str, Any]], files: List[Dict[str, Any]] ) -> str: """ Führt einen Workflow mit den angegebenen Agenten und Dateien aus. Anstatt die Agenten der Reihe nach abzuarbeiten, wird ein AI-Moderator verwendet, der die Agenten basierend auf ihren Fähigkeiten und bisherigen Antworten steuert. Verwendet parse_filedata aus dem jeweiligen Connector für die Verarbeitung von Dateien, wobei vorverarbeitete Dateiinhalte effizient wiederverwendet werden. """ logger.info(f"Starte Workflow {workflow_id} mit {len(agents_list)} Agenten und {len(files)} Dateien") # Mandanten- und Benutzerkontext in die Workflow-Daten aufnehmen self.workflows[workflow_id] = { "id": workflow_id, "mandate_id": self.mandate_id, "user_id": self.user_id, "status": "running", "progress": 0.0, "started_at": datetime.now().isoformat(), "completed_at": None, "agent_statuses": {}, "logs": [], "results": [] } # Log-Eintrag für den Start des Workflows self._add_log(workflow_id, "Workflow gestartet", "info") self._add_log(workflow_id, f"Verarbeite {len(files)} Dateien...", "info") # Dateikontexte vorbereiten file_contexts = file_handling.prepare_file_contexts(files, self.upload_dir) # Dateiinhalte lesen - EINMAL für den gesamten Workflow file_contents = file_handling.read_file_contents( file_contexts, self.upload_dir, workflow_id, self._add_log ) # Erstelle einen formatierten Kontext für die Protokollierung file_context_text = file_handling.format_file_context_text(file_contexts, file_contents) logger.debug(f"Dateikontexte erstellt: {len(file_contexts)} Dateien") self.workflows[workflow_id]["progress"] = 0.1 # Initialisiere den Chatverlauf für den Agenten-Dialog chat_history = [] # Erstelle das standardisierte Nachrichtenobjekt für die initialen Dateien und den Prompt # Verwende parse_filedata aus dem Connector und übergebe die vorverarbeiteten Dateiinhalte initial_message = self.service_aichat.parse_filedata( file_contexts, prompt, file_contents # Übergebe die vorverarbeiteten Inhalte ) # Initialen Prompt zum Chatverlauf hinzufügen chat_history.append(initial_message) # Initialisiere die verfügbaren Agenten mit ihren Fähigkeiten available_agents = agents.initialize_agents(agents_list) # Initialisiere den Status für jeden Agenten for agent_id in available_agents: self.workflows[workflow_id]["agent_statuses"][agent_id] = "pending" # Moderator-Prompt erstellen moderator_system_prompt = agents.get_moderator_prompt(available_agents) # Starte den Workflow mit dem Moderator self._add_log(workflow_id, f"Starte Agenten-Tischrunde mit Moderator und {len(available_agents)} Agenten", "info") # Maximale Anzahl der Runden zur Vermeidung endloser Schleifen max_rounds = 12 current_round = 0 workflow_complete = False while current_round < max_rounds and not workflow_complete: current_round += 1 self._add_log(workflow_id, f"Starte Runde {current_round}", "info") # Der Moderator wählt den nächsten Agenten aus moderator_prompt = { "role": "system", "content": moderator_system_prompt } # Kopie des Chatverlaufs für den Moderator erstellen moderator_chat = [moderator_prompt] + chat_history[-self.max_history:] # Füge eine Zusammenfassung der verfügbaren Agenten hinzu agent_info = "Verfügbare Agenten:\n" for agent_id, agent in available_agents.items(): status = "✓ Bereits verwendet" if agent["used"] else "✗ Noch nicht verwendet" agent_info += f"- {agent['name']} (Typ: {agent['type']}): {agent['capabilities']}\n Status: {status}\n" moderator_chat.append({ "role": "system", "content": agent_info + "\nWähle den nächsten Agenten aus oder beende den Workflow, wenn die Aufgabe erfüllt ist." }) # Moderator trifft die Entscheidung try: moderator_chat = self._sanitize_messages(moderator_chat) moderator_decision = await self.service_aichat.call_api(moderator_chat) moderator_text = moderator_decision["choices"][0]["message"]["content"] logger.debug(f"Full moderator decision text: {moderator_text}") # Füge die Entscheidung des Moderators zum Chatverlauf hinzu chat_history.append({ "role": "assistant", "content": f"[Moderator] {moderator_text}" }) # Log der Moderator-Entscheidung self._add_log(workflow_id, f"Moderator-Entscheidung: {moderator_text}", "info") # Finde den nächsten zu verwendenden Agenten selected_agent_id = agents.find_next_agent(moderator_text, available_agents) # Prüfe, ob der Workflow beendet werden soll if selected_agent_id == "WORKFLOW_COMPLETE": self._add_log(workflow_id, "Moderator hat den Workflow beendet", "success") workflow_complete = True break # Prüfe, ob ein Agent ausgewählt wurde if not selected_agent_id: self._add_log(workflow_id, "Kein Agent ausgewählt. Beende Workflow.", "warning") workflow_complete = True continue # Agenten aus der Liste markieren selected_agent = available_agents[selected_agent_id] selected_agent["used"] = True # Agenten-Status aktualisieren self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "running" self._add_log( workflow_id, f"Agent '{selected_agent['name']}' beginnt mit der Verarbeitung...", "start", selected_agent_id, selected_agent['name'] ) # Agent-spezifische Anweisungen erstellen agent_instructions = agents.get_agent_instructions(selected_agent["type"], selected_agent) # Agent-Prompt erstellen agent_prompt = agents.create_agent_prompt(selected_agent, agent_instructions) # Kopie des Chatverlaufs für den Agenten erstellen agent_chat = [agent_prompt] + chat_history[-self.max_history:] # Falls der Agent ein Webscraper ist und Scraping notwendig ist if selected_agent["type"] == "scraper": self._add_log(workflow_id, "Führe Web-Scraping durch...", "info", selected_agent_id, selected_agent["name"]) web_data = await self.service_aiscrap.scrape_web_data(prompt) if web_data: # Ensure web_data has no trailing whitespace web_data = web_data.strip() if isinstance(web_data, str) else web_data agent_chat.append({ "role": "system", "content": f"# Gescrapte Web-Daten\n{web_data}".strip() }) self._add_log(workflow_id, "Web-Scraping abgeschlossen", "info", selected_agent_id, selected_agent["name"]) # Agent führt seinen Teil aus try: # Initialisiere eine Schleife für mögliche Dateianfragen des Agenten agent_processing_complete = False max_file_requests = 5 # Begrenze die Anzahl der Dateianfragen pro Agent file_request_count = 0 while not agent_processing_complete and file_request_count < max_file_requests: # Rufe die API auf agent_chat = self._sanitize_messages(agent_chat) agent_response = await self.service_aichat.call_api(agent_chat) agent_text = agent_response["choices"][0]["message"]["content"] # Prüfe, ob der Agent weitere Dateien anfordert file_commands = file_handling.parse_file_access_commands(agent_text) if file_commands: file_request_count += 1 self._add_log( workflow_id, f"Agent '{selected_agent['name']}' fordert zusätzliche Dateiinhalte an (Anfrage {file_request_count})", "info", selected_agent_id, selected_agent["name"] ) # Verarbeite alle Dateianfragen file_responses = [] for cmd in file_commands: file_id = cmd.get('file_id') complete = cmd.get('complete', False) start = cmd.get('start') end = cmd.get('end') pages = cmd.get('pages') content = file_handling.load_additional_file_content( workflow_id, file_id, file_contents, file_contexts, self._add_log, read_complete=complete, start_pos=start, end_pos=end, page_numbers=pages ) if content: file_name = next((f['name'] for f in file_contexts if f['id'] == file_id), f"Datei {file_id}") file_responses.append(f"Zusätzlicher Inhalt für {file_name}:\n\n{content}") else: file_responses.append(f"Datei mit ID {file_id} konnte nicht geladen werden.") # Füge die Antworten zum Chatverlauf hinzu file_response_text = "\n\n".join(file_responses) system_response = { "role": "system", "content": (f"Hier sind die angeforderten Dateiinhalte:\n\n{file_response_text}\n\n" + f"Bitte fahre nun mit deiner Analyse fort.").strip() } # Füge Systemantwort zum Chatverlauf und zum Agentenchat hinzu chat_history.append(system_response) agent_chat.append(system_response) # Setze die Schleife fort, um dem Agenten die Möglichkeit zu geben, seine Analyse fortzusetzen continue # Keine Dateianfragen mehr - Agent ist fertig agent_processing_complete = True # Füge die endgültige Antwort des Agenten zum Chatverlauf hinzu chat_history.append({ "role": "assistant", "content": agent_text }) # Agent-Ergebnis erstellen result = results.create_agent_result( workflow_id, selected_agent, len(self.workflows[workflow_id]["results"]), prompt, file_contexts, agent_text, self.mandate_id, self.user_id ) self.workflows[workflow_id]["results"].append(result) self._add_log( workflow_id, f"Agent '{selected_agent['name']}' hat die Verarbeitung abgeschlossen", "complete", selected_agent_id, selected_agent["name"] ) # Agenten-Status aktualisieren self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "completed" except Exception as e: logger.error(f"Fehler bei der Ausführung von Agent '{selected_agent['name']}': {str(e)}") self._add_log( workflow_id, f"Fehler bei der Ausführung: {str(e)}", "error", selected_agent_id, selected_agent["name"] ) self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "failed" # Füge die Fehlermeldung zum Chatverlauf hinzu chat_history.append({ "role": "assistant", "content": f"[Fehler bei Agent '{selected_agent['name']}']: {str(e)}" }) # Fortschritt aktualisieren self.workflows[workflow_id]["progress"] = min(0.9, 0.1 + (current_round / max_rounds) * 0.8) except Exception as e: logger.error(f"Fehler in der Moderator-Phase: {str(e)}") self._add_log(workflow_id, f"Fehler in der Moderator-Phase: {str(e)}", "error") break # Workflow abschließen if workflow_complete: self.workflows[workflow_id]["status"] = "completed" self._add_log(workflow_id, "Workflow erfolgreich beendet", "success") elif current_round >= max_rounds: self.workflows[workflow_id]["status"] = "completed" self._add_log(workflow_id, f"Workflow nach {max_rounds} Runden automatisch beendet", "info") else: self.workflows[workflow_id]["status"] = "failed" self._add_log(workflow_id, "Workflow mit Fehlern beendet", "error") self.workflows[workflow_id]["progress"] = 1.0 self.workflows[workflow_id]["completed_at"] = datetime.now().isoformat() # Speichere Ergebnisse in Datei für spätere Verwendung results.save_workflow_results(self.workflows, workflow_id, self.results_dir) return workflow_id def _add_log( self, workflow_id: str, message: str, log_type: str, agent_id: Optional[str] = None, agent_name: Optional[str] = None ) -> None: """Fügt einen Protokolleintrag zum Workflow hinzu""" results.add_log( self.workflows, workflow_id, message, log_type, agent_id, agent_name, self.mandate_id, self.user_id ) def get_workflow_status(self, workflow_id: str) -> Optional[Dict[str, Any]]: """Gibt den Status eines Workflows zurück""" return results.get_workflow_status(self.workflows, workflow_id) def get_workflow_logs(self, workflow_id: str) -> Optional[List[Dict[str, Any]]]: """Gibt die Protokolle eines Workflows zurück""" return results.get_workflow_logs(self.workflows, workflow_id) def get_workflow_results(self, workflow_id: str) -> Optional[List[Dict[str, Any]]]: """Gibt die Ergebnisse eines Workflows zurück""" return results.get_workflow_results(self.workflows, workflow_id) async def close(self): """Schließt die HTTP-Clients beim Beenden der Anwendung""" await self.service_aichat.close() await self.service_aiscrap.close() def stop_workflow(self, workflow_id: str) -> bool: """ Stoppt einen laufenden Workflow. Args: workflow_id: ID des zu stoppenden Workflows Returns: bool: True, wenn der Workflow erfolgreich gestoppt wurde, False wenn nicht gefunden """ logger.info(f"Stoppe Workflow {workflow_id}") if workflow_id not in self.workflows: logger.warning(f"Workflow {workflow_id} nicht gefunden") return False # Prüfen, ob der Workflow bereits beendet ist current_status = self.workflows[workflow_id]["status"] if current_status not in ["running", "pending"]: logger.info(f"Workflow {workflow_id} ist bereits im Status {current_status}") return False # Workflow-Status auf "stopped" setzen self.workflows[workflow_id]["status"] = "stopped" self.workflows["progress"] = 1.0 self.workflows[workflow_id]["completed_at"] = datetime.now().isoformat() # Log-Eintrag für das Stoppen hinzufügen self._add_log(workflow_id, "Workflow manuell gestoppt", "warning") # Ergebnisse speichern results.save_workflow_results(self.workflows, workflow_id, self.results_dir) logger.info(f"Workflow {workflow_id} wurde gestoppt") return True def _sanitize_message_content(self, content): """Ensures content has no trailing whitespace.""" if isinstance(content, str): return content.rstrip() return content def _sanitize_messages(self, messages): """Sanitizes all messages to prevent API errors.""" if not messages: return messages sanitized = [] for message in messages: sanitized_message = message.copy() if "content" in sanitized_message: sanitized_message["content"] = self._sanitize_message_content(sanitized_message["content"]) sanitized.append(sanitized_message) return sanitized # Singleton-Factory für AgentService-Instanzen pro Kontext _agent_service_instances = {} def get_agentservice_interface(mandate_id: int = None, user_id: int = None) -> AgentService: """ Gibt eine AgentService-Instanz für den angegebenen Kontext zurück. Wiederverwendet bestehende Instanzen. """ context_key = f"{mandate_id}_{user_id}" if context_key not in _agent_service_instances: _agent_service_instances[context_key] = AgentService(mandate_id, user_id) return _agent_service_instances[context_key]