MVP1 rev3
This commit is contained in:
parent
de48c4d8cb
commit
9e2b6f1344
26 changed files with 6505 additions and 1790 deletions
4
_uploads/.gitignore
vendored
4
_uploads/.gitignore
vendored
|
|
@ -1,4 +0,0 @@
|
|||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
1109
modules/backup-lucydom_interface copy.py
Normal file
1109
modules/backup-lucydom_interface copy.py
Normal file
File diff suppressed because it is too large
Load diff
858
modules/chat.py
Normal file
858
modules/chat.py
Normal 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]
|
||||
786
modules/chat_agent_analyst.py
Normal file
786
modules/chat_agent_analyst.py
Normal 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
804
modules/chat_agent_coder.py
Normal 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
|
||||
131
modules/chat_agent_creative.py
Normal file
131
modules/chat_agent_creative.py
Normal 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
|
||||
320
modules/chat_agent_documentation.py
Normal file
320
modules/chat_agent_documentation.py
Normal 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
|
||||
654
modules/chat_agent_webcrawler.py
Normal file
654
modules/chat_agent_webcrawler.py
Normal 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
213
modules/chat_registry.py
Normal 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
139
modules/lucydom_model copy.py
Normal file
139
modules/lucydom_model copy.py
Normal 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")
|
||||
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
189
routes/files.py
189
routes/files.py
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
227
test_gateway.py
Normal 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
182
test_workflow.py
Normal 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)
|
||||
Loading…
Reference in a new issue