517 lines
No EOL
23 KiB
Python
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] |