gateway/gwserver/modules/agentservice_interface.py
2025-03-25 10:42:21 +01:00

517 lines
No EOL
23 KiB
Python

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]