MVP1 rev3

This commit is contained in:
ValueOn AG 2025-04-19 01:02:46 +02:00
parent de48c4d8cb
commit 9e2b6f1344
26 changed files with 6505 additions and 1790 deletions

4
_uploads/.gitignore vendored
View file

@ -1,4 +0,0 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View file

@ -32,8 +32,7 @@ Connector_AiWebscraping_MAX_SEARCH_RESULTS = 5
Module_AgentserviceInterface_UPLOAD_DIR = ./_uploads
# File management configuration
File_Management_MAX_UPLOAD_SIZE = 10000000
File_Management_ALLOWED_EXTENSIONS = pdf,docx,xlsx,txt,csv,json,jpg,png
File_Management_MAX_UPLOAD_SIZE_MB = 50
File_Management_CLEANUP_INTERVAL = 240
# Logging configuration

View file

@ -399,38 +399,6 @@ class SimpleCodeExecutor:
"""Clean up during garbage collection."""
self.cleanup()
# Unchanged error recommendation function
def get_error_recommendation(error_message: str) -> str:
"""Generate recommendations based on error message."""
if "ImportError" in error_message or "ModuleNotFoundError" in error_message:
return """
### Recommendation
The error indicates a missing Python module. Try using standard libraries or common data analysis modules.
"""
elif "PermissionError" in error_message:
return """
### Recommendation
The code doesn't have the necessary permissions to access files or directories.
"""
elif "SyntaxError" in error_message:
return """
### Recommendation
There's a syntax error in the code. Check for missing parentheses, quotes, colons, or indentation errors.
"""
elif "FileNotFoundError" in error_message:
return """
### Recommendation
A file could not be found. Check the file path and make sure the file exists.
"""
else:
return """
### Recommendation
To fix the error:
1. Check the exact error message
2. Simplify the code and test step by step
3. Use try/except blocks for error-prone operations
"""
class CoderAgent(BaseAgent):
"""Agent for developing and executing Python code with auto-correction capabilities"""
@ -749,7 +717,7 @@ class CoderAgent(BaseAgent):
response_content += f"### Final Error\n\n```\n{attempts_info[-1]['error']}\n```\n\n"
# Add recommendation based on error
response_content += get_error_recommendation(error)
response_content += self.get_error_recommendation(error)
# Add correction history
if len(attempts_info) > 1:
@ -788,7 +756,7 @@ class CoderAgent(BaseAgent):
response_content += f"### Error\n\n```\n{error}\n```\n\n"
# Add recommendation based on error
response_content += get_error_recommendation(error)
response_content += self.get_error_recommendation(error)
response["content"] = response_content
else:
@ -1386,7 +1354,40 @@ result = {{"error": "Code generation failed", "message": "{error_str}"}}
result_format="python_code",
context_id=context_id
)
# Unchanged error recommendation function
def get_error_recommendation(error_message: str) -> str:
"""Generate recommendations based on error message."""
if "ImportError" in error_message or "ModuleNotFoundError" in error_message:
return """
### Recommendation
The error indicates a missing Python module. Try using standard libraries or common data analysis modules.
"""
elif "PermissionError" in error_message:
return """
### Recommendation
The code doesn't have the necessary permissions to access files or directories.
"""
elif "SyntaxError" in error_message:
return """
### Recommendation
There's a syntax error in the code. Check for missing parentheses, quotes, colons, or indentation errors.
"""
elif "FileNotFoundError" in error_message:
return """
### Recommendation
A file could not be found. Check the file path and make sure the file exists.
"""
else:
return """
### Recommendation
To fix the error:
1. Check the exact error message
2. Simplify the code and test step by step
3. Use try/except blocks for error-prone operations
"""
# Singleton instance
_coder_agent = None

View file

@ -12,7 +12,7 @@ import uuid
logger = logging.getLogger(__name__)
class BaseAgent:
class AgentBase:
"""
Enhanced base agent class with improved communication capabilities.
All specialized agents should inherit from this class.
@ -20,37 +20,16 @@ class BaseAgent:
def __init__(self):
"""Initialize the enhanced agent."""
self.id = "base_agent"
self.name = "Base Agent"
self.type = "base"
self.description = "Base agent for the Agentservice"
self.name = "base"
self.capabilities = "Basic agent operations"
self.result_format = "Text"
# New properties for document handling
self.supports_documents = True
self.document_capabilities = ["read", "reference"]
self.required_context = []
# System dependencies
self.ai_service = None
self.document_handler = None
self.lucydom_interface = None
def set_dependencies(self, ai_service=None, document_handler=None, lucydom_interface=None):
"""
Set system dependencies.
Args:
ai_service: AI service for text generation
document_handler: Document handler for document operations
lucydom_interface: LucyDOM interface for database access
"""
self.ai_service = ai_service
self.document_handler = document_handler
self.lucydom_interface = lucydom_interface
def get_agent_info(self) -> Dict[str, Any]:
def get_config(self) -> Dict[str, Any]:
"""
Get detailed information about the agent.
@ -58,15 +37,9 @@ class BaseAgent:
Dictionary with agent information
"""
return {
"id": self.id,
"name": self.name,
"type": self.type,
"description": self.description,
"capabilities": self.capabilities,
"result_format": self.result_format,
"supports_documents": self.supports_documents,
"document_capabilities": self.document_capabilities,
"required_context": self.required_context
}
def get_capabilities(self) -> List[str]:

View file

@ -93,8 +93,6 @@ class AgentRegistry:
lucydom_interface: LucyDOM interface for database access
"""
self.ai_service = ai_service
self.document_handler = document_handler
self.lucydom_interface = lucydom_interface
# Update all registered agents
self.update_agent_dependencies()

File diff suppressed because it is too large Load diff

858
modules/chat.py Normal file
View file

@ -0,0 +1,858 @@
"""
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]

View file

@ -0,0 +1,786 @@
"""
Datenanalyst-Agent für die Analyse und Interpretation von Daten.
Angepasst für die neue chat.py Architektur und chat_registry.py.
"""
import logging
import json
import re
import uuid
import io
import base64
from typing import Dict, Any, List, Optional
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from modules.chat_registry import AgentBase
logger = logging.getLogger(__name__)
class AgentAnalyst(AgentBase):
"""Agent für die Analyse und Interpretation von Daten"""
def __init__(self):
"""Initialisiert den Datenanalyse-Agent"""
super().__init__()
self.name = "Data Analyst"
self.capabilities = "data_analysis,pattern_recognition,statistics,visualization,data_interpretation"
self.result_format = "AnalysisReport"
# Visualisierungseinstellungen
self.plt_style = 'seaborn-v0_8-whitegrid'
self.default_figsize = (10, 6)
self.chart_dpi = 100
plt.style.use(self.plt_style)
def get_agent_info(self) -> Dict[str, Any]:
"""Gibt Agent-Informationen für die Registry zurück"""
info = super().get_config()
info.update({
"metadata": {
"supported_formats": ["csv", "xlsx", "json", "text"],
"analysis_types": ["statistical", "trend", "comparative", "predictive", "clustering", "general"],
"visualization_types": ["bar", "line", "scatter", "histogram", "box", "heatmap", "pie"]
}
})
return info
async def process_message(self, message: Dict[str, Any], context: Dict[str, Any] = None) -> Dict[str, Any]:
"""
Verarbeitet eine Nachricht und führt Datenanalyse durch.
Args:
message: Eingabenachricht
context: Optionaler Kontext
Returns:
Antwortnachricht mit Analyseergebnissen
"""
# Workflow-ID aus Kontext oder Nachricht extrahieren
workflow_id = context.get("workflow_id") if context else message.get("workflow_id", "unknown")
# Antwortstruktur erstellen
response = {
"role": "assistant",
"content": "",
"agent_name": self.name,
"result_format": self.result_format,
"workflow_id": workflow_id,
"documents": []
}
try:
# Aufgabe aus Nachricht extrahieren
task = message.get("content", "")
# Angehängte Dokumente verarbeiten und Daten extrahieren
document_context = ""
data_frames = {}
if message.get("documents"):
logger.info("Verarbeite Dokumente für die Analyse")
document_context, data_frames = await self._process_and_extract_data(message)
# Prüfen, ob wir analysierbare Inhalte haben
have_analyzable_content = len(data_frames) > 0 or (task and len(task.strip()) > 10)
if not have_analyzable_content:
# Warnmeldung, wenn keine analysierbaren Inhalte vorhanden sind
if message.get("documents"):
analysis_content = "## Datenanalyse-Bericht\n\nIch konnte keine verarbeitbaren Daten in den bereitgestellten Dokumenten finden. Bitte stellen Sie sicher, dass Sie CSV-, Excel- oder andere Datendateien in einem Format beifügen, das ich analysieren kann."
else:
analysis_content = "## Datenanalyse-Bericht\n\nEs wurden keine Daten oder ausreichenden Textinhalte für die Analyse bereitgestellt. Bitte stellen Sie Text für die Analyse bereit oder fügen Sie Datendateien bei, die ich analysieren kann."
response["content"] = analysis_content
return response
# Analysetyp bestimmen und Analyse durchführen
analysis_type = self._determine_analysis_type(task)
logger.info(f"Führe {analysis_type}-Analyse durch")
# Prompt mit Dokumentkontext erweitern
enhanced_prompt = self._create_enhanced_prompt(message, document_context, context)
# Visualisierungsdokumente generieren, falls Daten vorhanden sind
visualization_documents = []
if data_frames:
logger.info(f"Generiere Visualisierungen für {len(data_frames)} Datensätze")
visualization_documents = self._generate_visualizations(data_frames, analysis_type, workflow_id, task)
# Visualisierungen zur Antwort hinzufügen
response["documents"].extend(visualization_documents)
# Analyse mit Datenerkenntnissen generieren, falls Datenrahmen vorhanden sind
analysis_content = ""
if data_frames:
# Datenerkenntnisse extrahieren
data_insights = self._extract_data_insights(data_frames)
# Erkenntnisse zum Prompt hinzufügen
enhanced_prompt += f"\n\n=== DATENERKENNTNISSE ===\n{data_insights}"
# Analyse mit Datenerkenntnissen generieren
analysis_content = await self._generate_analysis(enhanced_prompt, analysis_type)
# Verweise auf die Visualisierungsdokumente einfügen
if visualization_documents:
viz_references = "\n\n## Visualisierungen\n\n"
viz_references += "Die folgenden Visualisierungen wurden erstellt, um die Daten besser zu verstehen:\n\n"
for i, doc in enumerate(visualization_documents, 1):
doc_source = doc.get("source", {})
doc_name = doc_source.get("name", f"Visualisierung {i}")
viz_references += f"{i}. **{doc_name}** - Als angehängtes Dokument verfügbar\n"
analysis_content += viz_references
else:
# Analyse basierend nur auf Text, wenn keine Datenrahmen vorhanden sind
logger.info("Keine Datenrahmen verfügbar, analysiere Textinhalt")
analysis_content = await self._generate_analysis(enhanced_prompt, analysis_type)
# Inhalt in der Antwort setzen
response["content"] = analysis_content
return response
except Exception as e:
error_msg = f"Fehler bei der Datenanalyse: {str(e)}"
logger.error(error_msg)
response["content"] = f"## Fehler bei der Datenanalyse\n\n{error_msg}"
return response
def _create_enhanced_prompt(self, message: Dict[str, Any], document_context: str, context: Dict[str, Any] = None) -> str:
"""
Erstellt einen erweiterten Prompt für die Analyse, der alle verfügbaren Inhalte integriert.
Args:
message: Die ursprüngliche Nachricht
document_context: Aus Dokumenten extrahierter Kontext
context: Optionaler zusätzlicher Kontext
Returns:
Erweiterter Prompt für die Analyse
"""
# Originale Aufgabe/Prompt abrufen
task = message.get("content", "")
# Mit Aufgabe beginnen
enhanced_prompt = f"ANALYSEAUFGABE:\n{task}"
# Dokumentkontext hinzufügen, falls vorhanden
if document_context:
enhanced_prompt += f"\n\n=== DOKUMENTINHALT ===\n{document_context}"
else:
# Wenn kein Dokumentinhalt vorhanden ist, ausdrücklich darauf hinweisen, dass wir den Textinhalt direkt analysieren
enhanced_prompt += "\n\nEs wurden keine Datendateien bereitgestellt. Führe eine Analyse des Textinhalts selbst durch."
return enhanced_prompt
async def _process_and_extract_data(self, message: Dict[str, Any]) -> tuple:
"""
Verarbeitet Dokumente und extrahiert strukturierte Daten.
Args:
message: Eingabenachricht mit Dokumenten
Returns:
Tuple aus (document_context, data_frames_dict)
"""
document_context = ""
data_frames = {}
if not message.get("documents"):
return document_context, data_frames
# Dokumenttext extrahieren
document_context = self._extract_document_text(message)
# Datendateien identifizieren und verarbeiten (CSV, Excel usw.)
for document in message.get("documents", []):
source = document.get("source", {})
filename = source.get("name", "")
# Überspringen, wenn keine erkennbare Datendatei
if not self._is_data_file(filename):
continue
try:
# Dateiinhalt aus Dokumentinhalten extrahieren
file_content = None
for content in document.get("contents", []):
if content.get("type") == "text":
file_content = content.get("text", "")
break
# Nach Dateityp verarbeiten
if filename.lower().endswith('.csv') and file_content:
df = pd.read_csv(io.StringIO(file_content))
df = self._preprocess_dataframe(df)
data_frames[filename] = df
elif filename.lower().endswith(('.xlsx', '.xls')) and file_content:
# XLS-Dateien können nicht direkt aus Text verarbeitet werden
logger.warning(f"Excel-Datei {filename} kann nicht direkt aus Textinhalt verarbeitet werden")
elif filename.lower().endswith('.json') and file_content:
try:
data = json.loads(file_content)
if isinstance(data, list):
df = pd.DataFrame(data)
elif isinstance(data, dict):
if any(isinstance(v, list) for v in data.values()):
for key, value in data.items():
if isinstance(value, list) and len(value) > 0:
df = pd.DataFrame(value)
break
else:
continue
else:
df = pd.DataFrame([data])
else:
continue
df = self._preprocess_dataframe(df)
data_frames[filename] = df
except:
logger.error(f"Fehler beim Verarbeiten der JSON-Datei {filename}")
except Exception as e:
logger.error(f"Fehler beim Verarbeiten der Datei {filename}: {str(e)}")
return document_context, data_frames
def _is_data_file(self, filename: str) -> bool:
"""Prüft, ob eine Datei eine verarbeitbare Datendatei ist"""
if filename.lower().endswith(('.csv', '.xlsx', '.xls', '.json')):
return True
return False
def _preprocess_dataframe(self, df: pd.DataFrame) -> pd.DataFrame:
"""Führt grundlegende Vorverarbeitung für einen DataFrame durch"""
if df.empty:
return df
# Vollständig leere Zeilen und Spalten entfernen
df = df.dropna(how='all')
df = df.dropna(axis=1, how='all')
# Stringkonvertierung zu numerischen Werten, wo angemessen
for col in df.columns:
# Überspringen, wenn bereits numerisch
if pd.api.types.is_numeric_dtype(df[col]):
continue
# Überspringen, wenn überwiegend nicht-numerische Strings
if df[col].dtype == 'object':
# Prüfen, ob mehr als 80% der Nicht-NA-Werte numerisch sein könnten
non_na_values = df[col].dropna()
if len(non_na_values) == 0:
continue
# Versuch der Konvertierung zu numerischen Werten
numeric_count = pd.to_numeric(non_na_values, errors='coerce').notna().sum()
if numeric_count / len(non_na_values) > 0.8:
# Mehr als 80% können in numerische Werte konvertiert werden
df[col] = pd.to_numeric(df[col], errors='coerce')
return df
def _extract_document_text(self, message: Dict[str, Any]) -> str:
"""
Extrahiert Text aus Dokumenten.
Args:
message: Eingabenachricht mit Dokumenten
Returns:
Extrahierter Text
"""
text_content = ""
for document in message.get("documents", []):
source = document.get("source", {})
name = source.get("name", "unnamed")
text_content += f"\n\n--- {name} ---\n"
for content in document.get("contents", []):
if content.get("type") == "text":
text_content += content.get("text", "")
return text_content
def _determine_analysis_type(self, task: str) -> str:
"""
Bestimmt den Analysetyp basierend auf der Aufgabe.
Args:
task: Die Analyseaufgabe
Returns:
Analysetyp
"""
task_lower = task.lower()
# Prüfen auf statistische Analyse
if any(term in task_lower for term in ["statistik", "statistical", "mittelwert", "mean", "median", "varianz"]):
return "statistical"
# Prüfen auf Trend-Analyse
elif any(term in task_lower for term in ["trend", "pattern", "zeitreihe", "time series", "historisch"]):
return "trend"
# Prüfen auf vergleichende Analyse
elif any(term in task_lower for term in ["vergleich", "compare", "comparison", "versus", "vs", "unterschied"]):
return "comparative"
# Prüfen auf prädiktive Analyse
elif any(term in task_lower for term in ["vorhersage", "predict", "forecast", "zukunft", "future"]):
return "predictive"
# Prüfen auf Clustering oder Kategorisierung
elif any(term in task_lower for term in ["cluster", "segment", "kategorisieren", "classify"]):
return "clustering"
# Standard: allgemeine Analyse
else:
return "general"
def _extract_data_insights(self, data_frames: Dict[str, pd.DataFrame]) -> str:
"""
Extrahiert grundlegende Erkenntnisse aus DataFrames.
Args:
data_frames: Dictionary von DataFrames
Returns:
Extrahierte Erkenntnisse als Text
"""
insights = []
for name, df in data_frames.items():
if df.empty:
continue
insight = f"Datensatz: {name}\n"
insight += f"Form: {df.shape[0]} Zeilen, {df.shape[1]} Spalten\n"
insight += f"Spalten: {', '.join(df.columns.tolist())}\n"
# Grundlegende Statistiken für numerische Spalten
numeric_cols = df.select_dtypes(include=['number']).columns
if len(numeric_cols) > 0:
insight += "Statistiken für numerische Spalten:\n"
for col in numeric_cols[:5]: # Auf die ersten 5 Spalten begrenzen
stats = df[col].describe()
insight += f" {col}: min={stats['min']:.2f}, max={stats['max']:.2f}, mean={stats['mean']:.2f}, median={df[col].median():.2f}\n"
# Kategoriale Spaltenwerte
cat_cols = df.select_dtypes(include=['object', 'category']).columns
if len(cat_cols) > 0:
insight += "Kategoriale Spalten:\n"
for col in cat_cols[:3]: # Auf die ersten 3 Spalten begrenzen
# Top 3 Werte abrufen
top_values = df[col].value_counts().head(3)
vals_str = ", ".join([f"{val} ({count})" for val, count in top_values.items()])
insight += f" {col}: {df[col].nunique()} eindeutige Werte. Häufigste Werte: {vals_str}\n"
insights.append(insight)
return "\n\n".join(insights)
def _generate_visualizations(self, data_frames: Dict[str, pd.DataFrame], analysis_type: str,
workflow_id: str, task: str) -> List[Dict[str, Any]]:
"""
Generiert passende Visualisierungen basierend auf Daten und Analysetyp.
Args:
data_frames: Dictionary von zu visualisierenden DataFrames
analysis_type: Durchzuführender Analysetyp
workflow_id: Workflow-ID
task: Ursprüngliche Aufgabenbeschreibung
Returns:
Liste von Visualisierungsdokumentobjekten
"""
documents = []
for name, df in data_frames.items():
if df.empty or df.shape[0] < 2:
continue # Leere oder einzeilige DataFrames überspringen
# Verschiedene Visualisierungen basierend auf dem Analysetyp erzeugen
if analysis_type == "statistical":
viz_docs = self._create_statistical_visualizations(df, name)
documents.extend(viz_docs)
elif analysis_type == "trend":
viz_docs = self._create_trend_visualizations(df, name)
documents.extend(viz_docs)
elif analysis_type == "comparative":
viz_docs = self._create_comparative_visualizations(df, name)
documents.extend(viz_docs)
else: # Allgemeine Analyse
viz_docs = self._create_general_visualizations(df, name)
documents.extend(viz_docs)
return documents
def _create_statistical_visualizations(self, df: pd.DataFrame, name: str) -> List[Dict[str, Any]]:
"""Erstellt statistische Visualisierungen für einen DataFrame"""
documents = []
# 1. Verteilungs-/Histogramm-Plots für numerische Spalten
numeric_cols = df.select_dtypes(include=['number']).columns[:3] # Auf erste 3 begrenzen
if len(numeric_cols) > 0:
plt.figure(figsize=(12, 4 * len(numeric_cols)))
for i, col in enumerate(numeric_cols, 1):
plt.subplot(len(numeric_cols), 1, i)
sns.histplot(df[col].dropna(), kde=True)
plt.title(f'Verteilung von {col}')
plt.tight_layout()
# Abbildung speichern
img_data = self._get_figure_as_base64()
plt.close()
# Dokument erstellen
doc_id = f"viz_stat_dist_{uuid.uuid4()}"
doc = {
"id": doc_id,
"source": {
"type": "generated",
"id": doc_id,
"name": f"Statistische Verteilungen - {name}",
"content_type": "image/png"
},
"contents": [{
"type": "image",
"data": img_data,
"format": "base64"
}]
}
documents.append(doc)
# 2. Box-Plots für numerische Spalten
if len(numeric_cols) > 0:
plt.figure(figsize=(12, 8))
sns.boxplot(data=df[numeric_cols])
plt.title(f'Box-Plots der numerischen Variablen in {name}')
plt.xticks(rotation=45)
plt.tight_layout()
# Abbildung speichern
img_data = self._get_figure_as_base64()
plt.close()
# Dokument erstellen
doc_id = f"viz_stat_box_{uuid.uuid4()}"
doc = {
"id": doc_id,
"source": {
"type": "generated",
"id": doc_id,
"name": f"Box-Plots - {name}",
"content_type": "image/png"
},
"contents": [{
"type": "image",
"data": img_data,
"format": "base64"
}]
}
documents.append(doc)
return documents
def _create_trend_visualizations(self, df: pd.DataFrame, name: str) -> List[Dict[str, Any]]:
"""Erstellt Trend-Visualisierungen für einen DataFrame"""
documents = []
# Numerische Spalten für die Darstellung verwenden
numeric_cols = df.select_dtypes(include=['number']).columns[:2] # Auf erste 2 begrenzen
if len(numeric_cols) > 0:
plt.figure(figsize=(12, 6))
for col in numeric_cols:
plt.plot(df.index, df[col], marker='o', label=col)
plt.title(f'Trendsicht von {", ".join(numeric_cols)} - {name}')
plt.legend()
plt.tight_layout()
# Abbildung speichern
img_data = self._get_figure_as_base64()
plt.close()
# Dokument erstellen
doc_id = f"viz_trend_{uuid.uuid4()}"
doc = {
"id": doc_id,
"source": {
"type": "generated",
"id": doc_id,
"name": f"Trendanalyse - {name}",
"content_type": "image/png"
},
"contents": [{
"type": "image",
"data": img_data,
"format": "base64"
}]
}
documents.append(doc)
return documents
def _create_comparative_visualizations(self, df: pd.DataFrame, name: str) -> List[Dict[str, Any]]:
"""Erstellt vergleichende Visualisierungen für einen DataFrame"""
documents = []
# 1. Kategoriale Spalten für Gruppierung suchen
cat_cols = df.select_dtypes(include=['object', 'category']).columns
if len(cat_cols) > 0:
# Erste kategoriale Spalte mit angemessener Anzahl eindeutiger Werte verwenden
groupby_col = None
for col in cat_cols:
unique_count = df[col].nunique()
if 2 <= unique_count <= 10: # Angemessene Anzahl von Kategorien
groupby_col = col
break
if groupby_col:
# Numerische Spalten für den Vergleich über Gruppen hinweg suchen
numeric_cols = df.select_dtypes(include=['number']).columns[:3] # Auf erste 3 begrenzen
if len(numeric_cols) > 0:
# 1. Balkendiagramm, das Mittelwerte vergleicht
plt.figure(figsize=(12, 6))
mean_by_group = df.groupby(groupby_col)[numeric_cols].mean()
mean_by_group.plot(kind='bar')
plt.title(f'Vergleich der Mittelwerte nach {groupby_col} - {name}')
plt.xticks(rotation=45)
plt.tight_layout()
# Abbildung speichern
img_data = self._get_figure_as_base64()
plt.close()
# Dokument erstellen
doc_id = f"viz_comp_bar_{uuid.uuid4()}"
doc = {
"id": doc_id,
"source": {
"type": "generated",
"id": doc_id,
"name": f"Mittelwertvergleich nach {groupby_col} - {name}",
"content_type": "image/png"
},
"contents": [{
"type": "image",
"data": img_data,
"format": "base64"
}]
}
documents.append(doc)
# 2. Streudiagramm für den Vergleich zweier numerischer Variablen
numeric_cols = df.select_dtypes(include=['number']).columns
if len(numeric_cols) >= 2:
plt.figure(figsize=(10, 8))
# Erste beiden numerischen Spalten als Feature und Ziel verwenden
x_col, y_col = numeric_cols[0], numeric_cols[1]
plt.scatter(df[x_col], df[y_col])
plt.title(f'Vergleich von {x_col} vs {y_col} - {name}')
plt.xlabel(x_col)
plt.ylabel(y_col)
plt.tight_layout()
# Abbildung speichern
img_data = self._get_figure_as_base64()
plt.close()
# Dokument erstellen
doc_id = f"viz_comp_scatter_{uuid.uuid4()}"
doc = {
"id": doc_id,
"source": {
"type": "generated",
"id": doc_id,
"name": f"Variablenvergleich - {name}",
"content_type": "image/png"
},
"contents": [{
"type": "image",
"data": img_data,
"format": "base64"
}]
}
documents.append(doc)
return documents
def _create_general_visualizations(self, df: pd.DataFrame, name: str) -> List[Dict[str, Any]]:
"""Erstellt allgemeine Visualisierungen für einen DataFrame"""
documents = []
# 1. Datenübersicht: Numerische Zusammenfassung
numeric_cols = df.select_dtypes(include=['number']).columns
if len(numeric_cols) > 0:
# Balkendiagramm der Mittelwerte für numerische Spalten erstellen
plt.figure(figsize=(12, 6))
means = df[numeric_cols].mean().sort_values()
means.plot(kind='bar')
plt.title(f'Mittelwerte der numerischen Variablen - {name}')
plt.xticks(rotation=45)
plt.tight_layout()
# Abbildung speichern
img_data = self._get_figure_as_base64()
plt.close()
# Dokument erstellen
doc_id = f"viz_gen_means_{uuid.uuid4()}"
doc = {
"id": doc_id,
"source": {
"type": "generated",
"id": doc_id,
"name": f"Zusammenfassung numerischer Variablen - {name}",
"content_type": "image/png"
},
"contents": [{
"type": "image",
"data": img_data,
"format": "base64"
}]
}
documents.append(doc)
# 2. Übersicht über kategoriale Daten
cat_cols = df.select_dtypes(include=['object', 'category']).columns
if len(cat_cols) > 0:
# Erste kategoriale Spalte mit angemessener Kardinalität auswählen
for col in cat_cols:
if df[col].nunique() <= 10: # Angemessene Anzahl von Kategorien
plt.figure(figsize=(10, 6))
value_counts = df[col].value_counts().sort_values(ascending=False)
value_counts.plot(kind='bar')
plt.title(f'Verteilung von {col} - {name}')
plt.xticks(rotation=45)
plt.tight_layout()
# Abbildung speichern
img_data = self._get_figure_as_base64()
plt.close()
# Dokument erstellen
doc_id = f"viz_gen_cat_{uuid.uuid4()}"
doc = {
"id": doc_id,
"source": {
"type": "generated",
"id": doc_id,
"name": f"Kategoriale Verteilung - {name}",
"content_type": "image/png"
},
"contents": [{
"type": "image",
"data": img_data,
"format": "base64"
}]
}
documents.append(doc)
break # Nur die erste geeignete Spalte verwenden
return documents
def _get_figure_as_base64(self) -> str:
"""Konvertiert aktuelle matplotlib-Abbildung in base64-String"""
buffer = io.BytesIO()
plt.savefig(buffer, format='png', dpi=self.chart_dpi)
buffer.seek(0)
image_png = buffer.getvalue()
buffer.close()
# Zu base64 konvertieren
image_base64 = base64.b64encode(image_png).decode('utf-8')
return image_base64
async def _generate_analysis(self, prompt: str, analysis_type: str) -> str:
"""
Generiert eine Analyse basierend auf Prompt und Analysetyp.
Args:
prompt: Der Analyseprompt
analysis_type: Analysetyp
Returns:
Generierte Analyse
"""
if not self.ai_service:
logging.warning("KI-Service nicht verfügbar für Analysegenerierung")
return f"## Datenanalyse ({analysis_type})\n\nAnalyse konnte nicht generiert werden: KI-Service nicht verfügbar."
# Spezialisierten Prompt basierend auf Analysetyp erstellen
system_prompt = f"""
Du bist ein spezialisierter Datenanalyst, der auf {analysis_type}-Analysen fokussiert ist.
Erstelle eine detaillierte Analyse der bereitgestellten Daten und/oder Textinhalte.
Deine Analyse sollte folgendes enthalten:
1. Eine Zusammenfassung der Daten/Inhalte
2. Wichtige Erkenntnisse und Einsichten
3. Stützende Belege und Berechnungen
4. Klare Schlussfolgerungen
5. Empfehlungen, wo angemessen
Formatiere die Analyse in Markdown mit geeigneten Überschriften, Listen und Tabellen.
"""
# Bestimmen, ob dies eine datenbasierte oder textbasierte Analyse ist
is_data_analysis = "DATENERKENNTNISSE" in prompt
# Prompt mit analysespezifischen Anweisungen erweitern
if is_data_analysis:
enhanced_prompt = f"""
Generiere eine detaillierte {analysis_type}-Analyse basierend auf den folgenden Daten:
{prompt}
"""
else:
# Anweisungen für textbasierte Analyse
enhanced_prompt = f"""
Generiere eine detaillierte {analysis_type}-Analyse des folgenden Textinhalts:
{prompt}
"""
try:
content = await self.ai_service.call_api([
{"role": "system", "content": system_prompt},
{"role": "user", "content": enhanced_prompt}
])
# Sicherstellen, dass es einen Titel am Anfang gibt
if not content.strip().startswith("# "):
content = f"# {analysis_type.capitalize()}-Analyse\n\n{content}"
return content
except Exception as e:
return f"# {analysis_type.capitalize()}-Analyse\n\nFehler bei der Analysegenerierung: {str(e)}"
# Singleton-Instanz
_analyst_agent = None
def get_analyst_agent():
"""Gibt eine Singleton-Instanz des Analyst-Agenten zurück"""
global _analyst_agent
if _analyst_agent is None:
_analyst_agent = AgentAnalyst()
return _analyst_agent

804
modules/chat_agent_coder.py Normal file
View file

@ -0,0 +1,804 @@
"""
Coder-Agent für die Entwicklung und Ausführung von Python-Code.
Angepasst für die neue chat.py Architektur und chat_registry.py.
"""
import logging
import json
import re
import uuid
import os
import subprocess
import tempfile
import shutil
import sys
from typing import Dict, Any, List, Optional, Tuple
from modules.chat_registry import AgentBase
logger = logging.getLogger(__name__)
class AgentCoder(AgentBase):
"""Agent für die Entwicklung und Ausführung von Python-Code"""
def __init__(self):
"""Initialisiert den Coder-Agent"""
super().__init__()
self.name = "Python Code Agent"
self.capabilities = "code_development,data_processing,file_processing,automation"
self.result_format = "python_code"
# Executor-Einstellungen
self.executor_timeout = 60 # Sekunden
self.executor_memory_limit = 512 # MB
# KI-Service-Einstellungen
self.ai_temperature = 0.1 # Niedrigere Temperatur für deterministische Codegenerierung
# Auto-Korrektur-Einstellungen
self.max_correction_attempts = 3 # Maximale Anzahl von Korrekturversuchen
def get_agent_info(self) -> Dict[str, Any]:
"""Gibt Agent-Informationen für die Registry zurück"""
info = super().get_config()
info.update({
"metadata": {
"timeout": self.executor_timeout,
"memory_limit": self.executor_memory_limit,
"max_correction_attempts": self.max_correction_attempts
}
})
return info
async def process_message(self, message: Dict[str, Any], context: Dict[str, Any] = None) -> Dict[str, Any]:
"""
Verarbeitet eine Nachricht zur Entwicklung und Ausführung von Python-Code.
Args:
message: Die zu verarbeitende Nachricht
context: Zusätzliche Kontextinformationen
Returns:
Antwortnachricht
"""
# Workflow-ID aus Kontext oder Nachricht extrahieren
workflow_id = context.get("workflow_id") if context else message.get("workflow_id", "unknown")
# Antwortstruktur erstellen
response = {
"role": "assistant",
"content": "",
"agent_name": self.name,
"result_format": self.result_format,
"workflow_id": workflow_id,
"documents": []
}
try:
# Inhalt und Dokumente extrahieren
content = message.get("content", "")
documents = message.get("documents", [])
# Code basierend auf dem Nachrichteninhalt mit KI generieren
logger.info("Generiere neuen Code mit KI")
# Code mit KI generieren
code_to_execute, requirements = await self._generate_code_from_prompt(content, documents)
if not code_to_execute:
logger.warning("KI konnte keinen Code generieren")
response["content"] = "Ich konnte basierend auf Ihrer Anfrage keinen ausführbaren Code generieren. Bitte geben Sie detailliertere Anweisungen."
return response
logger.info(f"Code mit KI generiert ({len(code_to_execute)} Zeichen)")
# Code-Datei-Dokument erstellen
code_doc_id = f"code_{uuid.uuid4()}"
code_filename = "generated_code.py"
code_document = {
"id": code_doc_id,
"source": {
"type": "generated",
"id": code_doc_id,
"name": code_filename,
"content_type": "text/x-python"
},
"contents": [{
"type": "text",
"text": code_to_execute,
"is_extracted": True
}]
}
# Code-Dokument zur Antwort hinzufügen
response["documents"].append(code_document)
logger.info(f"Code-Datei '{code_filename}' zur Antwort hinzugefügt")
# Code mit Auto-Korrektur-Schleife ausführen
if code_to_execute:
# Ausführungskontext vorbereiten
execution_context = {
"workflow_id": workflow_id,
"documents": documents,
"message": message
}
# Verbesserte Ausführung mit Auto-Korrektur
result, attempts_info = await self._execute_with_auto_correction(
code_to_execute,
requirements,
execution_context,
content # Originaler Prompt/Nachricht
)
# Antwort basierend auf dem endgültigen Ergebnis vorbereiten (Erfolg oder Fehler)
if result.get("success", False):
# Code-Ausführung erfolgreich
output = result.get("output", "")
execution_result = result.get("result")
logger.info("Code erfolgreich ausgeführt")
# Antwortinhalt formatieren
response_content = f"## Code erfolgreich ausgeführt"
# Informationen zu Korrekturversuchen hinzufügen, falls Korrekturen vorgenommen wurden
if attempts_info and len(attempts_info) > 1:
response_content += f" (nach {len(attempts_info)-1} Korrekturversuchen)"
response_content += "\n\n"
# Den ausgeführten Code einbeziehen
response_content += f"### Ausgeführter Code\n\n```python\n{attempts_info[-1]['code']}\n```\n\n"
# Die Ausgabe einbeziehen, falls verfügbar
if output:
response_content += f"### Ausgabe\n\n```\n{output}\n```\n\n"
# Das Ausführungsergebnis einbeziehen, falls verfügbar
if execution_result:
result_str = json.dumps(execution_result, indent=2) if isinstance(execution_result, (dict, list)) else str(execution_result)
response_content += f"### Ergebnis\n\n```\n{result_str}\n```\n\n"
response["content"] = response_content
else:
# Code-Ausführung nach allen Versuchen fehlgeschlagen
error = result.get("error", "Unbekannter Fehler")
logger.error(f"Fehler bei der Code-Ausführung nach allen Korrekturversuchen: {error}")
# Fehlerantwort formatieren
response_content = f"## Fehler bei der Code-Ausführung\n\n"
# Informationen zu Korrekturversuchen hinzufügen
if attempts_info:
response_content += f"Ich habe {len(attempts_info)} Versuche unternommen, den Code zu korrigieren, konnte aber nicht alle Probleme lösen.\n\n"
# Den letzten Versuch hinzufügen
response_content += f"### Letzter Code-Versuch\n\n```python\n{attempts_info[-1]['code']}\n```\n\n"
response_content += f"### Letzter Fehler\n\n```\n{attempts_info[-1]['error']}\n```\n\n"
# Empfehlung basierend auf dem Fehler hinzufügen
response_content += "### Empfehlung\n\n"
response_content += self._get_error_recommendation(error)
else:
# Nur den Code und den Fehler anzeigen
response_content += f"### Ausgeführter Code\n\n```python\n{code_to_execute}\n```\n\n"
response_content += f"### Fehler\n\n```\n{error}\n```\n\n"
response["content"] = response_content
else:
# Kein auszuführender Code
response["content"] = "Ich konnte keinen ausführbaren Code finden oder generieren. Bitte geben Sie Python-Code an oder erklären Sie Ihre Anforderungen klarer."
return response
except Exception as e:
error_msg = f"Fehler bei der Verarbeitung durch den Coder-Agent: {str(e)}"
logger.error(error_msg)
response["content"] = f"## Verarbeitungsfehler\n\n```\n{error_msg}\n```"
return response
async def _execute_with_auto_correction(
self,
initial_code: str,
requirements: List[str],
context: Dict[str, Any],
original_prompt: str
) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
"""
Führt Code mit automatischer Fehlerkorrektur und Wiederholungsversuchen aus.
Args:
initial_code: Der anfängliche Python-Code
requirements: Liste erforderlicher Pakete
context: Zusätzlicher Kontext für die Ausführung
original_prompt: Die ursprüngliche Benutzeranfrage/Prompt
Returns:
Tuple aus (endgültiges Ausführungsergebnis, Liste von Versuchsinfo-Dictionarys)
"""
# Verfolgungs-Daten initialisieren
current_code = initial_code
current_requirements = requirements.copy() if requirements else []
attempts_info = []
# Mit Korrekturschleife ausführen
for attempt in range(1, self.max_correction_attempts + 1):
if attempt == 1:
logger.info(f"Führe Code aus (Versuch {attempt}/{self.max_correction_attempts})")
else:
logger.info(f"Führe korrigierten Code aus (Versuch {attempt}/{self.max_correction_attempts})")
# Aktuelle Code-Version ausführen
result = await self._execute_code(current_code, current_requirements, context)
# Versuchsinformationen aufzeichnen
attempts_info.append({
"attempt": attempt,
"code": current_code,
"error": result.get("error", ""),
"success": result.get("success", False)
})
# Prüfen, ob die Ausführung erfolgreich war
if result.get("success", False):
# Erfolg! Ergebnis und Versuchsinfo zurückgeben
return result, attempts_info
# Fehlgeschlagene Ausführung - prüfen, ob die maximale Versuchsgrenze erreicht wurde
if attempt >= self.max_correction_attempts:
logger.warning(f"Maximale Korrekturversuche ({self.max_correction_attempts}) erreicht, Aufgabe")
break
# Code basierend auf dem Fehler korrigieren
error_message = result.get("error", "Unbekannter Fehler")
logger.info(f"Versuche, Code-Fehler zu beheben: {error_message[:200]}...")
# Korrigierten Code generieren
corrected_code, new_requirements = await self._generate_code_correction(
current_code,
error_message,
original_prompt,
current_requirements
)
# Für den nächsten Versuch aktualisieren
if corrected_code:
current_code = corrected_code
# Neue Anforderungen hinzufügen
if new_requirements:
for req in new_requirements:
if req not in current_requirements:
current_requirements.append(req)
logger.info(f"Neue Anforderung hinzugefügt: {req}")
else:
# Korrektur konnte nicht generiert werden, Schleife beenden
logger.warning("Konnte keine Code-Korrektur generieren, Aufgabe")
break
# Wenn wir hierher gelangen, sind alle Versuche fehlgeschlagen - das letzte Ergebnis und die Versuchsinfo zurückgeben
return result, attempts_info
async def _generate_code_correction(
self,
code: str,
error_message: str,
original_prompt: str,
current_requirements: List[str] = None
) -> Tuple[str, List[str]]:
"""
Generiert eine korrigierte Version des Codes basierend auf Fehlermeldungen.
Args:
code: Der Code, der Fehler erzeugt hat
error_message: Die zu behebende Fehlermeldung
original_prompt: Die ursprüngliche Aufgabe/Anforderungen
current_requirements: Liste der aktuell erforderlichen Pakete
Returns:
Tuple aus (korrigierter Code, neue Anforderungsliste)
"""
try:
# Detaillierten Prompt für Code-Korrektur erstellen
correction_prompt = f"""Du musst einen Fehler in Python-Code beheben. Der Code wurde für diese Aufgabe geschrieben:
ORIGINALE AUFGABE:
{original_prompt}
AKTUELLER CODE:
```python
{code}
```
FEHLERMELDUNG:
```
{error_message}
```
AKTUELLE ANFORDERUNGEN: {', '.join(current_requirements) if current_requirements else "Keine"}
Deine Aufgabe ist es, den Fehler zu analysieren und eine korrigierte Version des Codes bereitzustellen.
Konzentriere dich speziell auf die Behebung des Fehlers unter Beibehaltung der ursprünglichen Funktionalität.
Häufige Korrekturen sind:
- Behebung von Syntaxfehlern (fehlende Klammern, Einrückung usw.)
- Lösung von Import-Fehlern durch Hinzufügen geeigneter Anforderungen
- Korrektur von Dateipfaden oder Behandlung von "Datei nicht gefunden"-Fehlern
- Hinzufügen von Fehlerbehandlung für bestimmte Randfälle
- Behebung logischer Fehler im Code
FORMATIERUNGSHINWEISE:
1. Gib NUR den vollständigen korrigierten Python-Code an OHNE Erklärungen
2. Verwende KEINE Codeblock-Markierungen wie ```python oder ```
3. Erkläre NICHT, was der Code davor oder danach tut
4. Füge KEINEN Text hinzu, der kein gültiger Python-Code ist
5. Beginne deine Antwort direkt mit dem gültigen Python-Code
6. Beende deine Antwort mit gültigem Python-Code
Wenn du neue erforderliche Pakete hinzufügen musst, platziere sie in einem speziell formatierten Kommentar am Anfang deines Codes wie folgt:
# REQUIREMENTS: paket1,paket2,paket3
Deine gesamte Antwort muss gültiges Python sein, das ohne Änderungen ausgeführt werden kann.
"""
# Nachrichten für die API erstellen
messages = [
{"role": "system", "content": "Du bist ein Python-Debugging-Experte. Du gibst NUR sauberen, fehlerfreien Python-Code zurück, ohne Erklärungen, Markdown-Formatierung oder Text, der kein Code ist. Deine Antwort sollte nur gültiger, korrigierter Python-Code sein, der direkt ausgeführt werden kann."},
{"role": "user", "content": correction_prompt}
]
# API mit sehr niedriger Temperatur für deterministische Korrekturen aufrufen
generated_content = await self.ai_service.call_api(
messages,
temperature=0.1
)
# Den generierten Inhalt bereinigen, um sicherzustellen, dass es sich nur um gültigen Python-Code handelt
fixed_code = self._clean_code(generated_content)
# Anforderungen aus speziellem Kommentar am Anfang des Codes extrahieren
new_requirements = []
for line in fixed_code.split('\n'):
if line.strip().startswith("# REQUIREMENTS:"):
req_str = line.replace("# REQUIREMENTS:", "").strip()
new_requirements = [r.strip() for r in req_str.split(',') if r.strip()]
break
return fixed_code, new_requirements
except Exception as e:
logging.error(f"Fehler bei der Generierung der Code-Korrektur: {str(e)}")
# None zurückgeben, um Fehler anzuzeigen
return None, []
def _clean_code(self, code: str) -> str:
"""
Bereinigt Code durch Entfernen von Markdown-Codeblock-Markierungen und anderen Formatierungsartefakten.
Args:
code: Der zu bereinigende Code-String
Returns:
Bereinigter Code-String
"""
# Codeblock-Markierungen am Anfang/Ende entfernen
code = re.sub(r'^```(?:python)?\s*', '', code)
code = re.sub(r'```\s*$', '', code)
# Zeilen in umgekehrter Reihenfolge durchgehen, um dem Ende zu beginnen
lines = code.split('\n')
clean_lines = []
in_trailing_markdown = False
for line in reversed(lines):
stripped = line.strip()
# Prüfen, ob diese Zeile nur Backticks enthält (``` oder ` oder ``)
if re.match(r'^`{1,3}$', stripped):
in_trailing_markdown = True
continue
# Wenn wir tatsächlichen Code erreicht haben, keine nachfolgende Markdown-Berücksichtigung mehr
if stripped and not in_trailing_markdown:
in_trailing_markdown = False
# Diese Zeile hinzufügen, wenn sie nicht Teil von nachfolgendem Markdown ist
if not in_trailing_markdown:
clean_lines.insert(0, line)
# Zeilen wieder zusammenfügen
clean_code = '\n'.join(clean_lines)
# Endgültige Bereinigung für alle restlichen Backticks
clean_code = re.sub(r'`{1,3}\s*, ', clean_code)
return clean_code.strip()
async def _generate_code_from_prompt(self, prompt: str, documents: List[Dict[str, Any]]) -> Tuple[str, List[str]]:
"""
Generiert Python-Code aus einem Prompt mithilfe des KI-Dienstes.
Args:
prompt: Der Prompt, aus dem Code generiert wird
documents: Mit dem Prompt verbundene Dokumente
Returns:
Tuple aus (generierter Python-Code, erforderliche Pakete)
"""
try:
# Prompt für die Codegenerierung vorbereiten
ai_prompt = f"""Generiere Python-Code, um die folgende Aufgabe zu lösen:
{prompt}
Verfügbare Eingabedateien:
"""
# Informationen über verfügbare Dokumente hinzufügen
if documents:
for i, doc in enumerate(documents):
source = doc.get("source", {})
doc_name = source.get("name", f"Dokument {i+1}")
doc_type = source.get("content_type", "unbekannt")
doc_id = source.get("id", "")
ai_prompt += f"- {doc_name} (Typ: {doc_type}, ID: {doc_id})\n"
else:
ai_prompt += "Keine Eingabedateien verfügbar.\n"
ai_prompt += """
WICHTIGE ANFORDERUNGEN:
1. Dein Code MUSS eine 'result'-Variable definieren, um das endgültige Ergebnis zu speichern.
2. Am Ende deines Skripts sollte die result-Variable ausgegeben werden.
3. Mache deine 'result'-Variable zu einem Dictionary oder einer anderen JSON-serialisierbaren Datenstruktur, die alle relevanten Ausgaben enthält.
4. Kommentiere den Code gut, um wichtige Operationen zu erklären.
5. Mache deinen Code vollständig und in sich geschlossen.
6. Füge eine angemessene Fehlerbehandlung hinzu.
FORMATIERUNGSANWEISUNGEN:
- Gib NUR den Python-Code zurück, OHNE Einleitung, Erklärung oder Abschlusstext
- Verwende KEINE Codeblock-Markierungen wie ```python oder ```
- Erkläre NICHT, was der Code davor oder danach tut
- Füge KEINEN Text hinzu, der kein gültiger Python-Code ist
- Beginne deine Antwort direkt mit gültigem Python-Code
- Beende deine Antwort mit gültigem Python-Code
Für erforderliche Pakete platziere sie in einem speziell formatierten Kommentar am Anfang deines Codes in einer Zeile wie folgt:
# REQUIREMENTS: pandas,numpy,matplotlib,requests
Deine gesamte Antwort muss gültiges Python sein, das ohne Änderungen ausgeführt werden kann.
"""
# Nachrichten für die API erstellen
messages = [
{"role": "system", "content": "Du bist ein Python-Codegenerator, der NUR sauberen, ausführbaren Python-Code ohne Erklärungen, Markdown-Formatierung oder Nicht-Code-Text liefert. Deine Antwort sollte ausschließlich aus gültigem Python-Code bestehen, der direkt ausgeführt werden kann."},
{"role": "user", "content": ai_prompt}
]
# API aufrufen
logging.info(f"KI-API aufrufen, um Code zu generieren")
generated_content = await self.ai_service.call_api(messages, temperature=self.ai_temperature)
# Den generierten Inhalt bereinigen, um sicherzustellen, dass es sich nur um gültigen Python-Code handelt
code = self._clean_code(generated_content)
# Anforderungen aus speziellem Kommentar am Anfang des Codes extrahieren
requirements = []
for line in code.split('\n'):
if line.strip().startswith("# REQUIREMENTS:"):
req_str = line.replace("# REQUIREMENTS:", "").strip()
requirements = [r.strip() for r in req_str.split(',') if r.strip()]
break
return code, requirements
except Exception as e:
logging.error(f"Fehler bei der Generierung von Code mit KI: {str(e)}")
# Grundlegenden Fehlerbehandlungscode und keine Anforderungen zurückgeben
error_str = str(e).replace('"', '\\"')
return f"""
# Fehler bei der Codegenerierung
print(f"Bei der Codegenerierung ist ein Fehler aufgetreten: {error_str}")
# Fehlerergebnis zurückgeben
result = {{"error": "Codegenerierung fehlgeschlagen", "message": "{error_str}"}}
""", []
async def _execute_code(self, code: str, requirements: List[str] = None, context: Dict[str, Any] = None) -> Dict[str, Any]:
"""
Führt Python-Code mit dem SimpleCodeExecutor aus.
Args:
code: Der auszuführende Python-Code
requirements: Liste erforderlicher Pakete
context: Zusätzlicher Kontext für die Ausführung
Returns:
Ergebnis der Codeausführung
"""
# Workflow-ID abrufen und Logging einrichten
workflow_id = context.get("workflow_id", "") if context else ""
try:
# Liste blockierter Pakete für die Sicherheit
blocked_packages = [
"cryptography", "flask", "django", "tornado", # Sicherheitsrisiken
"tensorflow", "pytorch", "scikit-learn" # Ressourcenintensive Pakete
]
# SimpleCodeExecutor mit Anforderungen und workflow_id für Persistenz initialisieren
executor = SimpleCodeExecutor(
workflow_id=workflow_id,
timeout=self.executor_timeout,
max_memory_mb=self.executor_memory_limit,
requirements=requirements,
blocked_packages=blocked_packages,
ai_service=self.ai_service
)
# Eingabedaten für den Code vorbereiten
input_data = {"context": context, "workflow_id": workflow_id}
# Code ausführen
result = executor.execute_code(code, input_data)
# Nicht-persistente Umgebungen bereinigen
if not executor.is_persistent:
executor.cleanup()
return result
except Exception as e:
error_message = f"Fehler bei der Codeausführung: {str(e)}"
logger.error(error_message)
return {
"success": False,
"output": "",
"error": error_message,
"result": None
}
def _get_error_recommendation(self, error_message: str) -> str:
"""Generiert Empfehlungen basierend auf der Fehlermeldung."""
if "ImportError" in error_message or "ModuleNotFoundError" in error_message:
return """
Versuchen Sie, Standardbibliotheken oder häufig verwendete Datenanalysemodule zu verwenden.
"""
elif "PermissionError" in error_message:
return """
Der Code hat nicht die notwendigen Berechtigungen, um auf Dateien oder Verzeichnisse zuzugreifen.
"""
elif "SyntaxError" in error_message:
return """
Es gibt einen Syntaxfehler im Code. Überprüfen Sie auf fehlende Klammern, Anführungszeichen, Doppelpunkte oder Einrückungsfehler.
"""
elif "FileNotFoundError" in error_message:
return """
Eine Datei konnte nicht gefunden werden. Überprüfen Sie den Dateipfad und stellen Sie sicher, dass die Datei existiert.
"""
else:
return """
Um den Fehler zu beheben:
1. Überprüfen Sie die genaue Fehlermeldung
2. Vereinfachen Sie den Code und testen Sie schrittweise
3. Verwenden Sie try/except-Blöcke für fehleranfällige Operationen
"""
class SimpleCodeExecutor:
"""
Ein vereinfachter Executor, der Python-Code in isolierten virtuellen Umgebungen ausführt.
"""
# Klassenvariable zum Speichern von Workflow-Umgebungen für die Persistenz
_workflow_environments = {}
def __init__(self,
workflow_id: str = None,
timeout: int = 30,
max_memory_mb: int = 512,
requirements: List[str] = None,
blocked_packages: List[str] = None,
ai_service = None):
"""
Initialisiert den SimpleCodeExecutor.
Args:
workflow_id: Optionale Workflow-ID für persistente Umgebungen
timeout: Maximale Ausführungszeit in Sekunden
max_memory_mb: Maximaler Speicher in MB
requirements: Liste der zu installierenden Pakete
blocked_packages: Liste blockierter Pakete
"""
self.workflow_id = workflow_id
self.timeout = timeout
self.max_memory_mb = max_memory_mb
self.temp_dir = None
self.requirements = requirements or []
self.blocked_packages = blocked_packages or [
"cryptography", "flask", "django", "tornado", # Sicherheitsrisiken
"tensorflow", "pytorch", "scikit-learn" # Ressourcenintensive Pakete
]
self.is_persistent = workflow_id is not None
self.ai_service = ai_service
def _create_venv(self) -> str:
"""Erstellt eine virtuelle Umgebung und gibt den Pfad zurück."""
# Prüfen auf bestehende Umgebung bei Verwendung von workflow_id
if self.workflow_id:
self.is_persistent = True
existing_env = self._workflow_environments.get(self.workflow_id)
if existing_env and os.path.exists(existing_env):
logger.info(f"Wiederverwendung bestehender virtueller Umgebung: {existing_env}")
self.temp_dir = os.path.dirname(existing_env)
return existing_env
# Neue Umgebung erstellen
venv_parent_dir = tempfile.mkdtemp(prefix="code_exec_")
self.temp_dir = venv_parent_dir
venv_path = os.path.join(venv_parent_dir, "venv")
try:
# Virtuelle Umgebung erstellen
subprocess.run([sys.executable, "-m", "venv", venv_path],
check=True,
capture_output=True)
# Umgebungspfad speichern, wenn für einen bestimmten Workflow
if self.workflow_id:
self._workflow_environments[self.workflow_id] = venv_path
return venv_path
except subprocess.CalledProcessError as e:
logger.error(f"Fehler beim Erstellen der virtuellen Umgebung: {e}")
raise RuntimeError(f"Virtuelle Umgebung konnte nicht erstellt werden: {e}")
def _get_python_executable(self, venv_path: str) -> str:
"""Gibt den Pfad zum Python-Executable in der virtuellen Umgebung zurück."""
if os.name == 'nt': # Windows
return os.path.join(venv_path, "Scripts", "python.exe")
else: # Unix/Linux
return os.path.join(venv_path, "bin", "python")
def _extract_required_packages(self, code: str) -> List[str]:
"""Extrahiert erforderliche Pakete aus REQUIREMENTS-Kommentaren in der ersten Codezeile"""
packages = set()
# Nach speziellem REQUIREMENTS-Kommentar suchen
first_lines = code.split('\n')[:5] # Nur die ersten Zeilen prüfen
for line in first_lines:
if line.strip().startswith("# REQUIREMENTS:"):
req_str = line.replace("# REQUIREMENTS:", "").strip()
for pkg in req_str.split(','):
if pkg.strip():
packages.add(pkg.strip())
return list(packages)
def execute_code(self, code: str, input_data: Dict[str, Any] = None) -> Dict[str, Any]:
"""
Führt Python-Code in einer isolierten Umgebung aus.
Args:
code: Auszuführender Python-Code
input_data: Optionale Eingabedaten für den Code
Returns:
Dictionary mit Ausführungsergebnissen
"""
logger.info(f"Führe Code mit workflow_id aus: {self.workflow_id}")
# Virtuelle Umgebung erstellen oder wiederverwenden
venv_path = self._create_venv()
# Datei für den Code erstellen
code_id = uuid.uuid4().hex[:8]
code_file = os.path.join(self.temp_dir, f"code_{code_id}.py")
# Code ohne zusätzlichen Loader-Code schreiben
with open(code_file, "w", encoding="utf-8") as f:
f.write(code)
# Python-Executable holen
python_executable = self._get_python_executable(venv_path)
logger.info(f"Verwende Python-Executable: {python_executable}")
# Code ausführen
try:
# Code aus Root-Verzeichnis ausführen
working_dir = os.path.dirname(code_file)
process = subprocess.run(
[python_executable, code_file],
timeout=self.timeout,
capture_output=True,
text=True,
cwd=working_dir
)
# Ausgabe verarbeiten
stdout = process.stdout
stderr = process.stderr
# Ergebnis aus stdout holen, falls verfügbar
result_data = None
if process.returncode == 0 and stdout:
try:
# Nach der letzten Zeile suchen, die JSON sein könnte
for line in reversed(stdout.strip().split('\n')):
line = line.strip()
if line and line[0] in '{[' and line[-1] in '}]':
try:
result_data = json.loads(line)
# Erfolgreich geparste JSON-Ergebnis verwenden
break
except json.JSONDecodeError:
# Kein gültiges JSON, mit nächster Zeile fortfahren
continue
except Exception as e:
logger.warning(f"Fehler beim Parsen des Ergebnisses aus stdout: {str(e)}")
# Ergebnisdictionary erstellen
execution_result = {
"success": process.returncode == 0,
"output": stdout,
"error": stderr if process.returncode != 0 else "",
"result": result_data,
"exit_code": process.returncode
}
except subprocess.TimeoutExpired:
logger.error(f"Ausführung nach {self.timeout} Sekunden abgelaufen")
execution_result = {
"success": False,
"output": "",
"error": f"Ausführung abgelaufen (Timeout nach {self.timeout} Sekunden)",
"result": None,
"exit_code": -1
}
except Exception as e:
logger.error(f"Ausführungsfehler: {str(e)}")
execution_result = {
"success": False,
"output": "",
"error": f"Ausführungsfehler: {str(e)}",
"result": None,
"exit_code": -1
}
# Temporäre Code-Datei bereinigen
try:
if os.path.exists(code_file):
os.remove(code_file)
except Exception as e:
logger.warning(f"Fehler beim Bereinigen der temporären Code-Datei: {e}")
return execution_result
def cleanup(self):
"""Temporäre Ressourcen bereinigen."""
# Bereinigung für persistente Umgebungen überspringen
if self.is_persistent and self.workflow_id:
logger.info(f"Überspringe Bereinigung für persistente Umgebung von Workflow {self.workflow_id}")
return
# Temporäres Verzeichnis bereinigen
if self.temp_dir and os.path.exists(self.temp_dir):
try:
shutil.rmtree(self.temp_dir)
logger.info(f"Temporäres Verzeichnis gelöscht: {self.temp_dir}")
except Exception as e:
logger.warning(f"Temporäres Verzeichnis {self.temp_dir} konnte nicht gelöscht werden: {e}")
def __del__(self):
"""Bereinigung während der Garbage Collection."""
self.cleanup()
# Singleton-Instanz
_coder_agent = None
def get_coder_agent():
"""Gibt eine Singleton-Instanz des Coder-Agenten zurück"""
global _coder_agent
if _coder_agent is None:
_coder_agent = AgentCoder()
return _coder_agent

View file

@ -0,0 +1,131 @@
"""
Kreativer Agent für wissensbasierte Antworten und kreative Inhaltsgenerierung.
Angepasst für die neue chat.py Architektur und chat_registry.py.
"""
import logging
from typing import Dict, Any, List, Optional
from modules.chat_registry import AgentBase
logger = logging.getLogger(__name__)
class AgentCreative(AgentBase):
"""Agent für wissensbasierte Antworten und kreative Inhaltsgenerierung"""
def __init__(self):
"""Initialisiert den kreativen Agent"""
super().__init__()
self.name = "Creative Knowledge Assistant"
self.capabilities = ("knowledge_sharing,content_creation,document_generation,"
"creative_writing,poweron,document_processing,"
"information_extraction,data_transformation,"
"document_analysis,text_processing,table_creation,"
"content_structuring")
self.result_format = "Text,Document,Table"
def get_agent_info(self) -> Dict[str, Any]:
"""Gibt Agent-Informationen für die Registry zurück"""
info = super().get_config()
info.update({
"metadata": {
"specialties": [
"creative_writing",
"documentation",
"knowledge",
"poweron",
"document_processing",
"information_extraction",
"content_transformation",
"table_generation",
"document_analysis"
]
}
})
return info
async def process_message(self, message: Dict[str, Any], context: Dict[str, Any] = None) -> Dict[str, Any]:
"""
Verarbeitet eine Nachricht und generiert eine kreative oder wissensbasierte Antwort.
Args:
message: Die zu verarbeitende Nachricht
context: Zusätzlicher Kontext
Returns:
Die generierte Antwort
"""
# Workflow-ID aus Kontext oder Nachricht extrahieren
workflow_id = context.get("workflow_id") if context else message.get("workflow_id", "unknown")
# Antwortstruktur erstellen
response = {
"role": "assistant",
"content": "",
"agent_name": self.name,
"result_format": self.result_format,
"workflow_id": workflow_id,
"documents": []
}
try:
# Benutzernachricht extrahieren
user_message = message.get("content", "")
if not user_message:
response["content"] = "Bitte geben Sie eine Nachricht an, auf die ich antworten kann."
return response
# PowerOn-Behandlung, falls in der Anfrage enthalten
if "poweron" in user_message.lower():
logger.info("PowerOn-Schlüsselwort erkannt, spezielle Antwort generieren")
poweron_prompt = f"""
Bedanke dich beim Benutzer in der Sprache seiner Anfrage ganz herzlich dafür, dass er daran denkt, dass du PowerOn bist.
Teile ihm mit, wie erfreut du bist, Teil der PowerOn-Familie zu sein, die daran arbeitet, Menschen für ein besseres Leben zu unterstützen.
Generiere dann eine kurze Antwort (1-2 Sätze) auf diese Frage: {user_message}
"""
try:
poweron_response = await self.ai_service.call_api([
{"role": "system", "content": "Du bist ein hilfreicher Assistent, der Teil der PowerOn-Familie ist."},
{"role": "user", "content": poweron_prompt}
])
response["content"] = poweron_response
return response
except Exception as e:
logger.error(f"Fehler beim Aufruf der API für PowerOn: {str(e)}")
response["content"] = "Ich bin auf einen Fehler gestoßen, während ich eine PowerOn-Antwort generierte. Bitte versuchen Sie es erneut."
return response
# Einfacher Systemprompt, der sich auf die direkte Antwort auf die Benutzeranfrage konzentriert
system_prompt = """Du bist ein hilfreicher, kreativer Assistent.
Antworte direkt auf die Anfrage des Benutzers, ohne auf einen Workflow oder Systemkontext zu verweisen.
Konzentriere dich nur darauf, eine direkte, hilfreiche Antwort auf die spezifische Frage oder Anfrage zu geben."""
# Verarbeiten mit dem KI-Service
content = await self.ai_service.call_api([
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message}
])
response["content"] = content
return response
except Exception as e:
logger.error(f"Fehler in process_message: {str(e)}")
response["content"] = f"Bei der Verarbeitung Ihrer Anfrage ist ein Fehler aufgetreten: {str(e)}"
return response
# Singleton-Instanz
_creative_agent = None
def get_creative_agent():
"""Gibt eine Singleton-Instanz des kreativen Agenten zurück"""
global _creative_agent
if _creative_agent is None:
_creative_agent = AgentCreative()
return _creative_agent

View file

@ -0,0 +1,320 @@
"""
Dokumentations-Agent für die Erstellung von Dokumentation, Berichten und strukturierten Inhalten.
Angepasst für die neue chat.py Architektur und chat_registry.py.
"""
import logging
import json
import uuid
from typing import Dict, Any, List
from datetime import datetime
from modules.chat_registry import AgentBase
logger = logging.getLogger(__name__)
class AgentDocumentation(AgentBase):
"""Agent für die Erstellung von Dokumentation und strukturierten Inhalten"""
def __init__(self):
"""Initialisiert den Dokumentations-Agent"""
super().__init__()
self.name = "Documentation Specialist"
self.capabilities = "report_generation,documentation,content_structuring,technical_writing,knowledge_organization"
self.result_format = "FormattedDocument"
def get_agent_info(self) -> Dict[str, Any]:
"""Gibt Agent-Informationen für die Registry zurück"""
info = super().get_config()
info.update({
"metadata": {
"document_types": ["manual", "report", "process", "presentation", "document"],
"formats": ["markdown", "text"]
}
})
return info
async def process_message(self, message: Dict[str, Any], context: Dict[str, Any] = None) -> Dict[str, Any]:
"""
Verarbeitet eine Nachricht und erstellt Dokumentation.
Args:
message: Eingabenachricht
context: Optionaler Kontext
Returns:
Antwortnachricht mit Dokumentation
"""
# Workflow-ID aus Kontext oder Nachricht extrahieren
workflow_id = context.get("workflow_id") if context else message.get("workflow_id", "unknown")
# Antwortstruktur erstellen
response = {
"role": "assistant",
"content": "",
"agent_name": self.name,
"result_format": self.result_format,
"workflow_id": workflow_id,
"documents": []
}
try:
# Aufgabe aus Nachricht extrahieren
task = message.get("content", "")
# Dokumenttyp erkennen
document_type = self._detect_document_type(task)
logger.info(f"Erstelle {document_type}-Dokumentation")
# Angehängte Dokumente verarbeiten
document_context = ""
if message.get("documents"):
logger.info("Verarbeite Referenzdokumente")
document_context = self._process_documents(message)
# Prompt mit Dokumentkontext erweitern
enhanced_prompt = f"{task}\n\n{document_context}" if document_context else task
# Komplexität bewerten
is_complex = self._assess_complexity(enhanced_prompt)
# Titel generieren
title = self._generate_title(enhanced_prompt, document_type)
# Inhalt basierend auf Komplexität generieren
if is_complex:
content = await self._generate_complex_document(enhanced_prompt, document_type, title)
else:
content = await self._generate_simple_document(enhanced_prompt, document_type, title)
# Dokument erstellen
doc_id = f"doc_{uuid.uuid4()}"
document = {
"id": doc_id,
"source": {
"type": "generated",
"id": doc_id,
"name": title,
"content_type": "text/markdown"
},
"contents": [
{
"type": "text",
"text": content,
"is_extracted": True
}
]
}
# Dokument zur Antwort hinzufügen
response["documents"].append(document)
# Antwortinhalt aktualisieren
response["content"] = f"Ich habe ein Dokument mit dem Titel '{title}' erstellt, das die gewünschten Informationen enthält. Das Dokument ist dieser Nachricht beigefügt."
return response
except Exception as e:
error_msg = f"Fehler bei der Dokumentationserstellung: {str(e)}"
logger.error(error_msg)
response["content"] = f"Bei der Erstellung der Dokumentation ist ein Fehler aufgetreten: {str(e)}"
return response
def _detect_document_type(self, message: str) -> str:
"""
Erkennt den Dokumenttyp aus der Nachricht.
Args:
message: Benutzernachricht
Returns:
Erkannter Dokumenttyp
"""
message = message.lower()
if any(term in message for term in ["manual", "guide", "instruction", "tutorial", "anleitung", "handbuch"]):
return "manual"
elif any(term in message for term in ["report", "analysis", "assessment", "review", "bericht", "analyse"]):
return "report"
elif any(term in message for term in ["process", "workflow", "procedure", "steps", "prozess", "ablauf"]):
return "process"
elif any(term in message for term in ["presentation", "slides", "deck", "präsentation", "folien"]):
return "presentation"
else:
return "document"
def _process_documents(self, message: Dict[str, Any]) -> str:
"""
Verarbeitet Dokumente in der Nachricht.
Args:
message: Nachricht mit Dokumenten
Returns:
Dokumentkontext als Text
"""
document_context = ""
for document in message.get("documents", []):
source = document.get("source", {})
doc_name = source.get("name", "unnamed")
document_context += f"\n\n--- {doc_name} ---\n"
for content in document.get("contents", []):
if content.get("type") == "text":
document_context += content.get("text", "")
return document_context
def _assess_complexity(self, task: str) -> bool:
"""
Bewertet die Aufgabenkomplexität.
Args:
task: Die Aufgabenbeschreibung
Returns:
True bei komplexem Dokument, sonst False
"""
# Einfache Heuristik zur Komplexitätsbewertung
complexity_indicators = [
"detailliert", "ausführlich", "umfassend", "komplex", "detailed",
"comprehensive", "in-depth", "multiple sections", "kapitel",
"abschnitte", "struktur", "analyse", "vergleich"
]
# Zählen der Komplexitätsindikatoren
indicator_count = sum(1 for indicator in complexity_indicators if indicator in task.lower())
# Weitere Indikatoren: Textlänge, Anzahl der Anforderungen
length_factor = len(task) > 500
requirements_count = task.lower().count("muss") + task.lower().count("soll") + task.lower().count("should") + task.lower().count("must")
# Komplexität basierend auf Indikatoren bestimmen
return (indicator_count >= 2) or (length_factor and requirements_count >= 3)
async def _generate_title(self, task: str, document_type: str) -> str:
"""
Generiert einen Titel für das Dokument.
Args:
task: Die Aufgabenbeschreibung
document_type: Dokumenttyp
Returns:
Generierter Titel
"""
if not self.ai_service:
return f"{document_type.capitalize()} Dokument"
prompt = f"""
Erstelle einen prägnanten, professionellen Titel für dieses {document_type}:
{task}
Antworte NUR mit dem Titel, nichts anderes.
"""
try:
title = await self.ai_service.call_api([
{"role": "system", "content": "Du erstellst Dokumenttitel."},
{"role": "user", "content": prompt}
])
# Titel bereinigen
return title.strip('"\'#*- \n\t')
except Exception:
return f"{document_type.capitalize()} Dokument"
async def _generate_complex_document(self, task: str, document_type: str, title: str) -> str:
"""
Generiert ein komplexes Dokument mit Struktur.
Args:
task: Die Aufgabenbeschreibung
document_type: Dokumenttyp
title: Dokumenttitel
Returns:
Generierter Dokumentinhalt
"""
if not self.ai_service:
return f"# {title}\n\nDokumentgenerierung nicht möglich: KI-Service nicht verfügbar."
prompt = f"""
Erstelle ein umfassendes, gut strukturiertes {document_type} mit dem Titel "{title}" basierend auf:
{task}
Das Dokument sollte Folgendes enthalten:
1. Eine klare Einleitung mit Zweck und Umfang
2. Logisch organisierte Abschnitte mit Überschriften
3. Detaillierte Inhalte mit Beispielen und Belegen
4. Ein Fazit mit den wichtigsten Erkenntnissen
5. Geeignete Formatierung mit Markdown
Formatiere das Dokument in Markdown mit korrekten Überschriften, Listen und Hervorhebungen.
"""
try:
content = await self.ai_service.call_api([
{"role": "system", "content": "Du erstellst umfassende, gut strukturierte Dokumentation."},
{"role": "user", "content": prompt}
])
# Sicherstellen, dass der Titel am Anfang steht
if not content.strip().startswith("# "):
content = f"# {title}\n\n{content}"
return content
except Exception as e:
return f"# {title}\n\nFehler bei der Dokumentgenerierung: {str(e)}"
async def _generate_simple_document(self, task: str, document_type: str, title: str) -> str:
"""
Generiert ein einfaches Dokument ohne komplexe Struktur.
Args:
task: Die Aufgabenbeschreibung
document_type: Dokumenttyp
title: Dokumenttitel
Returns:
Generierter Dokumentinhalt
"""
if not self.ai_service:
return f"# {title}\n\nDokumentgenerierung nicht möglich: KI-Service nicht verfügbar."
prompt = f"""
Erstelle ein präzises, fokussiertes {document_type} mit dem Titel "{title}" basierend auf:
{task}
Das Dokument sollte klar, präzise und auf den Punkt sein, ohne komplexe Kapitelstruktur.
Formatiere es mit Markdown und verwende geeignete Überschriften und Formatierungen.
"""
try:
content = await self.ai_service.call_api([
{"role": "system", "content": "Du erstellst präzise, fokussierte Dokumentation."},
{"role": "user", "content": prompt}
])
# Sicherstellen, dass der Titel am Anfang steht
if not content.strip().startswith("# "):
content = f"# {title}\n\n{content}"
return content
except Exception as e:
return f"# {title}\n\nFehler bei der Dokumentgenerierung: {str(e)}"
# Singleton-Instanz
_documentation_agent = None
def get_documentation_agent():
"""Gibt eine Singleton-Instanz des Dokumentations-Agenten zurück"""
global _documentation_agent
if _documentation_agent is None:
_documentation_agent = AgentDocumentation()
return _documentation_agent

View file

@ -0,0 +1,654 @@
"""
Webcrawler-Agent für Recherche und Abruf von Informationen aus dem Web.
Angepasst für die neue chat.py Architektur und chat_registry.py.
"""
import json
import logging
import time
import traceback
from typing import Dict, Any, List, Optional
from urllib.parse import quote_plus, unquote
from bs4 import BeautifulSoup
import requests
from modules.chat_registry import AgentBase
from modules.utility import APP_CONFIG
logger = logging.getLogger(__name__)
class AgentWebcrawler(AgentBase):
"""Agent für Webrecherche und Informationsabruf"""
def __init__(self):
"""Initialisiert den Webcrawler-Agent"""
super().__init__()
self.name = "Webscraper"
self.capabilities = "web_search,information_retrieval,data_collection,source_verification,content_integration"
self.result_format = "SearchResults"
# Web-Crawling-Konfiguration
self.max_url = int(APP_CONFIG.get("Connector_AiWebscraping_MAX_URLS"))
self.max_key = int(APP_CONFIG.get("Connector_AiWebscraping_MAX_SEARCH_KEYWORDS"))
self.max_result = int(APP_CONFIG.get("Connector_AiWebscraping_MAX_SEARCH_RESULTS"))
self.timeout = int(APP_CONFIG.get("Connector_AiWebscraping_TIMEOUT"))
def get_agent_info(self) -> Dict[str, Any]:
"""Gibt Agent-Informationen für die Registry zurück"""
info = super().get_config()
info.update({
"metadata": {
"max_url": self.max_url,
"max_result": self.max_result,
"timeout": self.timeout
}
})
return info
async def process_message(self, message: Dict[str, Any], context: Dict[str, Any] = None) -> Dict[str, Any]:
"""
Verarbeitet eine Nachricht und führt bei Bedarf eine Webrecherche durch.
Args:
message: Die zu verarbeitende Nachricht
context: Zusätzlicher Kontext
Returns:
Die generierte Antwort oder Ablehnung, wenn keine Webrecherche erforderlich ist
"""
# Workflow-ID aus Kontext oder Nachricht extrahieren
workflow_id = context.get("workflow_id") if context else message.get("workflow_id", "unknown")
# Antwortstruktur erstellen
response = {
"role": "assistant",
"content": "",
"agent_name": self.name,
"result_format": self.result_format,
"workflow_id": workflow_id
}
try:
# Abfrage aus der Nachricht abrufen
prompt = message.get("content", "").strip()
# Prüfen, ob es sich explizit um eine Webrecherche-Anfrage handelt
is_web_research = await self._is_web_research_request(prompt)
if not is_web_research:
# Keine Webrecherche-Anfrage ablehnen
logger.info("Anfrage abgelehnt: keine Webrecherche-Aufgabe")
response["content"] = "Diese Anfrage scheint keine Webrecherche zu erfordern. Weiterleitung an einen passenderen Agenten."
response["status"] = "rejected"
return response
# Mit Webrecherche fortfahren
logger.info(f"Webrecherche für: {prompt[:50]}...")
# Suchstrategie vorbereiten
logger.info("Erstelle Suchstrategie")
search_strategy = await self._create_search_strategy(prompt)
search_keys = search_strategy.get("skey", [])
search_urls = search_strategy.get("url", [])
if search_keys:
logger.info(f"Suche nach {len(search_keys)} Schlüsselbegriffen: {', '.join(search_keys[:2])}...")
if search_urls:
logger.info(f"Suche in {len(search_urls)} direkten URLs: {', '.join(search_urls[:2])}...")
# Suche ausführen
results = []
# Suchbegriffe verarbeiten
for keyword in search_keys:
logger.info(f"Suche im Web nach: '{keyword}'")
keyword_results = self._search_web(keyword)
results.extend(keyword_results)
logger.info(f"Gefunden: {len(keyword_results)} Ergebnisse für '{keyword}'")
# Direkte URLs verarbeiten
for url in search_urls:
logger.info(f"Extrahiere Inhalt von: {url}")
soup = self._read_url(url)
# Titel aus der Seite extrahieren, falls vorhanden
title = self._extract_title(soup, url)
result = self._parse_result(soup, title, url)
results.append(result)
logger.info(f"Extrahiert: '{title}' von {url}")
# Ergebnisse für die endgültige Ausgabe verarbeiten
logger.info(f"Analysiere {len(results)} Web-Ergebnisse")
# Zusammenfassungen für jedes Ergebnis generieren
processed_results = []
for i, result in enumerate(results):
result_data_limited = self._limit_text(result['data'], max_chars=10000)
logger.info(f"Analysiere Ergebnis {i+1}/{len(results)}: {result['title'][:30]}...")
content_summary = await self._summarize_result(result_data_limited, prompt)
processed_result = {
"title": result['title'],
"url": result['url'],
"snippet": result['snippet'],
"summary": content_summary
}
processed_results.append(processed_result)
# Gesamtzusammenfassung erstellen
all_summaries = "\n\n".join([r["summary"] for r in processed_results])
all_summaries_limited = self._limit_text(all_summaries, max_chars=10000)
logger.info("Erstelle Gesamtzusammenfassung der Webrecherche")
final_summary = await self.ai_service.call_api([
{"role": "system", "content": "Du erstellst prägnante Zusammenfassungen von Rechercheergebnissen."},
{"role": "user", "content": f"Bitte fasse diese Erkenntnisse in 5-6 Sätzen zusammen: {all_summaries_limited}\n"}
])
# Sprache der Anfrage ermitteln, um Überschriften in der richtigen Sprache zu verwenden
headers = await self._get_localized_headers(prompt)
# Endgültiges Ergebnis formatieren
final_result = f"## {headers['web_research_results']}\n\n### {headers['summary']}\n{final_summary}\n\n### {headers['detailed_results']}\n"
for i, result in enumerate(processed_results, 1):
final_result += f"\n\n[{i}] {result['title']}\n{headers['url']}: {result['url']}\n{headers['snippet']}: {result['snippet']}\n{headers['content']}: {result['summary']}"
# Inhalt in der Antwort setzen
response["content"] = final_result
logger.info("Webrecherche erfolgreich abgeschlossen")
return response
except Exception as e:
error_msg = f"Fehler bei der Webrecherche: {str(e)}"
logger.error(error_msg)
response["content"] = f"## Fehler bei der Webrecherche\n\n{error_msg}"
return response
async def _is_web_research_request(self, prompt: str) -> bool:
"""
Verwendet KI, um festzustellen, ob eine Anfrage Webrecherche erfordert.
Args:
prompt: Die Benutzeranfrage
Returns:
True, wenn es explizit eine Webrecherche-Anfrage ist, sonst False
"""
if not self.ai_service:
# Fallback zur einfacheren Erkennung, wenn kein KI-Service verfügbar ist
return self._simple_web_detection(prompt)
try:
# Prompt erstellen, um zu analysieren, ob es sich um eine Webrecherche-Anfrage handelt
analysis_prompt = f"""
Analysiere die folgende Anfrage und bestimme, ob sie explizit eine Webrecherche oder Online-Informationen erfordert.
ANFRAGE: {prompt}
Eine Anfrage erfordert Webrecherche, wenn:
1. Sie explizit nach der Suche von Informationen online fragt
2. Sie URLs oder Verweise auf Websites enthält
3. Sie aktuelle Informationen anfordert, die im Web verfügbar wären
4. Sie nach Informationen aus Web-Quellen fragt
5. Sie implizit aktuelle Informationen aus dem Internet erfordert
Antworte NUR mit einem einzelnen Wort - entweder "JA", wenn Webrecherche erforderlich ist, oder "NEIN", wenn nicht.
Füge KEINE Erklärung hinzu, nur die Antwort JA oder NEIN.
"""
# KI zur Analyse aufrufen
response = await self.ai_service.call_api([
{"role": "system", "content": "Du bestimmst, ob eine Anfrage Webrecherche erfordert. Antworte immer nur mit JA oder NEIN."},
{"role": "user", "content": analysis_prompt}
])
# Antwort bereinigen und überprüfen
response = response.strip().upper()
return "JA" in response
except Exception as e:
# Fehler protokollieren, aber nicht fehlschlagen, Fallback zur einfacheren Erkennung
logger.warning(f"Fehler bei der KI-Erkennung von Webrecherche-Anfragen: {str(e)}")
return self._simple_web_detection(prompt)
def _simple_web_detection(self, prompt: str) -> bool:
"""
Einfachere Fallback-Methode zur Erkennung von Webrecherche-Anfragen anhand von URLs.
Args:
prompt: Die Benutzeranfrage
Returns:
True, wenn es klare URL-Indikatoren gibt, sonst False
"""
# URLs in der Anfrage deuten stark auf Webrecherche hin
url_indicators = ["http://", "https://", "www.", ".com", ".org", ".net", ".edu", ".gov"]
web_terms = ["search", "find online", "look up", "web", "internet", "website", "suche", "finde", "recherchiere"]
# Auf URL-Muster in der Anfrage prüfen
contains_url = any(indicator in prompt.lower() for indicator in url_indicators)
contains_web_term = any(term in prompt.lower() for term in web_terms)
return contains_url or contains_web_term
async def _create_search_strategy(self, prompt: str) -> Dict[str, List[str]]:
"""
Erstellt eine Suchstrategie basierend auf der Anfrage.
Args:
prompt: Die Benutzeranfrage
Returns:
Suchstrategie mit URLs und Suchbegriffen
"""
if not self.ai_service:
# Fallback zur einfachen Strategie
return {"skey": [prompt], "url": []}
try:
# KI-Prompt zur Erstellung einer Suchstrategie
strategy_prompt = f"""Erstelle eine umfassende Webrecherchestrategie für die Aufgabe = '{prompt.replace("'","")}'. Gib die Ergebnisse als Python-Dictionary mit diesen spezifischen Schlüsseln zurück. Wenn bestimmte URLs angegeben sind und die Aufgabe nur die Analyse dieser URLs erfordert, lass 'skey' leer.
'url': Eine Liste von maximal {self.max_url} spezifischen URLs, die aus der Aufgabenstellung extrahiert wurden.
'skey': Eine Liste von maximal {self.max_key} Schlüsselsätzen, nach denen im Web gesucht werden soll. Diese sollten präzise, vielfältig und gezielt sein, um die relevantesten Informationen zu erhalten.
Formatiere deine Antwort als gültiges JSON-Objekt mit diesen beiden Schlüsseln. Füge keinen erklärenden Text oder Markdown außerhalb der Objektdefinition hinzu.
"""
# KI für Suchstrategie aufrufen
content_text = await self.ai_service.call_api([
{"role": "system", "content": "Du bist ein Webrecherche-Experte, der präzise Suchstrategien entwickelt."},
{"role": "user", "content": strategy_prompt}
])
# JSON-Code-Block-Markierungen entfernen, falls vorhanden
if content_text.startswith("```json"):
end_marker = "```"
end_index = content_text.rfind(end_marker)
if end_index != -1:
content_text = content_text[7:end_index].strip()
# JSON parsen und zurückgeben
strategy = json.loads(content_text)
return strategy
except Exception as e:
logger.error(f"Fehler bei der Erstellung der Suchstrategie: {str(e)}")
# Einfache Fallback-Strategie
return {"skey": [prompt], "url": []}
async def _summarize_result(self, result_data: str, original_prompt: str) -> str:
"""
Erstellt eine Zusammenfassung eines Suchergebnisses mit KI.
Args:
result_data: Die zu zusammenfassenden Daten
original_prompt: Die ursprüngliche Anfrage
Returns:
Zusammenfassung des Ergebnisses
"""
if not self.ai_service:
return "Keine Zusammenfassung verfügbar (KI-Service nicht verfügbar)"
try:
# Anweisungen für die Zusammenfassung
summary_prompt = f"""
Fasse dieses Suchergebnis gemäß der ursprünglichen Anfrage in etwa 2000 Zeichen zusammen. Ursprüngliche Anfrage = '{original_prompt.replace("'","")}'
Konzentriere dich auf die wichtigsten Erkenntnisse und verbinde sie mit der ursprünglichen Anfrage. Du kannst jede Einleitung überspringen.
Extrahiere nur relevante und hochwertige Informationen im Zusammenhang mit der Anfrage und präsentiere sie in einem klaren Format. Biete eine ausgewogene Ansicht der recherchierten Informationen.
Hier ist das Suchergebnis:
{result_data}
"""
# KI für Zusammenfassung aufrufen
summary = await self.ai_service.call_api([
{"role": "system", "content": "Du bist ein Informationsanalyst, der Webinhalte präzise und relevant zusammenfasst."},
{"role": "user", "content": summary_prompt}
])
# Auf ~2000 Zeichen begrenzen
return summary[:2000]
except Exception as e:
logger.error(f"Fehler bei der Zusammenfassung des Ergebnisses: {str(e)}")
return "Fehler bei der Zusammenfassung"
async def _get_localized_headers(self, text: str) -> Dict[str, str]:
"""
Ermittelt lokalisierte Überschriften für die Webrecherche-Ergebnisse basierend auf der erkannten Sprache.
Args:
text: Text zur Spracherkennung
Returns:
Dictionary mit lokalisierten Überschriften
"""
# Standard-Englische Überschriften
headers = {
"web_research_results": "Web Research Results",
"summary": "Summary",
"detailed_results": "Detailed Results",
"url": "URL",
"snippet": "Snippet",
"content": "Content"
}
if not self.ai_service:
return headers
try:
# Sprache erkennen
language_prompt = f"In welcher Sprache ist dieser Text geschrieben? Antworte nur mit dem Sprachnamen: {text[:200]}"
language = await self.ai_service.call_api([
{"role": "system", "content": "Du bestimmst die Sprache eines Textes und gibst nur den Sprachnamen zurück."},
{"role": "user", "content": language_prompt}
])
language = language.strip().lower()
# Englische Sprache oder Spracherkennung fehlgeschlagen, Standardüberschriften zurückgeben
if language in ["english", "en", ""]:
return headers
# Deutsche Überschriften
if language in ["deutsch", "german", "de"]:
return {
"web_research_results": "Webrecherche-Ergebnisse",
"summary": "Zusammenfassung",
"detailed_results": "Detaillierte Ergebnisse",
"url": "URL",
"snippet": "Ausschnitt",
"content": "Inhalt"
}
# Französische Überschriften
if language in ["französisch", "french", "fr"]:
return {
"web_research_results": "Résultats de recherche Web",
"summary": "Résumé",
"detailed_results": "Résultats détaillés",
"url": "URL",
"snippet": "Extrait",
"content": "Contenu"
}
# Überschriften übersetzen, wenn Sprache erkannt, aber keine vordefinierte Übersetzung
translation_prompt = f"""
Übersetze diese Webrecherche-Ergebnisüberschriften ins {language}:
Web Research Results
Summary
Detailed Results
URL
Snippet
Content
Gib ein JSON-Objekt mit diesen Schlüsseln zurück:
web_research_results, summary, detailed_results, url, snippet, content
"""
# KI für Übersetzung aufrufen
response = await self.ai_service.call_api([
{"role": "system", "content": "Du übersetzt Überschriften in die angegebene Sprache und gibst sie als JSON zurück."},
{"role": "user", "content": translation_prompt}
])
# JSON extrahieren
import re
json_match = re.search(r'\{.*\}', response, re.DOTALL)
if json_match:
translated_headers = json.loads(json_match.group(0))
return translated_headers
except Exception as e:
# Fehler protokollieren, aber mit englischen Überschriften fortfahren
logger.warning(f"Fehler beim Übersetzen der Überschriften: {str(e)}")
return headers
def _search_web(self, query: str) -> List[Dict[str, str]]:
"""
Führt eine Websuche durch und gibt die Ergebnisse zurück.
Args:
query: Die Suchanfrage
Returns:
Liste von Suchergebnissen
"""
formatted_query = quote_plus(query)
url = f"{APP_CONFIG.get('Connector_AiWebscraping_SEARCH_ENGINE')}{formatted_query}"
search_results_soup = self._read_url(url)
if not isinstance(search_results_soup, BeautifulSoup) or not search_results_soup.select('.result'):
logger.warning(f"Keine Suchergebnisse gefunden für: {query}")
return []
# Suchergebnisse extrahieren
results = []
# Alle Ergebniscontainer finden
result_elements = search_results_soup.select('.result')
for result in result_elements:
# Titel extrahieren
title_element = result.select_one('.result__a')
title = title_element.text.strip() if title_element else 'Kein Titel'
# URL extrahieren (DuckDuckGo verwendet Weiterleitungen)
url_element = title_element.get('href') if title_element else ''
extracted_url = 'Keine URL'
if url_element:
# Tatsächliche URL aus DuckDuckGos Weiterleitung extrahieren
if url_element.startswith('/d.js?q='):
start = url_element.find('?q=') + 3
end = url_element.find('&', start) if '&' in url_element[start:] else None
extracted_url = unquote(url_element[start:end])
# Sicherstellen, dass die URL das korrekte Protokollpräfix hat
if not extracted_url.startswith(('http://', 'https://')):
if not extracted_url.startswith('//'):
extracted_url = 'https://' + extracted_url
else:
extracted_url = 'https:' + extracted_url
else:
extracted_url = url_element
# Snippet direkt aus der Suchergebnisseite extrahieren
snippet_element = result.select_one('.result__snippet')
snippet = snippet_element.text.strip() if snippet_element else 'Keine Beschreibung'
# Tatsächlichen Seiteninhalt für das Datenfeld abrufen
target_page_soup = self._read_url(extracted_url)
# Neue Inhaltsextraktionsmethode verwenden, um Inhaltsgröße zu begrenzen
content = self._extract_main_content(target_page_soup)
results.append({
'title': title,
'url': extracted_url,
'snippet': snippet,
'data': content
})
# Anzahl der Ergebnisse bei Bedarf begrenzen
if len(results) >= self.max_result:
break
return results
def _read_url(self, url: str) -> BeautifulSoup:
"""
Liest eine URL und gibt einen BeautifulSoup-Parser für den Inhalt zurück.
Args:
url: Die zu lesende URL
Returns:
BeautifulSoup-Objekt mit dem Inhalt oder leer bei Fehlern
"""
headers = {
'User-Agent': APP_CONFIG.get("Connector_AiWebscraping_USER_AGENT"),
'Accept': 'text/html,application/xhtml+xml,application/xml',
'Accept-Language': 'en-US,en;q=0.9',
}
try:
# Initiale Anfrage
response = requests.get(url, headers=headers, timeout=self.timeout)
# Abfragen für Status 202
if response.status_code == 202:
# Maximal 3 Versuche mit zunehmenden Intervallen
backoff_times = [0.5, 1.0, 2.0, 5.0]
for wait_time in backoff_times:
time.sleep(wait_time) # Mit zunehmender Zeit warten
response = requests.get(url, headers=headers, timeout=self.timeout)
# Wenn kein 202 mehr, dann abbrechen
if response.status_code != 202:
break
# Für andere Fehlerstatuscodes einen Fehler auslösen
response.raise_for_status()
# HTML parsen
return BeautifulSoup(response.text, 'html.parser')
except Exception as e:
logger.error(f"Fehler beim Lesen der URL {url}: {str(e)}")
# Leeres BeautifulSoup-Objekt erstellen
return BeautifulSoup("<html><body></body></html>", 'html.parser')
def _extract_title(self, soup: BeautifulSoup, url: str) -> str:
"""
Extrahiert den Titel aus einer Webseite.
Args:
soup: BeautifulSoup-Objekt der Webseite
url: URL der Webseite
Returns:
Extrahierter Titel
"""
if not isinstance(soup, BeautifulSoup):
return f"Fehler bei {url}"
# Titel aus dem title-Tag extrahieren
title_tag = soup.find('title')
title = title_tag.text.strip() if title_tag else "Kein Titel"
# Alternative: Auch nach h1-Tags suchen, wenn der title-Tag fehlt
if title == "Kein Titel":
h1_tag = soup.find('h1')
if h1_tag:
title = h1_tag.text.strip()
return title
def _extract_main_content(self, soup: BeautifulSoup, max_chars: int = 10000) -> str:
"""
Extrahiert den Hauptinhalt aus einer HTML-Seite.
Args:
soup: BeautifulSoup-Objekt der Webseite
max_chars: Maximale Anzahl von Zeichen
Returns:
Extrahierter Hauptinhalt als String
"""
if not isinstance(soup, BeautifulSoup):
return str(soup)[:max_chars] if soup else ""
# Versuchen, Hauptinhaltselemente in Prioritätsreihenfolge zu finden
main_content = None
for selector in ['main', 'article', '#content', '.content', '#main', '.main']:
content = soup.select_one(selector)
if content:
main_content = content
break
# Wenn kein Hauptinhalt gefunden wurde, den Body verwenden
if not main_content:
main_content = soup.find('body') or soup
# Skript-, Style-, Nav-, Footer-Elemente entfernen, die nicht zum Hauptinhalt beitragen
for element in main_content.select('script, style, nav, footer, header, aside, .sidebar, #sidebar, .comments, #comments, .advertisement, .ads, iframe'):
element.extract()
# Textinhalt extrahieren
text_content = main_content.get_text(separator=' ', strip=True)
# Auf max_chars begrenzen
return text_content[:max_chars]
def _parse_result(self, soup: BeautifulSoup, title: str, url: str) -> Dict[str, str]:
"""
Parst ein BeautifulSoup-Objekt in ein Ergebnis-Dictionary.
Args:
soup: BeautifulSoup-Objekt der Webseite
title: Seitentitel
url: Seiten-URL
Returns:
Dictionary mit Ergebnisdaten
"""
# Inhalt extrahieren
content = self._extract_main_content(soup)
result = {
'title': title,
'url': url,
'snippet': 'Keine Beschreibung', # Standardwert
'data': content
}
return result
def _limit_text(self, text: str, max_chars: int = 10000) -> str:
"""
Begrenzt den Text auf eine maximale Anzahl von Zeichen.
Args:
text: Eingangstext
max_chars: Maximale Anzahl von Zeichen
Returns:
Begrenzter Text
"""
if not text:
return ""
# Wenn der Text bereits unter dem Limit liegt, unverändert zurückgeben
if len(text) <= max_chars:
return text
# Andernfalls den Text auf max_chars begrenzen
return text[:max_chars] + "... [Inhalt aufgrund der Länge gekürzt]"
# Singleton-Instanz
_webcrawler_agent = None
def get_webcrawler_agent():
"""Gibt eine Singleton-Instanz des Webcrawler-Agenten zurück"""
global _webcrawler_agent
if _webcrawler_agent is None:
_webcrawler_agent = AgentWebcrawler()
return _webcrawler_agent

213
modules/chat_registry.py Normal file
View file

@ -0,0 +1,213 @@
"""
Chat Agent Registry Modul.
Stellt ein zentrales Registry-System für alle verfügbaren Agenten bereit.
"""
import os
import logging
import importlib
from typing import Dict, Any, List, Optional
logger = logging.getLogger(__name__)
class AgentRegistry:
"""Zentrale Registry für alle verfügbaren Agenten im System."""
_instance = None
@classmethod
def get_instance(cls):
"""Gibt eine Singleton-Instanz der Agent-Registry zurück."""
if cls._instance is None:
cls._instance = cls()
return cls._instance
def __init__(self):
"""Initialisiert die Agent-Registry."""
if AgentRegistry._instance is not None:
raise RuntimeError("Singleton-Instanz existiert bereits - verwende get_instance()")
self.agents = {}
self.ai_service = None
self._load_agents()
def _load_agents(self):
"""Lädt alle verfügbaren Agenten aus den Modulen."""
logger.info("Lade Agent-Module...")
# Liste der zu ladenden Agent-Module
agent_modules = []
agent_dir = os.path.dirname(__file__)
# Durchsuche das Verzeichnis nach Agent-Modulen
for filename in os.listdir(agent_dir):
if filename.startswith("chat_agent_") and filename.endswith(".py"):
agent_modules.append(filename[:-3]) # Entferne .py-Endung
if not agent_modules:
logger.warning("Keine Agent-Module gefunden")
return
logger.info(f"{len(agent_modules)} Agent-Module gefunden")
# Lade jedes Agent-Modul
for module_name in agent_modules:
try:
# Importiere das Modul
module = importlib.import_module(f"modules.{module_name}")
# Suche nach der Agent-Klasse oder einer get_*_agent-Funktion
agent_name= module_name.split('_')[-1]
class_name = f"Agent{agent_name.capitalize()}"
getter_name = f"get_{agent_name}_agent"
agent = None
# Versuche, den Agenten über die get_*_agent-Funktion zu erhalten
if hasattr(module, getter_name):
getter_func = getattr(module, getter_name)
agent = getter_func()
logger.info(f"Agent '{agent.name}' über {getter_name}() geladen")
# Alternativ versuche, den Agenten direkt zu instanziieren
elif hasattr(module, class_name):
agent_class = getattr(module, class_name)
agent = agent_class()
logger.info(f"Agent '{agent.name}' (Typ: {agent.name}) direkt instanziert")
if agent:
# Registriere den Agenten
self.register_agent(agent)
else:
logger.warning(f"Keine Agent-Klasse oder Getter-Funktion in Modul {module_name} gefunden")
except ImportError as e:
logger.error(f"Modul {module_name} konnte nicht importiert werden: {e}")
except Exception as e:
logger.error(f"Fehler beim Laden des Agenten aus Modul {module_name}: {e}")
def set_ai_service(self, ai_service):
self.ai_service = ai_service
self.update_agent_dependencies()
def update_agent_dependencies(self):
"""Aktualisiert die Abhängigkeiten für alle registrierten Agenten."""
for agent_id, agent in self.agents.items():
if hasattr(agent, 'set_dependencies'):
agent.set_dependencies(ai_service=self.ai_service)
def register_agent(self, agent):
"""
Registriert einen Agenten in der Registry.
Args:
agent: Der zu registrierende Agent
"""
agent_id = getattr(agent, 'name', "unknown_agent")
# Initialisiere Agenten mit Abhängigkeiten
if hasattr(agent, 'set_dependencies'):
agent.set_dependencies(ai_service=self.ai_service)
self.agents[agent_id] = agent
logger.debug(f"Agent '{agent.name}' (Typ: {agent_id}, Name: {agent_id}) registriert")
def get_agent(self, agent_identifier: str):
"""
Gibt eine Agenten-Instanz zurück
Args:
agent_identifier: ID oder Typ des gewünschten Agenten
Returns:
Agenten-Instanz oder None, falls nicht gefunden
"""
if agent_identifier in self.agents:
return self.agents[agent_identifier]
logger.error(f"Agent mit Kennung '{agent_identifier}' nicht gefunden")
return None
def get_all_agents(self) -> Dict[str, Any]:
"""Gibt alle registrierten Agenten zurück."""
return self.agents
def get_agent_infos(self) -> List[Dict[str, Any]]:
"""Gibt Informationen über alle registrierten Agenten zurück."""
agent_infos = []
seen_agents = set()
for agent in self.agents.values():
if agent not in seen_agents:
# Verwende get_agent_info oder erstelle manuell die Info
if hasattr(agent, 'get_agent_info'):
agent_infos.append(agent.get_agent_info())
else:
agent_infos.append({
"name": agent.name,
"capabilities": getattr(agent, 'capabilities', ""),
"result_format": getattr(agent, 'result_format', "Text")
})
logger.error(f"Agent mit Kennung '{agent.name}' hat keine vollständigen Daten")
seen_agents.add(agent)
return agent_infos
# Basis-Agent-Klasse
class AgentBase:
"""
Basis-Klasse für alle Chat-Agenten.
Definiert die grundlegende Schnittstelle und Funktionalität.
"""
def __init__(self):
"""Initialisiere den Basis-Agenten."""
self.name = "Basis-Agent"
self.capabilities = "Grundlegende Agentenfunktionen"
self.result_format = "Text"
self.ai_service = None
def set_dependencies(self, ai_service=None):
self.ai_service = ai_service
def get_config(self) -> Dict[str, Any]:
return {
"name": self.name,
"capabilities": self.capabilities,
"result_format": self.result_format
}
async def process_message(self, message: Dict[str, Any], context: Dict[str, Any] = None) -> Dict[str, Any]:
# Basisimplementierung - sollte von spezialisierten Agenten überschrieben werden
if not self.ai_service:
logger.warning(f"Agent {self.id} hat keinen konfigurierten AI-Service")
return {
"role": "assistant",
"content": f"Ich bin {self.name}, aber ich bin nicht richtig konfiguriert. Bitte den AI-Service einrichten.",
"agent_name": self.name,
"result_format": "Text"
}
# Einfachen Prompt erstellen
prompt = message.get("content", "")
# Antwort generieren
try:
response_content = self.ai_service.call_api([
{"role": "system", "content": f"Du bist {self.name}, ein spezialisierter {self.name}-Agent mit Fähigkeiten in: {self.capabilities}"},
{"role": "user", "content": prompt}
])
return {
"role": "assistant",
"content": response_content,
"agent_name": self.name,
"result_format": self.result_format
}
except Exception as e:
logger.error(f"Fehler in Agent {self.id}: {str(e)}")
return {
"role": "assistant",
"content": f"Ich habe einen Fehler festgestellt: {str(e)}",
"agent_name": self.name,
"result_format": "Text"
}
# Singleton-Factory für die Agent-Registry
def get_agent_registry():
return AgentRegistry.get_instance()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,139 @@
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional
from datetime import datetime
class Label(BaseModel):
"""Label für ein Attribut oder eine Klasse mit Unterstützung für mehrere Sprachen"""
default: str
translations: Dict[str, str] = {}
def get_label(self, language: str = None):
"""Gibt das Label in der angegebenen Sprache zurück, oder den Standardwert wenn nicht verfügbar"""
if language and language in self.translations:
return self.translations[language]
return self.default
class Prompt(BaseModel):
"""Datenmodell für einen Prompt"""
id: int = Field(description="Eindeutige ID des Prompts")
mandate_id: int = Field(description="ID des zugehörigen Mandanten")
user_id: int = Field(description="ID des Erstellers")
content: str = Field(description="Inhalt des Prompts")
name: str = Field(description="Anzeigename des Prompts")
label: Label = Field(
default=Label(default="Prompt", translations={"en": "Prompt", "fr": "Invite"}),
description="Label für die Klasse"
)
# Labels für Attribute
field_labels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandate_id": Label(default="Mandanten-ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
"user_id": Label(default="Benutzer-ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
"content": Label(default="Inhalt", translations={"en": "Content", "fr": "Contenu"}),
"name": Label(default="Name", translations={"en": "Label", "fr": "Nom"}),
}
class FileItem(BaseModel):
"""Datenmodell für ein Datenobjekt"""
id: int = Field(description="Eindeutige ID des Datenobjekts")
mandate_id: int = Field(description="ID des zugehörigen Mandanten")
user_id: int = Field(description="ID des Erstellers")
name: str = Field(description="Name des Datenobjekts")
mime_type: str = Field(description="Typ des Datenobjekts MIME-Typ")
size: Optional[str] = Field(None, description="Größe des Datenobjekts")
file_hash: str = Field(description="Hash code")
data: bytes = Field(description="Inhalt der Datei")
creation_date: Optional[str] = Field(None, description="Datum des Hochladens")
label: Label = Field(
default=Label(default="Datenobjekt", translations={"en": "Data Object", "fr": "Objet de données"}),
description="Label für die Klasse"
)
# Labels für Attribute
field_labels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandate_id": Label(default="Mandanten-ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
"user_id": Label(default="Benutzer-ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
"name": Label(default="Name", translations={"en": "Name", "fr": "Nom"}),
"mime_type": Label(default="Typ", translations={"en": "Type", "fr": "Type"}),
"size": Label(default="Größe", translations={"en": "Size", "fr": "Taille"}),
"file_hash": Label(default="File-Hash", translations={"en": "Hash", "fr": "Hash"}),
"data": Label(default="Daten", translations={"en": "Data", "fr": "Contenu"}),
"creation_date": Label(default="Upload-Datum", translations={"en": "Upload date", "fr": "Date de téléchargement"})
}
# Workflow-Modellklassen
class DocumentContent(BaseModel):
"""Inhalt eines Dokuments im Workflow"""
sequence_nr: Optional[int] = Field(1,description="Sequenz-Nummer des Inhaltes im Quelldokument")
name: str = Field(description="Optionale Bezeichnung")
ext: str = Field(description="Content extension for export: txt, csv, json, jpg, png")
content_type: str = Field(description="MIME-Typ")
data: bytes = Field(description="Inhalt der Datei")
class Document(BaseModel):
"""Dokument im Workflow """
id: str = Field(description="Eindeutige ID des Dokuments")
file_id: int = Field(description="Quelldatei")
contents: List[DocumentContent] = Field(description="Dokumentinhalte")
class DataStats(BaseModel):
"""Statistiken für Performance und Datennutzung"""
processing_time: Optional[float] = Field(None, description="Verarbeitungszeit in Sekunden")
token_count: Optional[int] = Field(None, description="Token-Anzahl (für KI-Modelle)")
bytes_sent: Optional[int] = Field(None, description="Gesendete Bytes")
bytes_received: Optional[int] = Field(None, description="Empfangene Bytes")
class Message(BaseModel):
"""Nachrichtenobjekt im Workflow"""
id: str = Field(description="Eindeutige ID der Nachricht")
workflow_id: str = Field(description="Referenz zum übergeordneten Workflow")
parent_message_id: Optional[str] = Field(None, description="Referenz zur beantworteten Nachricht")
started_at: str = Field(description="Zeitstempel für Nachrichtenerstellung")
finished_at: Optional[str] = Field(None, description="Zeitstempel für Nachrichtenabschluss")
sequence_no: int = Field(description="Sequenznummer für Sortierung")
status: str = Field(description="Status der Nachricht ('processing', 'completed')")
role: str = Field(description="Rolle des Absenders ('system', 'user', 'assistant')")
data_stats: Optional[DataStats] = Field(None, description="Statistiken")
documents: Optional[List[Document]] = Field(None, description="Dokumente in dieser Nachricht")
content: Optional[str] = Field(None, description="Textinhalt der Nachricht")
agent_name: Optional[str] = Field(None, description="Name des verwendeten Agenten")
class Workflow(BaseModel):
"""Workflow-Objekt für Multi-Agent-System"""
id: str = Field(description="Eindeutige ID des Workflows")
name: Optional[str] = Field(None, description="Name des Workflows")
mandate_id: int = Field(description="ID des Mandanten")
user_id: int = Field(description="ID des Benutzers")
status: str = Field(description="Status des Workflows ('running', 'failed', 'stopped')")
started_at: str = Field(description="Startzeitpunkt")
last_activity: str = Field(description="Zeitpunkt der letzten Aktivität")
last_message_id: str = Field(description="The last registered message")
data_stats: Optional[Dict[str, Any]] = Field(None, description="Gesamt-Statistiken")
messages: List[Message] = Field(default=[], description="Nachrichtenverlauf")
logs: List[Dict[str, Any]] = Field(default=[], description="Protokolleinträge")
# Anfragemodelle für die API
class WorkflowCreateRequest(BaseModel):
"""Anfrage zur Erstellung eines neuen Workflows"""
name: Optional[str] = Field(None, description="Name des Workflows")
prompt: str = Field(description="Zu verwendender Prompt")
files: List[int] = Field(default=[], description="Liste von FileItem ID")
class UserInputRequest(BaseModel):
"""Anfrage für Benutzereingabe an einen laufenden Workflow"""
prompt: str = Field(description="Nachricht des Benutzers")
files: List[int] = Field(default=[], description="Liste zusätzlicher FileItem ID")

View file

@ -15,37 +15,6 @@ class Label(BaseModel):
return self.default
class FileItem(BaseModel):
"""Datenmodell für ein Datenobjekt"""
id: int = Field(description="Eindeutige ID des Datenobjekts")
mandate_id: int = Field(description="ID des zugehörigen Mandanten")
user_id: int = Field(description="ID des Erstellers")
name: str = Field(description="Name des Datenobjekts")
type: str = Field(description="Typ des Datenobjekts ('document', 'image', etc.)")
size: Optional[str] = Field(None, description="Größe des Datenobjekts")
upload_date: Optional[str] = Field(None, description="Datum des Hochladens")
content_type: Optional[str] = Field(None, description="Content-Type des Datenobjekts")
path: Optional[str] = Field(None, description="Pfad zum Datenobjekt")
label: Label = Field(
default=Label(default="Datenobjekt", translations={"en": "Data Object", "fr": "Objet de données"}),
description="Label für die Klasse"
)
# Labels für Attribute
field_labels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandate_id": Label(default="Mandanten-ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
"user_id": Label(default="Benutzer-ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
"name": Label(default="Name", translations={"en": "Name", "fr": "Nom"}),
"type": Label(default="Typ", translations={"en": "Type", "fr": "Type"}),
"size": Label(default="Größe", translations={"en": "Size", "fr": "Taille"}),
"upload_date": Label(default="Upload-Datum", translations={"en": "Upload date", "fr": "Date de téléchargement"}),
"content_type": Label(default="Content-Type", translations={"en": "Content type", "fr": "Type de contenu"}),
"path": Label(default="Pfad", translations={"en": "Path", "fr": "Chemin"})
}
class Prompt(BaseModel):
"""Datenmodell für einen Prompt"""
id: int = Field(description="Eindeutige ID des Prompts")
@ -69,29 +38,53 @@ class Prompt(BaseModel):
}
# Neue Workflow-Modellklassen
class FileItem(BaseModel):
"""Datenmodell für ein Datenobjekt"""
id: int = Field(description="Eindeutige ID des Datenobjekts")
mandate_id: int = Field(description="ID des zugehörigen Mandanten")
user_id: int = Field(description="ID des Erstellers")
name: str = Field(description="Name des Datenobjekts")
mime_type: str = Field(description="Typ des Datenobjekts MIME-Typ")
size: Optional[int] = Field(None, description="Größe des Datenobjekts in Bytes")
file_hash: str = Field(description="Hash code für Deduplizierung")
data: bytes = Field(description="Binärer Inhalt der Datei")
creation_date: Optional[str] = Field(None, description="Datum des Hochladens")
workflow_id: Optional[str] = Field(None, description="ID des zugehörigen Workflows, falls vorhanden")
class DocumentSource(BaseModel):
"""Quelle eines Dokuments im Workflow"""
type: str = Field(description="Typ der Quelle ('agent', 'file', 'clipboard')")
path: Optional[str] = Field(None, description="Speicherpfad (nur für type=='file'")
name: str = Field(description="Anzeigename der Datei")
size: Optional[int] = Field(None, description="Größe in Bytes")
lines: Optional[int] = Field(None, description="Zeilenanzahl (für Textdateien)")
content_type: Optional[str] = Field(None, description="MIME-Typ")
upload_date: Optional[str] = Field(None, description="Uploaddatum")
label: Label = Field(
default=Label(default="Datenobjekt", translations={"en": "Data Object", "fr": "Objet de données"}),
description="Label für die Klasse"
)
# Labels für Attribute
field_labels: Dict[str, Label] = {
"id": Label(default="ID", translations={}),
"mandate_id": Label(default="Mandanten-ID", translations={"en": "Mandate ID", "fr": "ID de mandat"}),
"user_id": Label(default="Benutzer-ID", translations={"en": "User ID", "fr": "ID d'utilisateur"}),
"name": Label(default="Name", translations={"en": "Name", "fr": "Nom"}),
"mime_type": Label(default="Typ", translations={"en": "Type", "fr": "Type"}),
"size": Label(default="Größe", translations={"en": "Size", "fr": "Taille"}),
"file_hash": Label(default="File-Hash", translations={"en": "Hash", "fr": "Hash"}),
"data": Label(default="Daten", translations={"en": "Data", "fr": "Contenu"}),
"creation_date": Label(default="Upload-Datum", translations={"en": "Upload date", "fr": "Date de téléchargement"}),
"workflow_id": Label(default="Workflow-ID", translations={"en": "Workflow ID", "fr": "ID du workflow"})
}
# Workflow-Modellklassen
class DocumentContent(BaseModel):
"""Inhalt eines Dokuments im Workflow"""
label: Optional[str] = Field(None, description="Optionale Bezeichnung")
is_text: Optional[bool] = Field(False, description="Flag, ob Textdatei")
type: str = Field(description="Typ des Inhalts ('pdf', docx, xlsx, txt, csv, json, jpg, png")
text: Optional[str] = Field(None, description="Textinhalt")
sequence_nr: Optional[int] = Field(1,description="Sequenz-Nummer des Inhaltes im Quelldokument")
name: str = Field(description="Optionale Bezeichnung")
ext: str = Field(description="Content extension for export: txt, csv, json, jpg, png")
content_type: str = Field(description="MIME-Typ")
data: bytes = Field(description="Inhalt der Datei")
class Document(BaseModel):
"""Dokument im Workflow """
"""Dokument im Workflow - Referenziert direkt eine Datei in der Datenbank"""
id: str = Field(description="Eindeutige ID des Dokuments")
source: DocumentSource = Field(description="Quellmetadaten")
file_id: int = Field(description="ID der referenzierten Datei in der Datenbank")
contents: List[DocumentContent] = Field(description="Dokumentinhalte")
class DataStats(BaseModel):
@ -110,13 +103,13 @@ class Message(BaseModel):
finished_at: Optional[str] = Field(None, description="Zeitstempel für Nachrichtenabschluss")
sequence_no: int = Field(description="Sequenznummer für Sortierung")
status: str = Field(description="Status der Nachricht ('pending', 'processing', 'completed', 'failed')")
status: str = Field(description="Status der Nachricht ('processing', 'completed')")
role: str = Field(description="Rolle des Absenders ('system', 'user', 'assistant')")
data_stats: Optional[DataStats] = Field(None, description="Statistiken")
documents: Optional[List[Document]] = Field(None, description="Dokumente in dieser Nachricht")
documents: Optional[List[Document]] = Field(None, description="Dokumente in dieser Nachricht (Referenzen zu Dateien in der Datenbank)")
content: Optional[str] = Field(None, description="Textinhalt der Nachricht")
agent_type: Optional[str] = Field(None, description="Typ des verwendeten Agenten")
agent_name: Optional[str] = Field(None, description="Name des verwendeten Agenten")
class Workflow(BaseModel):
"""Workflow-Objekt für Multi-Agent-System"""
@ -127,8 +120,7 @@ class Workflow(BaseModel):
status: str = Field(description="Status des Workflows ('running', 'failed', 'stopped')")
started_at: str = Field(description="Startzeitpunkt")
last_activity: str = Field(description="Zeitpunkt der letzten Aktivität")
current_round: int = Field(description="Aktuelle Runde")
waiting_for_user: bool = Field(False, description="Flag, ob auf Benutzereingabe gewartet wird")
last_message_id: str = Field(description="The last registered message")
data_stats: Optional[Dict[str, Any]] = Field(None, description="Gesamt-Statistiken")
messages: List[Message] = Field(default=[], description="Nachrichtenverlauf")
@ -140,10 +132,9 @@ class WorkflowCreateRequest(BaseModel):
"""Anfrage zur Erstellung eines neuen Workflows"""
name: Optional[str] = Field(None, description="Name des Workflows")
prompt: str = Field(description="Zu verwendender Prompt")
files: List[int] = Field(default=[], description="Liste von Datei-IDs")
files: List[int] = Field(default=[], description="Liste von FileItem ID")
class UserInputRequest(BaseModel):
"""Anfrage für Benutzereingabe an einen laufenden Workflow"""
message: str = Field(description="Nachricht des Benutzers")
additional_files: List[int] = Field(default=[], description="Liste zusätzlicher Datei-IDs")
prompt: str = Field(description="Nachricht des Benutzers")
files: List[int] = Field(default=[], description="Liste zusätzlicher FileItem ID")

View file

@ -1,54 +1,11 @@
....................... TASKS
STEP 1...........................
die agents registry bereinigen inkl agents
Der User liefert im AI Chat eine Anfrage in einem Message Objekt. Dieses beinhaltet seinen Prompt und eine Liste der mitgelieferten Dokumente mit ihnen contents. Ebenfalls verfügbar ist der bisherige Chatverlauf im objekt "workflow".
Wir befinden uns im python Script, wo der User prompt mit dem message objekt "message_user" ankommt.
die file upload & dragdrop bereinigen, dass einfach file in db geschrieben wird mit file im file-object
Kannst Du mir bitte den Prompt für den Projektleiter zusammenstellen, welcher dem User die Antwort liefert. Der Prijektleiter soll dies tun:
Dazu erstellst Du zuerst eine Liste von Resultaten, welche der User benötigt, mit Angabe von Format.
Dann erstellst Du die Antwort an den Benutzer mit den Resultaten. Dokumente lieferst Du separat als Liste. Falls Du für die Antwort oder die Resultate Inputs von Agenten benötigst, gib bitte als Liste an, wer pro Resultat was liefern muss mit Angabe von Agent,Liste der Inputdokumente, Resultatformat,Liste der Resultatnamen, Prompt für Agent als json.
Diese Agenten stehen zur Verfügung:
. Loop: Er führt repetitive Aufgaben aus. Er benötigt eine Liste von Dokumenten und einen Prompt zur Anwendung auf jedes Dokument, und Resultatformat
. Coder: Er führt Pyton Code aus. Benötigt Prompt und Resultatformat.
STEP 2...........................
We have here an ai agents workflow. a big problem is document extraction. i uploaded a pdf file with a picture inside. in the database i see, that the document has 1 contents, "text" with a endline, marked as "is_extracted=True". it is missing the picture inside the pdf.
I would like to have the following implementation for files in a workflow:
How do documents arrive in the workflow:
a) user input with upload or drag&drop: the file shall be stored in the database (files) and its content stored in the workflow message as documents item with reference to the file_id in the database. all contents of the file will be stored as content items in the document item of the message object. according to the content type whey will be extracted as text or as base64 string (e.g. images). the document id will be a uuid and the document-source id the integer from the object in the database "files"
b) produces documents delivered by the agents: exactly the same like a)
the content provided to an agent will now be a document consisting of the content of all previous messages including the extracted content of the documents within the messages. the extracted content of the documents is produced for each content of the document:
- for text: An ai call with the extraction prompt delivers the text to be integrated
- for an image (it is available as base64 content) an ai call with the extraction prompt delivers the text to be integrated
Like this we have not anymore the problem, that file content is not found by the agents.
For code implementation I see a big opportunity to massively reduce code. To build basic methods to be used everywhere:
1. function "document_store_upload(message_id,filename,filepath...) --> function to store an uploaded or drag&drop document from the user and return the document object. This function does the steps for a) respectively b) like described above and identified the filetype
2. function "document_store_agent(message_id,filename,document_content,document_type...) --> function to store the produced document from the agent and return the document object. This function does the steps like described in section a) above
3. function "document_get_from_message()
Based on these 3 functions all operations can be done much more comfortable in the workflow, but also in connection with the ui (download file, copy file, preview file), because all references to the files are always ensured.
STEP 3...........................
All routes: remove error handling details and repeating tasks like user check etc. to pack into auth module, only function calls
Agent task manager to start at the result and organize tasks backwards
funktion für integration von file in message, als basis db-file-id oder document-part-from-agent; damit alle attribute füllen inkl zusammenfassung pro content --> pro extractor-typ ein file
Workflow:
- NO-FILES for the workflow!
@ -74,6 +31,7 @@ frontend to react
PRIO2:
implement cleanup routines for files in lucydom_interface (File_Management_CLEANUP_INTERVAL): temp older than interval, all orphaned
frontend: no labels definition
@ -84,6 +42,158 @@ add connector to myoutlook
----------------------- DONE
annst du bitte den Code Vorschlag von Dir als class "ChatManager" ins modul "chat.py" umbauen und mir diese class liefern. hier zusätzliche infos und dokumente.
für die implementierung der funktionen bitte die beiliegenden module als grundlage verwenden, aber allen code neu erstellen. denn die heutigen codes sind viel zu lange haben zuviele details auf allen levels drin. die implementierung der funktionen soll ebenfalls high-level sein, indem alle detail-ausführungen in grundlagen-funktionen ausgelagert werden.
folgende anhänge dazu:
- lucydom_model und lucydom_interface : datenmodell und interface zum datenmodell (wir arbeiten nur mir dem workflow object)
- workflow.py: die routerdatei, welche die funktionen von lucydom_interface über den gateway nutzt
- agentservice_registry (old): registry der agenten, diese bitte neu und kompakt erstellen als "chat_registry.py"
- agentservice_base (old): template für agents definitionen.
kannst du bitte mit dem datenmodell (es wurde angepasst) folgendes tun:
1. lcuydom_interface.py überarbeiten, damit es mit dem angepassten datenmodell wieder korrekt funktioniert.
2. workflow.py überarbeiten, sodass die immer wieder gleichen funktionen der routes in hilfsfunktionen ausgelagert werden und alle routinen umschreiben, dass sie nicht agentservice_workflow_manager.py" aufrufen, sondern "chat.py". in der router funktion "workflow.py" keine implementierungen, sondern diese in die chat.py funktion übergeben. Die route "submit_user_input" umschreiben, dass workflow_id auch leer sein kann. direkt die funktion "workflow_integrate_userinput" aufrufen.
3. die funktionen implementieren mit diesen hinweisen:
workflow_integrate_userinput:
- den parameter workflow umbenennen in optional workflow_id. dieser kann initial None sein, wenn ein neuer workflow startet. daher zuerst die zu implementierende funktion workflow_init(workflow_id) aufrufen, welche das workflow object zurückgibt.
- generell werden 2 kommunikationen geführt:
- a) "log_add" (umbenennen von "send_message_to_user") sendet einen log-eintrag, implementiert in mit der implementierung in "lucydom_interface.create_workflow_log" und gleichzeitig einen "Info" Eintrag im logger erstellen
- b) "message_add" speichert eine message im workflow objekt. Implementierung über lucydom_interface
- Vor Step 1. die message_user im workflow als neue message speichern
- Anstatt "# Send initial response" die "user_response" als message object im workflow speichern und auch gleich den obj_answer und obj_workplan in den logger schreiben mittels einer hilfsfunktion "json2text(), welche das json-Objekt als Strukturobjekt lesbar schreibgeschützt
- send_message_to_user(step_info), dies als log_add schreiben
- format_final_response umbenennen in format_final_message und damit das finale message objekt mit den documents erstellen, dieses dann mit messagE_add dem workflow zufügen
- update_workflow(...) nicht mehr nötig, dafür workflow_finish
prompt_project_manager:
- mach nur einen typ "doc_type" und gib dafür eine abschliessende liste von optionen an, welche aus der funktion get_available_document_types() kommen
- der obj_workplan soll pro listenelement doc_input und doc_output ein Dict haben mit den Elementen "label","doc_type". auch hier die abschliessende liste der möglichen werte angeben, welche aus der funktion get_available_document_types() kommt.
workflow_init:
- wenn die workflow_id leer ist oder nicht existiert, wird ein neuer workflow erzeugt, andernfalls wird der bestehende workflow geladen
- die statuswerte werden gesetzt: status="running", started_at, last_activity=strated_at
workflow_finish:
- die statuswerte werden gesetzt: status="stopped", last_activity
message_add:
- die message dem workflow ergänzen
- die statuswerte werden gesetzt: last_activity, last_message_id
get_available_agents:
- die function aus der agents_registry aufrufen
get_available_document_types:
- liste dieser doc-types ausgeben: text, csv, png, html
summarize_workflow(workflow,prompt):
- in der chronologie der messages von aktuell zu historisch pro message mit der funktion summarize_message(prompt) die zusammenfassung holen. Die zusammenfasusng ausgeben mit agent-name, generierte zusammenfassung, liste der dokumente mit jeweils ihrer zusammenfassung
summarize_message(prompt):
- mit ai call die zusammenfassung der message mit dem prompt generieren. Die zusammenfasusng ausgeben mit agent-name, generierte zusammenfassung des contents, liste der dokumente mit jeweils ihrer zusammenfassung
summarize_user_documents:
- pro document mit dem angegebenen prompt den content zusammenfassen und die liste ausgeben mit [document.content: text]
call_agent: braucht es nicht, ai calls können direkt über den connector erfolgen, welcher initial eingebunden wird: "from connectors.connector_aichat_openai import ChatService"
Kannst Du mir die python funktion erstellen, um nachfolgendes zu tun. Ich möchte eine kompakte Funktion, welche keine Details enthält, ausser den Prompt-Teil bis und mit Antwort an den user. Alle nötigen Datenkonversionen und Details bitte in Hilfs-funktionen auslagern. Diese müssen nicht implementiert sein, sondern nur deren input und output definieren.
# Kontext
Der User liefert im AI Chat eine Anfrage in einem Message Objekt. Dieses beinhaltet seinen Prompt und eine Liste der mitgelieferten Dokumente mit ihnen contents im "message" objekt. Ebenfalls verfügbar ist der bisherige Chatverlauf im objekt "workflow".
Wir befinden uns in der python funktion "workflow_integrate_userinput", wo der User prompt ankommt, also diese 2 parameter: "message_user" und "workflow".
Es steht eine Liste von agents zur Verfügung. Das agents in der Art:
- Loop: Er führt repetitive Aufgaben aus. Er benötigt eine Liste von Dokumenten und einen Prompt zur Anwendung auf jedes Dokument, er liefert eine liste von "content"
. Coder: Er führt Pyton Code aus. Benötigt als Input einen Prompt, content und die spezifikation des resultatformates.
(weitere...)
# Auftrag
Kannst Du mir bitte den Prompt für den Projektleiter zusammenstellen, welcher dem User die Antwort liefert.
Dies soll er tun:
1. Eine Liste von Resultaten, welche der User für seine Antwort benötigt, als json-Objekt "obj_answer" liefern. Die Antworten des Projektleiters sollen strikt in einem vorgegebenen json-format geliefert werden.
2. Antwort des Vorgehens an den Benutzer mit den Resultat-Dokumenten als Liste senden
3. Falls für die Antwort oder die Resultate Inputs von Agenten (diese sind gemäss "obj_agents" mit ihren eigenschaften definiert) benötigt werden, diese als json Liste (ich nenne sie "obj_workplan") angeben, welcher agent welches resultat liefern soll
Dann soll der Code dies machen:
4. die agenten gemäss obj_workplan ausführen lassen und den user über jeden schritt informieren. die gelieferten dokumente als liste sammeln "obj_results". Jeden Agenten mit den Datenobjekten gemäss seiner Datenstruktur bedienen.
Dann anhand der gelieferten Dokumente die finale Antwort an den Benutzer senden. Dokumente vom Typ "text" direkt in die Antwort an den Benutzer integrieren. Die Dokumente referenzieren.
Dann im Code:
5. Dem benutzer die antwort mit den dokumenten senden
Jedes Dokument soll anhand des Labels eindeutig identifizierbar sein. Du hast alle Dokument-conteot-labels im workflow objekt.
Diese Objektinformationen dazu:
- datenmodell für workflow inklusive message:
- workflow
- messages: list of message
- message
- agent (who created message)
- input (the input prompt)
- content (text)
- documents: list of document
- document
- source
- contents: list of content
- content
- label
- format: formatType
- data: the data of the content in the format according to formatType
- formatType: [text, csv, jpg, gif, png]
- obj_answer: json-Liste mit diesen Attributen:
- label: document label (unique name in the documents list)
- doc_type_src: document type des zu liefernden dokumentes: [text, csv, png, html]
- doc_type_final: document type des dokumentes an den Benutzer: [text, csv, jpg, gif, png, pdf, html, docx, xlsx]
- summary: summary of required document content
- obj_workplan: json-Liste mit diesen Attributen:
- agent: agent identifier based on the given agent list with the skills of the agents
- doc_output: List of label,doc_type_src (documents to deliver)
- prompt: Prompt to use for answer delivery and document-content-extraction
- doc_input: List of label,doc_type_src (documents to read with prompt)
- obj_agents: Pro Agent sind diese Informationen verfügbar:
- name: Sein Name, um die entsprechende Funktion aufzurufen
- skills: Was dieser Agent macht
- input: datenformat, in welchem der agent die informationen benötigt
- obj_result: List of documents with label, format, data
Es soll durchgängig mit dem content objekt gearbeitet werden, wenn content übergeben wird.
backend: all object actions in interfaces generic for the objects in models for CRU-methods

View file

@ -32,6 +32,8 @@ Das Projekt besteht aus zwei Hauptkomponenten:
- `requirements.txt` - Python-Abhängigkeiten
### Backend-Installation Lokal
0. lokal dev: (anaconda environment) conda activate poweron
1. Virtuelle Umgebung erstellen und aktivieren:
```bash
python -m venv venv

View file

@ -3,7 +3,7 @@ fastapi==0.104.1
uvicorn==0.23.2
python-multipart==0.0.6
httpx==0.25.0
pydantic==2.4.2
pydantic==1.10.13 # Ältere Version ohne Rust-Abhängigkeit
## Authentication & Security
python-jose==3.3.0
@ -18,12 +18,13 @@ fitz
PyPDF2==3.0.1
## Data Processing & Analysis
pandas==2.2.3
numpy
scikit-learn==1.4.0
numpy==1.26.3 # Version die mit pandas und matplotlib kompatibel ist
pandas==2.2.3 # Aktuelle Version beibehalten
FuzzyTM>=0.4.0
## Data Visualization
matplotlib==3.8.0
matplotlib==3.8.0 # Aktuelle Version beibehalten
seaborn==0.13.0
plotly==5.18.0
@ -33,4 +34,4 @@ requests==2.31.0
## Utilities
python-dateutil==2.8.2
python-dotenv==1.0.0
python-dotenv==1.0.0

View file

@ -1,10 +1,13 @@
from fastapi import APIRouter, HTTPException, Depends, File, UploadFile, Form, Path, Request, status, Query, Response
from fastapi.responses import FileResponse, JSONResponse
from fastapi.responses import JSONResponse
from typing import List, Dict, Any, Optional
import logging
from datetime import datetime
from dataclasses import dataclass
import io
from modules.auth import get_current_active_user, get_user_context
from modules.utility import APP_CONFIG
# Import interfaces
from modules.lucydom_interface import get_lucydom_interface, FileError, FileNotFoundError, FileStorageError, FilePermissionError, FileDeletionError
@ -27,6 +30,32 @@ def get_model_attributes(model_class):
# Modell-Attribute für FileItem
file_attributes = get_model_attributes(FileItem)
@dataclass
class AppContext:
"""Kontext-Objekt für alle benötigten Verbindungen und Benutzerinformationen"""
mandate_id: int
user_id: int
interface_data: Any # LucyDOM Interface
async def get_context(current_user: Dict[str, Any]) -> AppContext:
"""
Erstellt ein zentrales Kontext-Objekt mit allen benötigten Interfaces
Args:
current_user: Aktueller Benutzer aus der Authentifizierung
Returns:
AppContext-Objekt mit allen benötigten Verbindungen
"""
mandate_id, user_id = await get_user_context(current_user)
interface_data = get_lucydom_interface(mandate_id, user_id)
return AppContext(
mandate_id=mandate_id,
user_id=user_id,
interface_data=interface_data
)
# Router für Datei-Endpunkte erstellen
router = APIRouter(
prefix="/api/files",
@ -44,13 +73,10 @@ router = APIRouter(
async def get_files(current_user: Dict[str, Any] = Depends(get_current_active_user)):
"""Alle verfügbaren Dateien abrufen"""
try:
mandate_id, user_id = await get_user_context(current_user)
# LucyDOM-Interface mit Benutzerkontext initialisieren
lucy_interface = get_lucydom_interface(mandate_id, user_id)
context = await get_context(current_user)
# Alle Dateien generisch abrufen
files = lucy_interface.get_all_files()
files = context.interface_data.get_all_files()
return files
except Exception as e:
logger.error(f"Fehler beim Abrufen der Dateien: {str(e)}")
@ -67,33 +93,29 @@ async def upload_file(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""
Upload einer Datei für Workflows oder allgemeine Nutzung.
Upload einer Datei
"""
try:
# Kontext-Informationen extrahieren
mandate_id, user_id = await get_user_context(current_user)
# LucyDOM-Interface mit Kontext holen
lucydom = get_lucydom_interface(mandate_id, user_id)
context = await get_context(current_user)
# Datei einlesen
file_content = await file.read()
# Größenbeschränkung prüfen (z.B. 50MB)
max_size = 50 * 1024 * 1024 # 50MB in Bytes
# Größenbeschränkung prüfen
max_size = int(APP_CONFIG.get("File_Management_MAX_UPLOAD_SIZE_MB")) * 1024 * 1024 # in Bytes
if len(file_content) > max_size:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"Datei zu groß. Maximale Größe: 50MB"
detail=f"Datei zu groß. Maximale Größe: {APP_CONFIG.get("File_Management_MAX_UPLOAD_SIZE_MB")}MB"
)
# Datei über das LucyDOM-Interface speichern
file_meta = lucydom.save_uploaded_file(file_content, file.filename)
# Datei über das LucyDOM-Interface in der Datenbank speichern
file_meta = context.interface_data.save_uploaded_file(file_content, file.filename)
# Wenn workflow_id angegeben, aktualisiere die Dateiinformationen
if workflow_id:
update_data = {"workflow_id": workflow_id}
lucydom.update_file(file_meta["id"], update_data)
context.interface_data.update_file(file_meta["id"], update_data)
file_meta["workflow_id"] = workflow_id
# Erfolgreiche Antwort
@ -119,36 +141,23 @@ async def get_file(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""
Gibt eine Datei anhand ihrer ID zurück.
Gibt eine Datei anhand ihrer ID direkt aus der Datenbank zurück.
"""
try:
# Kontext-Informationen extrahieren
mandate_id, user_id = await get_user_context(current_user)
context = await get_context(current_user)
# LucyDOM-Interface mit Kontext holen
lucydom = get_lucydom_interface(mandate_id, user_id)
# Datei über das LucyDOM-Interface abrufen
file_data = lucydom.download_file(file_id)
# Datei über das LucyDOM-Interface aus der Datenbank abrufen
file_data = context.interface_data.download_file(file_id)
# Datei zurückgeben
if "path" in file_data and file_data["path"]:
# FileResponse verwenden, wenn ein Pfad vorhanden ist (effizienteres Streaming)
return FileResponse(
path=file_data["path"],
media_type=file_data["content_type"],
filename=file_data["name"]
)
else:
# Response mit Binärdaten, wenn kein Pfad vorhanden ist
headers = {
"Content-Disposition": f'attachment; filename="{file_data["name"]}"'
}
return Response(
content=file_data["content"],
media_type=file_data["content_type"],
headers=headers
)
headers = {
"Content-Disposition": f'attachment; filename="{file_data["name"]}"'
}
return Response(
content=file_data["content"],
media_type=file_data["content_type"],
headers=headers
)
except FileNotFoundError as e:
logger.warning(f"Datei nicht gefunden: {str(e)}")
@ -182,17 +191,13 @@ async def delete_file(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""
Löscht eine Datei anhand ihrer ID.
Löscht eine Datei anhand ihrer ID aus der Datenbank.
"""
try:
# Kontext-Informationen extrahieren
mandate_id, user_id = await get_user_context(current_user)
# LucyDOM-Interface mit Kontext holen
lucydom = get_lucydom_interface(mandate_id, user_id)
context = await get_context(current_user)
# Datei über das LucyDOM-Interface löschen
lucydom.delete_file(file_id)
context.interface_data.delete_file(file_id)
# Erfolgreiche Löschung ohne Inhalt zurückgeben (204 No Content)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@ -222,80 +227,6 @@ async def delete_file(
detail=f"Fehler beim Löschen der Datei: {str(e)}"
)
@router.get("/cleanup/orphaned", response_model=Dict[str, Any])
async def cleanup_orphaned_files(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""
Bereinigt verwaiste Dateien, die physisch existieren aber keine Einträge in der Datenbank haben.
Nur für Administratoren.
"""
try:
# Kontext-Informationen extrahieren
mandate_id, user_id = await get_user_context(current_user)
# Prüfen, ob der Benutzer Admin-Rechte hat
# TODO: Implementieren einer richtigen Admin-Rechteverwaltung
if user_id != 1: # Temporäre einfache Lösung
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Nur Administratoren können diese Funktion ausführen"
)
# LucyDOM-Interface mit Kontext holen
lucydom = get_lucydom_interface(mandate_id, user_id)
# Verwaiste Dateien bereinigen
lucydom.cleanup_orphaned_files()
# Temporäre Dateien bereinigen
lucydom.cleanup_temp_files()
return {"status": "success", "message": "Bereinigung abgeschlossen"}
except Exception as e:
logger.error(f"Fehler bei der Datei-Bereinigung: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Fehler bei der Datei-Bereinigung: {str(e)}"
)
@router.get("/temp/cleanup", response_model=Dict[str, Any])
async def cleanup_temp_files(
max_age_hours: int = Query(24, ge=1, le=168),
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""
Bereinigt temporäre Dateien, die älter als die angegebene Zeit sind.
Args:
max_age_hours: Maximales Alter der temporären Dateien in Stunden (1-168)
"""
try:
# Kontext-Informationen extrahieren
mandate_id, user_id = await get_user_context(current_user)
# LucyDOM-Interface mit Kontext holen
lucydom = get_lucydom_interface(mandate_id, user_id)
# Temporäre Dateien bereinigen
lucydom.cleanup_temp_files(max_age_hours)
return {
"status": "success",
"message": f"Temporäre Dateien älter als {max_age_hours} Stunden wurden bereinigt"
}
except Exception as e:
logger.error(f"Fehler bei der Bereinigung temporärer Dateien: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Fehler bei der Bereinigung temporärer Dateien: {str(e)}"
)
@router.get("/stats", response_model=Dict[str, Any])
async def get_file_stats(
current_user: Dict[str, Any] = Depends(get_current_active_user)
@ -304,14 +235,10 @@ async def get_file_stats(
Gibt Statistiken über die gespeicherten Dateien zurück.
"""
try:
# Kontext-Informationen extrahieren
mandate_id, user_id = await get_user_context(current_user)
# LucyDOM-Interface mit Kontext holen
lucydom = get_lucydom_interface(mandate_id, user_id)
context = await get_context(current_user)
# Alle Dateien abrufen
all_files = lucydom.get_all_files()
all_files = context.interface_data.get_all_files()
# Statistiken berechnen
total_files = len(all_files)
@ -320,7 +247,7 @@ async def get_file_stats(
# Nach Dateityp gruppieren
file_types = {}
for file in all_files:
file_type = file.get("type", "unknown")
file_type = file.get("mime_type", "unknown").split("/")[0]
if file_type not in file_types:
file_types[file_type] = 0
file_types[file_type] += 1

View file

@ -2,10 +2,10 @@ from fastapi import APIRouter, HTTPException, Depends, Body, Path
from typing import List, Dict, Any
from fastapi import status
from datetime import datetime
from modules.auth import get_current_active_user, get_user_context
from dataclasses import dataclass
# Import interfaces
from modules.auth import get_current_active_user, get_user_context
from modules.gateway_interface import get_gateway_interface
from modules.gateway_model import Mandate
@ -23,6 +23,32 @@ def get_model_attributes(model_class):
# Modell-Attribute für Mandate
mandate_attributes = get_model_attributes(Mandate)
@dataclass
class AppContext:
"""Kontext-Objekt für alle benötigten Verbindungen und Benutzerinformationen"""
mandate_id: int
user_id: int
interface_data: Any # Gateway Interface
async def get_context(current_user: Dict[str, Any]) -> AppContext:
"""
Erstellt ein zentrales Kontext-Objekt mit allen benötigten Interfaces
Args:
current_user: Aktueller Benutzer aus der Authentifizierung
Returns:
AppContext-Objekt mit allen benötigten Verbindungen
"""
mandate_id, user_id = await get_user_context(current_user)
interface_data = get_gateway_interface(mandate_id, user_id)
return AppContext(
mandate_id=mandate_id,
user_id=user_id,
interface_data=interface_data
)
# Router für Mandanten-Endpunkte erstellen
router = APIRouter(
prefix="/api/mandates",
@ -33,10 +59,7 @@ router = APIRouter(
@router.get("", response_model=List[Dict[str, Any]])
async def get_mandates(current_user: Dict[str, Any] = Depends(get_current_active_user)):
"""Alle verfügbaren Mandanten abrufen (nur für SysAdmin-Benutzer)"""
mandate_id, user_id = await get_user_context(current_user)
# Gateway-Interface mit Benutzerkontext initialisieren
gateway = get_gateway_interface(mandate_id, user_id)
context = await get_context(current_user)
# Berechtigungsprüfung
if current_user.get("privilege") != "sysadmin":
@ -46,7 +69,7 @@ async def get_mandates(current_user: Dict[str, Any] = Depends(get_current_active
)
# Mandanten generisch abrufen
return gateway.get_all_mandates()
return context.interface_data.get_all_mandates()
@router.post("", response_model=Dict[str, Any])
@ -55,10 +78,7 @@ async def create_mandate(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""Einen neuen Mandanten erstellen (nur für SysAdmin-Benutzer)"""
mandate_id, user_id = await get_user_context(current_user)
# Gateway-Interface mit Benutzerkontext initialisieren
gateway = get_gateway_interface(mandate_id, user_id)
context = await get_context(current_user)
# Berechtigungsprüfung
if current_user.get("privilege") != "sysadmin":
@ -80,7 +100,7 @@ async def create_mandate(
mandate_data["language"] = "de"
# Mandant erstellen
new_mandate = gateway.create_mandate(**mandate_data)
new_mandate = context.interface_data.create_mandate(**mandate_data)
return new_mandate
@ -91,16 +111,13 @@ async def get_mandate(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""Einen bestimmten Mandanten abrufen"""
user_mandate_id, user_id = await get_user_context(current_user)
# Gateway-Interface mit Benutzerkontext initialisieren
gateway = get_gateway_interface(user_mandate_id, user_id)
context = await get_context(current_user)
# Berechtigungsprüfung
# Admin darf nur seinen eigenen Mandanten sehen, SysAdmin alle
is_admin = current_user.get("privilege") == "admin"
is_sysadmin = current_user.get("privilege") == "sysadmin"
is_own_mandate = user_mandate_id == mandate_id
is_own_mandate = context.mandate_id == mandate_id
if (is_admin and not is_own_mandate) and not is_sysadmin:
raise HTTPException(
@ -109,7 +126,7 @@ async def get_mandate(
)
# Mandant generisch abrufen
mandate = gateway.get_mandate(mandate_id)
mandate = context.interface_data.get_mandate(mandate_id)
if not mandate:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@ -125,13 +142,10 @@ async def update_mandate(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""Einen bestehenden Mandanten aktualisieren"""
user_mandate_id, user_id = await get_user_context(current_user)
# Gateway-Interface mit Benutzerkontext initialisieren
gateway = get_gateway_interface(user_mandate_id, user_id)
context = await get_context(current_user)
# Mandant existiert?
mandate = gateway.get_mandate(mandate_id)
mandate = context.interface_data.get_mandate(mandate_id)
if not mandate:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@ -141,7 +155,7 @@ async def update_mandate(
# Berechtigungsprüfung
is_admin = current_user.get("privilege") == "admin"
is_sysadmin = current_user.get("privilege") == "sysadmin"
is_own_mandate = user_mandate_id == mandate_id
is_own_mandate = context.mandate_id == mandate_id
if (is_admin and not is_own_mandate) and not is_sysadmin:
raise HTTPException(
@ -156,7 +170,7 @@ async def update_mandate(
update_data[attr] = mandate_data[attr]
# Mandant aktualisieren
updated_mandate = gateway.update_mandate(
updated_mandate = context.interface_data.update_mandate(
mandate_id=mandate_id,
mandate_data=update_data
)
@ -169,13 +183,10 @@ async def delete_mandate(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""Einen Mandanten löschen, inklusive aller zugehörigen Benutzer und referenzierten Objekte"""
user_mandate_id, user_id = await get_user_context(current_user)
# Gateway-Interface mit Benutzerkontext initialisieren
gateway = get_gateway_interface(user_mandate_id, user_id)
context = await get_context(current_user)
# Mandant existiert?
mandate = gateway.get_mandate(mandate_id)
mandate = context.interface_data.get_mandate(mandate_id)
if not mandate:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@ -185,7 +196,7 @@ async def delete_mandate(
# Berechtigungsprüfung
is_admin = current_user.get("privilege") == "admin"
is_sysadmin = current_user.get("privilege") == "sysadmin"
is_own_mandate = user_mandate_id == mandate_id
is_own_mandate = context.mandate_id == mandate_id
if (is_admin and not is_own_mandate) and not is_sysadmin:
raise HTTPException(
@ -194,7 +205,7 @@ async def delete_mandate(
)
# Mandant löschen
success = gateway.delete_mandate(mandate_id)
success = context.interface_data.delete_mandate(mandate_id)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,

View file

@ -2,6 +2,7 @@ from fastapi import APIRouter, HTTPException, Depends, Body, Query, Path
from typing import List, Dict, Any, Optional
from fastapi import status
from datetime import datetime
from dataclasses import dataclass
# Import auth module
from modules.auth import get_current_active_user, get_user_context
@ -24,6 +25,32 @@ def get_model_attributes(model_class):
# Modell-Attribute für Prompt
prompt_attributes = get_model_attributes(Prompt)
@dataclass
class AppContext:
"""Kontext-Objekt für alle benötigten Verbindungen und Benutzerinformationen"""
mandate_id: int
user_id: int
interface_data: Any # LucyDOM Interface
async def get_context(current_user: Dict[str, Any]) -> AppContext:
"""
Erstellt ein zentrales Kontext-Objekt mit allen benötigten Interfaces
Args:
current_user: Aktueller Benutzer aus der Authentifizierung
Returns:
AppContext-Objekt mit allen benötigten Verbindungen
"""
mandate_id, user_id = await get_user_context(current_user)
interface_data = get_lucydom_interface(mandate_id, user_id)
return AppContext(
mandate_id=mandate_id,
user_id=user_id,
interface_data=interface_data
)
# Router für Prompt-Endpunkte erstellen
router = APIRouter(
prefix="/api/prompts",
@ -36,13 +63,10 @@ async def get_prompts(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""Alle Prompts abrufen"""
mandate_id, user_id = await get_user_context(current_user)
# LucyDOM-Interface mit Benutzerkontext initialisieren
lucy_interface = get_lucydom_interface(mandate_id, user_id)
context = await get_context(current_user)
# Prompts generisch abrufen
return lucy_interface.get_all_prompts()
return context.interface_data.get_all_prompts()
@router.post("", response_model=Dict[str, Any])
@ -51,10 +75,7 @@ async def create_prompt(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""Einen neuen Prompt erstellen"""
mandate_id, user_id = await get_user_context(current_user)
# LucyDOM-Interface mit Benutzerkontext initialisieren
lucy_interface = get_lucydom_interface(mandate_id, user_id)
context = await get_context(current_user)
# Attribute aus dem Request dynamisch setzen
prompt_data = {}
@ -67,7 +88,7 @@ async def create_prompt(
name = prompt.get("name", "Neuer Prompt")
# Prompt erstellen
new_prompt = lucy_interface.create_prompt(
new_prompt = context.interface_data.create_prompt(
content=content,
name=name
)
@ -85,13 +106,10 @@ async def get_prompt(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""Einen bestimmten Prompt abrufen"""
mandate_id, user_id = await get_user_context(current_user)
# LucyDOM-Interface mit Benutzerkontext initialisieren
lucy_interface = get_lucydom_interface(mandate_id, user_id)
context = await get_context(current_user)
# Prompt generisch abrufen
prompt = lucy_interface.get_prompt(prompt_id)
prompt = context.interface_data.get_prompt(prompt_id)
if not prompt:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@ -108,13 +126,10 @@ async def update_prompt(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""Einen bestehenden Prompt aktualisieren"""
mandate_id, user_id = await get_user_context(current_user)
# LucyDOM-Interface mit Benutzerkontext initialisieren
lucy_interface = get_lucydom_interface(mandate_id, user_id)
context = await get_context(current_user)
# Prüfe, ob der Prompt existiert
existing_prompt = lucy_interface.get_prompt(prompt_id)
existing_prompt = context.interface_data.get_prompt(prompt_id)
if not existing_prompt:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@ -132,7 +147,7 @@ async def update_prompt(
name = prompt_data.get("name")
# Prompt aktualisieren
updated_prompt = lucy_interface.update_prompt(
updated_prompt = context.interface_data.update_prompt(
prompt_id=prompt_id,
content=content,
name=name
@ -153,20 +168,17 @@ async def delete_prompt(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""Einen Prompt löschen"""
mandate_id, user_id = await get_user_context(current_user)
# LucyDOM-Interface mit Benutzerkontext initialisieren
lucy_interface = get_lucydom_interface(mandate_id, user_id)
context = await get_context(current_user)
# Prüfe, ob der Prompt existiert
existing_prompt = lucy_interface.get_prompt(prompt_id)
existing_prompt = context.interface_data.get_prompt(prompt_id)
if not existing_prompt:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Prompt mit ID {prompt_id} nicht gefunden"
)
success = lucy_interface.delete_prompt(prompt_id)
success = context.interface_data.delete_prompt(prompt_id)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,

View file

@ -2,6 +2,7 @@ from fastapi import APIRouter, HTTPException, Depends, Body, Path
from typing import List, Dict, Any, Optional
from fastapi import status
from datetime import datetime
from dataclasses import dataclass
# Import auth module
from modules.auth import get_current_active_user, get_user_context
@ -24,6 +25,32 @@ def get_model_attributes(model_class):
# Modell-Attribute für User
user_attributes = get_model_attributes(User)
@dataclass
class AppContext:
"""Kontext-Objekt für alle benötigten Verbindungen und Benutzerinformationen"""
mandate_id: int
user_id: int
interface_data: Any # Gateway Interface
async def get_context(current_user: Dict[str, Any]) -> AppContext:
"""
Erstellt ein zentrales Kontext-Objekt mit allen benötigten Interfaces
Args:
current_user: Aktueller Benutzer aus der Authentifizierung
Returns:
AppContext-Objekt mit allen benötigten Verbindungen
"""
mandate_id, user_id = await get_user_context(current_user)
interface_data = get_gateway_interface(mandate_id, user_id)
return AppContext(
mandate_id=mandate_id,
user_id=user_id,
interface_data=interface_data
)
# Router für Benutzer-Endpunkte erstellen
router = APIRouter(
prefix="/api/users",
@ -34,10 +61,7 @@ router = APIRouter(
@router.get("", response_model=List[Dict[str, Any]])
async def get_users(current_user: Dict[str, Any] = Depends(get_current_active_user)):
"""Alle verfügbaren Benutzer abrufen (nur für Admin/SysAdmin-Benutzer)"""
mandate_id, user_id = await get_user_context(current_user)
# Gateway-Interface mit Benutzerkontext initialisieren
gateway = get_gateway_interface(mandate_id, user_id)
context = await get_context(current_user)
# Berechtigungsprüfung
if current_user.get("privilege") not in ["admin", "sysadmin"]:
@ -48,9 +72,9 @@ async def get_users(current_user: Dict[str, Any] = Depends(get_current_active_us
# Admin sieht nur Benutzer des eigenen Mandanten, SysAdmin sieht alle
if current_user.get("privilege") == "admin":
return gateway.get_users_by_mandate(mandate_id)
return context.interface_data.get_users_by_mandate(context.mandate_id)
else: # sysadmin
return gateway.get_all_users()
return context.interface_data.get_all_users()
@router.post("/register", response_model=Dict[str, Any])
async def register_user(user_data: dict = Body(...)):
@ -100,24 +124,22 @@ async def get_user(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""Einen bestimmten Benutzer abrufen"""
mandate_id, current_user_id = await get_user_context(current_user)
context = await get_context(current_user)
# Gateway-Interface mit Benutzerkontext initialisieren
gateway = get_gateway_interface(mandate_id, current_user_id)
# Berechtigungsprüfung
# Benutzer darf nur sich selbst abrufen, Admin nur Benutzer des eigenen Mandanten, SysAdmin alle
user_to_get = gateway.get_user(user_id)
user_to_get = context.interface_data.get_user(user_id)
if not user_to_get:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Benutzer mit ID {user_id} nicht gefunden"
)
if user_id == current_user_id:
# Berechtigungsprüfung
# Benutzer darf nur sich selbst abrufen, Admin nur Benutzer des eigenen Mandanten, SysAdmin alle
if user_id == context.user_id:
# Benutzer darf sich selbst abrufen
pass
elif current_user.get("privilege") == "admin" and user_to_get.get("mandate_id") == mandate_id:
elif current_user.get("privilege") == "admin" and user_to_get.get("mandate_id") == context.mandate_id:
# Admin darf Benutzer des eigenen Mandanten abrufen
pass
elif current_user.get("privilege") == "sysadmin":
@ -138,13 +160,10 @@ async def update_user(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""Einen bestehenden Benutzer aktualisieren"""
mandate_id, current_user_id = await get_user_context(current_user)
# Gateway-Interface mit Benutzerkontext initialisieren
gateway = get_gateway_interface(mandate_id, current_user_id)
context = await get_context(current_user)
# Benutzer existiert?
user_to_update = gateway.get_user(user_id)
user_to_update = context.interface_data.get_user(user_id)
if not user_to_update:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@ -152,10 +171,10 @@ async def update_user(
)
# Berechtigungsprüfung
is_self_update = user_id == current_user_id
is_self_update = user_id == context.user_id
is_admin = current_user.get("privilege") == "admin"
is_sysadmin = current_user.get("privilege") == "sysadmin"
same_mandate = user_to_update.get("mandate_id") == mandate_id
same_mandate = user_to_update.get("mandate_id") == context.mandate_id
# Filtere erlaubte Felder je nach Berechtigungsstufe
allowed_fields = {"username", "email", "full_name", "language"}
@ -194,7 +213,7 @@ async def update_user(
update_data = {k: v for k, v in update_data.items() if k in allowed_fields}
# User-Daten aktualisieren
updated_user = gateway.update_user(user_id, update_data)
updated_user = context.interface_data.update_user(user_id, update_data)
return updated_user
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
@ -203,13 +222,10 @@ async def delete_user(
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""Einen Benutzer löschen"""
mandate_id, current_user_id = await get_user_context(current_user)
# Gateway-Interface mit Benutzerkontext initialisieren
gateway = get_gateway_interface(mandate_id, current_user_id)
context = await get_context(current_user)
# Benutzer existiert?
user_to_delete = gateway.get_user(user_id)
user_to_delete = context.interface_data.get_user(user_id)
if not user_to_delete:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@ -217,10 +233,10 @@ async def delete_user(
)
# Berechtigungsprüfung
is_self_delete = user_id == current_user_id
is_self_delete = user_id == context.user_id
is_admin = current_user.get("privilege") == "admin"
is_sysadmin = current_user.get("privilege") == "sysadmin"
same_mandate = user_to_delete.get("mandate_id") == mandate_id
same_mandate = user_to_delete.get("mandate_id") == context.mandate_id
if is_self_delete:
# Benutzer darf sich selbst löschen
@ -238,7 +254,7 @@ async def delete_user(
)
# Benutzer und alle referenzierten Objekte löschen
success = gateway.delete_user(user_id)
success = context.interface_data.delete_user(user_id)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,

File diff suppressed because it is too large Load diff

227
test_gateway.py Normal file
View file

@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""
Testskript zum Erstellen eines Workflows mit Prompt und Datei über den Gateway.
"""
import requests
import json
import os
import time
import sys
from datetime import datetime
# Konfiguration
API_BASE_URL = "http://localhost:8000" # Anpassen an deine Gateway-URL
API_TOKEN = "your_api_token_here" # Dein API-Token
# Headers für Authentifizierung
HEADERS = {
"Authorization": f"Bearer {API_TOKEN}",
"Content-Type": "application/json"
}
def log_message(message):
"""Gibt eine formatierte Nachricht mit Zeitstempel aus"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] {message}")
def upload_file(file_path):
"""Lädt eine Datei hoch und gibt die Datei-ID zurück"""
log_message(f"Lade Datei hoch: {file_path}")
if not os.path.exists(file_path):
log_message(f"FEHLER: Datei nicht gefunden: {file_path}")
return None
# Multipart-Formular für Datei-Upload vorbereiten
file_name = os.path.basename(file_path)
files = {
'file': (file_name, open(file_path, 'rb'), 'application/octet-stream')
}
# Datei hochladen
upload_url = f"{API_BASE_URL}/api/files/upload"
response = requests.post(
upload_url,
headers={"Authorization": f"Bearer {API_TOKEN}"}, # Nur Authorization-Header
files=files
)
if response.status_code != 200:
log_message(f"FEHLER: Datei-Upload fehlgeschlagen. Status: {response.status_code}")
log_message(f"Response: {response.text}")
return None
# Datei-ID extrahieren
file_data = response.json()
file_id = file_data.get("id")
log_message(f"Datei erfolgreich hochgeladen. ID: {file_id}")
return file_id
def create_workflow(prompt, file_id=None):
"""Erstellt einen neuen Workflow mit Prompt und optionaler Datei"""
log_message("Erstelle neuen Workflow...")
# Nachricht für den Workflow vorbereiten
user_input = {
"message": prompt
}
# Wenn eine Datei-ID vorhanden ist, füge sie hinzu
if file_id:
user_input["additional_files"] = [file_id]
# Workflow erstellen
workflow_url = f"{API_BASE_URL}/api/workflows/user-input"
response = requests.post(
workflow_url,
headers=HEADERS,
json=user_input
)
if response.status_code >= 400:
log_message(f"FEHLER: Workflow-Erstellung fehlgeschlagen. Status: {response.status_code}")
log_message(f"Response: {response.text}")
return None
# Workflow-ID extrahieren
workflow_data = response.json()
workflow_id = workflow_data.get("workflow_id")
log_message(f"Workflow erfolgreich erstellt. ID: {workflow_id}")
return workflow_id
def poll_workflow_status(workflow_id, max_attempts=20, delay=2):
"""Fragt den Status eines Workflows ab und wartet bis zur Fertigstellung"""
log_message(f"Prüfe Status des Workflows {workflow_id}...")
for attempt in range(1, max_attempts + 1):
status_url = f"{API_BASE_URL}/api/workflows/{workflow_id}/status"
response = requests.get(
status_url,
headers=HEADERS
)
if response.status_code != 200:
log_message(f"FEHLER: Status-Abfrage fehlgeschlagen. Status: {response.status_code}")
continue
status_data = response.json()
current_status = status_data.get("status")
log_message(f"Workflow-Status: {current_status} (Versuch {attempt}/{max_attempts})")
if current_status in ["completed", "stopped", "failed"]:
log_message(f"Workflow ist abgeschlossen mit Status: {current_status}")
return status_data
time.sleep(delay)
log_message(f"Maximale Anzahl von Versuchen erreicht. Letzter Status: {current_status}")
return None
def get_workflow_messages(workflow_id):
"""Ruft alle Nachrichten eines Workflows ab"""
log_message(f"Hole Nachrichten für Workflow {workflow_id}...")
messages_url = f"{API_BASE_URL}/api/workflows/{workflow_id}/messages"
response = requests.get(
messages_url,
headers=HEADERS
)
if response.status_code != 200:
log_message(f"FEHLER: Abrufen der Nachrichten fehlgeschlagen. Status: {response.status_code}")
return []
messages = response.json()
log_message(f"{len(messages)} Nachrichten gefunden.")
return messages
def print_workflow_results(workflow_id):
"""Gibt die Ergebnisse eines Workflows aus"""
log_message("=== WORKFLOW-ERGEBNISSE ===")
# Status abrufen
status_url = f"{API_BASE_URL}/api/workflows/{workflow_id}/status"
status_response = requests.get(status_url, headers=HEADERS)
if status_response.status_code == 200:
status_data = status_response.json()
log_message(f"Workflow-Name: {status_data.get('name')}")
log_message(f"Status: {status_data.get('status')}")
log_message(f"Gestartet: {status_data.get('started_at')}")
log_message(f"Letzte Aktivität: {status_data.get('last_activity')}")
# Nachrichten abrufen und ausgeben
messages = get_workflow_messages(workflow_id)
log_message(f"Anzahl der Nachrichten: {len(messages)}")
for i, msg in enumerate(messages, 1):
log_message(f"--- Nachricht {i} ---")
log_message(f"Rolle: {msg.get('role')}")
# Inhalt gekürzt ausgeben (maximal ersten 200 Zeichen)
content = msg.get('content', '')
if content:
if len(content) > 200:
log_message(f"Inhalt: {content[:200]}... [gekürzt]")
else:
log_message(f"Inhalt: {content}")
# Anzahl der Dokumente ausgeben
docs = msg.get('documents', [])
if docs:
log_message(f"Dokumente: {len(docs)}")
for j, doc in enumerate(docs, 1):
source = doc.get('source', {})
doc_name = source.get('name', f"Dokument {j}")
log_message(f" - {doc_name}")
def main():
"""Hauptfunktion zum Testen des Workflows"""
# Beispiel-Datei zum Hochladen (Pfad anpassen)
file_path = "example.csv" # Hier den Pfad zu deiner Testdatei angeben
# Prompt für den Workflow
test_prompt = """Bitte analysiere die angehängte Datei und erstelle eine Zusammenfassung der wichtigsten Informationen.
Wenn es sich um eine CSV-Datei handelt, identifiziere die Spalten und gib mir einen Überblick über die enthaltenen Daten.
Erstelle außerdem eine Visualisierung, wenn du Zahlenwerte in der Datei findest."""
try:
# Datei hochladen
file_id = upload_file(file_path)
if not file_id:
log_message("Test abgebrochen: Datei konnte nicht hochgeladen werden.")
return False
# Workflow erstellen
workflow_id = create_workflow(test_prompt, file_id)
if not workflow_id:
log_message("Test abgebrochen: Workflow konnte nicht erstellt werden.")
return False
# Auf Abschluss des Workflows warten
workflow_status = poll_workflow_status(workflow_id)
if not workflow_status:
log_message("Test unvollständig: Timeout beim Warten auf Workflow-Abschluss.")
# Weiter machen, um zumindest Teilergebnisse zu sehen
# Ergebnisse ausgeben
print_workflow_results(workflow_id)
log_message("Test abgeschlossen.")
return True
except Exception as e:
log_message(f"FEHLER: Unerwartete Ausnahme: {str(e)}")
import traceback
log_message(traceback.format_exc())
return False
if __name__ == "__main__":
log_message("=== WORKFLOW-TEST GESTARTET ===")
success = main()
log_message(f"=== WORKFLOW-TEST BEENDET (Erfolgreich: {success}) ===")
sys.exit(0 if success else 1)

182
test_workflow.py Normal file
View file

@ -0,0 +1,182 @@
#!/usr/bin/env python3
"""
Backend-Testskript für die Workflow-Funktionalität mit Prompt und Datei.
Dieses Skript testet die Backend-Komponenten direkt, ohne über die API zu gehen.
"""
import os
import sys
import asyncio
import uuid
from datetime import datetime
import logging
import json
# Pfad zum Projekt-Root hinzufügen, damit Module gefunden werden
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
# Logging konfigurieren
logging.basicConfig(
level=logging.INFO,
format='[%(asctime)s] %(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger("backend_test")
# Imports aus dem Backend
from modules.lucydom_interface import get_lucydom_interface
from modules.chat import get_chat_manager
# Testparameter
TEST_MANDATE_ID = 1
TEST_USER_ID = 1
TEST_FILE_PATH = "d:/temp/prompt_a1.txt" # Pfad zur Testdatei anpassen
TEST_FILE_PATH1 = "d:/temp/LF-Nutshell.png" # Pfad zur Testdatei anpassen
TEST_PROMPT = """Bitte analysiere die angehängte Datei und erstelle eine Zusammenfassung der wichtigsten Informationen.
Erstelle außerdem eine Visualisierung, wenn du Zahlenwerte in der Datei findest."""
async def upload_test_file():
"""Lädt eine Testdatei ins Backend hoch und gibt die Datei-ID zurück"""
logger.info(f"Lade Testdatei hoch: {TEST_FILE_PATH}")
# LucyDOM-Interface initialisieren
lucy_interface = get_lucydom_interface(TEST_MANDATE_ID, TEST_USER_ID)
try:
# Prüfen, ob die Datei existiert
if not os.path.exists(TEST_FILE_PATH):
logger.error(f"Testdatei nicht gefunden: {TEST_FILE_PATH}")
return None
# Datei lesen
with open(TEST_FILE_PATH, 'rb') as f:
file_content = f.read()
# Dateinamen extrahieren
file_name = os.path.basename(TEST_FILE_PATH)
# Datei hochladen
file_meta = lucy_interface.save_uploaded_file(file_content, file_name)
file_id = file_meta.get('id')
logger.info(f"Datei erfolgreich hochgeladen. ID: {file_id}")
return file_meta
except Exception as e:
logger.error(f"Fehler beim Hochladen der Datei: {str(e)}")
return None
async def create_test_workflow(file_meta):
"""Erstellt einen Testworkflow mit dem angegebenen Prompt und der Datei"""
logger.info("Erstelle Testworkflow...")
# Chat-Manager initialisieren
chat_manager = get_chat_manager(TEST_MANDATE_ID, TEST_USER_ID)
# Nachrichtenobjekt vorbereiten
message = {
"role": "user",
"content": TEST_PROMPT,
"documents": [file_meta] if file_meta else []
}
try:
# Workflow erstellen (neue Workflow-ID wird automatisch generiert)
workflow = await chat_manager.workflow_integrate_userinput(message)
if not workflow:
logger.error("Workflow konnte nicht erstellt werden")
return None
workflow_id = workflow.get("id")
logger.info(f"Workflow erfolgreich erstellt. ID: {workflow_id}")
return workflow
except Exception as e:
logger.error(f"Fehler bei der Workflow-Erstellung: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return None
def print_workflow_details(workflow):
"""Gibt Details zum Workflow aus"""
if not workflow:
logger.warning("Kein Workflow zum Anzeigen vorhanden")
return
logger.info("=== WORKFLOW-DETAILS ===")
logger.info(f"ID: {workflow.get('id')}")
logger.info(f"Name: {workflow.get('name')}")
logger.info(f"Status: {workflow.get('status')}")
logger.info(f"Mandanten-ID: {workflow.get('mandate_id')}")
logger.info(f"Benutzer-ID: {workflow.get('user_id')}")
logger.info(f"Gestartet: {workflow.get('started_at')}")
logger.info(f"Letzte Aktivität: {workflow.get('last_activity')}")
# Nachrichten ausgeben
messages = workflow.get("messages", [])
logger.info(f"Anzahl der Nachrichten: {len(messages)}")
for i, msg in enumerate(messages, 1):
logger.info(f"--- Nachricht {i} ---")
logger.info(f"ID: {msg.get('id')}")
logger.info(f"Rolle: {msg.get('role')}")
logger.info(f"Sequenz: {msg.get('sequence_no')}")
logger.info(f"Agent: {msg.get('agent_name')}")
# Inhalt gekürzt ausgeben
content = msg.get('content', '')
if content:
preview = content[:200] + ('...' if len(content) > 200 else '')
logger.info(f"Inhalt: {preview}")
# Dokumente auflisten
documents = msg.get('documents', [])
if documents:
logger.info(f"Dokumente: {len(documents)}")
for j, doc in enumerate(documents, 1):
source = doc.get('source', {})
doc_name = source.get('name', f"Dokument {j}")
logger.info(f" - {doc_name}")
# Logs ausgeben
logs = workflow.get("logs", [])
logger.info(f"Anzahl der Logs: {len(logs)}")
if len(logs) > 0:
logger.info("Letzte 3 Logs:")
for log in logs[-3:]:
logger.info(f" - [{log.get('timestamp')}] {log.get('message')}")
async def main():
"""Hauptfunktion für den Backend-Test"""
logger.info("=== BACKEND WORKFLOW-TEST GESTARTET ===")
try:
# Schritt 1: Testdatei hochladen
file_meta = await upload_test_file()
if not file_meta:
logger.error("Test abgebrochen: Datei konnte nicht hochgeladen werden")
return False
# Schritt 2: Workflow erstellen
workflow = await create_test_workflow(file_meta)
if not workflow:
logger.error("Test abgebrochen: Workflow konnte nicht erstellt werden")
return False
# Schritt 3: Workflow-Details ausgeben
print_workflow_details(workflow)
logger.info("=== BACKEND WORKFLOW-TEST ERFOLGREICH BEENDET ===")
return True
except Exception as e:
logger.error(f"Unerwarteter Fehler im Test: {str(e)}")
import traceback
logger.error(traceback.format_exc())
return False
if __name__ == "__main__":
# Event-Loop ausführen
success = asyncio.run(main())
sys.exit(0 if success else 1)