running frontend - basic agent

This commit is contained in:
valueon 2025-03-25 00:05:01 +01:00
parent 06e6d09629
commit 19e44da2f8
17 changed files with 1999 additions and 667 deletions

View file

@ -1,5 +1,6 @@
{
"workspaces": 1,
"agents": 1,
"prompts": 1
"prompts": 1,
"files": 1
}

View file

@ -1 +1,24 @@
[]
[
{
"mandate_id": 1,
"user_id": 1,
"name": "test.pdf",
"type": "document",
"content_type": "application/pdf",
"size": 299729,
"path": "./_uploads\\587069d1-a14c-48c9-a911-233608668870.pdf",
"upload_date": "2025-03-24T16:44:13.751746",
"id": 1
},
{
"mandate_id": 1,
"user_id": 1,
"name": "test.pdf",
"type": "document",
"content_type": "application/pdf",
"size": 299729,
"path": "./_uploads\\c30faecc-c041-4225-805f-741328a04bba.pdf",
"upload_date": "2025-03-24T16:58:50.089708",
"id": 2
}
]

View file

@ -62,15 +62,6 @@
"name": "Protokoll: Besprechungsprotokoll",
"id": 7
},
{
"mandate_id": 1,
"user_id": 1,
"content": "Entwickle ein UI/UX-Designkonzept für [ANWENDUNG/WEBSITE]. Berücksichtige die Zielgruppe, Hauptfunktionen und die Markenidentität. Beschreibe die visuelle Gestaltung, Navigation, Interaktionsmuster und Informationsarchitektur. Erläutere, wie das Design die Benutzerfreundlichkeit und das Nutzererlebnis optimiert.",
"workspace_id": 1,
"created_at": "2025-03-23T21:43:42.714851",
"name": "Design: UI/UX Design",
"id": 8
},
{
"mandate_id": 1,
"user_id": 1,

View file

@ -1,6 +1,44 @@
.......................... Tasks
kannst du die tabelle und eingabeformulare prüfen und korrigieren. es gibt probleme bei der nutzung und der darstellung der buttons. hier am beispiel des modules "prompts.js". alle module haben die selben probleme.
generell: bitte alle styles für dieses modul "form_generic_entity.js" nach styles_form.css auslagern und styles.css bereinigen. form_generic_entity.js soll generisch bleiben.
Diese Themen für die Tabellensicht:
. Buttons sind zu gross, etwas kleiner
. sortieren und filtern gibt fehlermeldung
. den kopieren button ohne text, text "Kopieren" in hover verlagern
. den button "Verwenden" ohne Text, den Text in Hover verlagern
. alle buttons nebeneinander auf der linken seite der tabellenspalte platzieren, nach der checkbox
. bitte entferne die system-prompts zur konfirmation von delete oder update
Diese Themen für die Formularsicht:
. Buttons sind teilweise doppelt. dies bereinigen
. bitte entferne die system-prompts zur konfirmation von delete oder update
hier fehlermeldungen aus der console, helfen vielleicht:
form-generic-entity.js:95 Fehler beim Aktualisieren des prompt: TypeError: window.globalUtils.updateApiData is not a function
at updateEntity (form-generic-entity.js:77:54)
at HTMLButtonElement.<anonymous> (form-generic-entity.js:770:9)
updateEntity @ form-generic-entity.js:95
(anonymous) @ form-generic-entity.js:770Understand this errorAI
5form-generic-entity.js:520 Uncaught TypeError: Cannot set properties of null (setting 'className')
at form-generic-entity.js:520:28
at NodeList.forEach (<anonymous>)
at handleSort (form-generic-entity.js:511:13)
at HTMLTableCellElement.<anonymous> (form-generic-entity.js:484:44)
(anonymous) @ form-generic-entity.js:520
handleSort @ form-generic-entity.js:511
(anonymous) @ form-generic-entity.js:484Understand this errorAI
form-generic-entity.js:95 Fehler beim Aktualisieren des prompt: TypeError: window.globalUtils.updateApiData is not a function
at updateEntity (form-generic-entity.js:77:54)
at HTMLButtonElement.<anonymous> (form-generic-entity.js:770:9)
hast Du alle informationen zur korrektur?
--------------------------- OPEN
Chat mit Instant message - auch inputs geben während der ausführung

View file

@ -4,7 +4,7 @@ ALGORITHM = HS256
ACCESS_TOKEN_EXPIRE_MINUTES = 300
[Env]
INSTANCE = Dev # "Dev" or "Prod"
INSTANCE = Dev ; Dev or Prod
URL_ALLOWED = ["http://localhost:8080","https://poweron-lucyagents-xxx.germanywestcentral-01.azurewebsites.net"]
[Database_Dev]
@ -24,7 +24,7 @@ DEBUG = True
UPLOAD_DIR = ./_uploads
RESULTS_DIR = ./_results
MAX_HISTORY = 50
AI_PROVIDER = anthropic # Mögliche Werte: "openai" oder "anthropic"
AI_PROVIDER = anthropic ; openai oder anthropic
[Connector_AiOpenai]
API_KEY = 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
@ -33,15 +33,15 @@ MODEL_NAME = gpt-4o
TEMPERATURE = 0.2
MAX_TOKENS = 2000
[Connector_AiWebscraping]
TIMEOUT = 10
MAX_URLS = 3
MAX_CONTENT_LENGTH = 3000
USER_AGENT = Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36
[Connector_AiAnthropic]
API_KEY = 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
API_URL = https://api.anthropic.com/v1/messages
MODEL_NAME = claude-3-opus-20240229
TEMPERATURE = 0.2
MAX_TOKENS = 2000
[Connector_AiWebscraping]
TIMEOUT = 10
MAX_URLS = 3
MAX_CONTENT_LENGTH = 3000
USER_AGENT = Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36

View file

@ -1,11 +1,31 @@
import configparser
import os
import re
# Konfiguration aus config.ini laden
def load_config():
config = configparser.ConfigParser()
config = configparser.ConfigParser(
comment_prefixes=('#', ';'), # Define comment prefixes
inline_comment_prefixes=('#', ';') # Enable inline comments
)
config_path = os.path.join(os.path.dirname(__file__), 'config.ini')
if not os.path.exists(config_path):
exit("No config file")
config.read(config_path)
return config
# Clean up any comment text that might have been included in values
for section in config.sections():
for key in config[section]:
# Get the original value
value = config[section][key]
# Remove inline comments if they exist
# This regex looks for the first occurrence of a comment character that's not escaped
comment_match = re.search(r'(?<!\\)[;#]', value)
if comment_match:
# Keep only the part before the comment
clean_value = value[:comment_match.start()].strip()
config[section][key] = clean_value
return config

View file

@ -17,10 +17,10 @@ def load_config_data():
config = configload.load_config()
return {
"api_key": config.get('Connector_AiAnthropic', 'API_KEY'),
"api_url": config.get('Connector_AiAnthropic', 'API_URL', fallback="https://api.anthropic.com/v1/messages"),
"model_name": config.get('Connector_AiAnthropic', 'MODEL_NAME', fallback="claude-3-opus-20240229"),
"temperature": float(config.get('Connector_AiAnthropic', 'TEMPERATURE', fallback="0.2")),
"max_tokens": int(config.get('Connector_AiAnthropic', 'MAX_TOKENS', fallback="2000"))
"api_url": config.get('Connector_AiAnthropic', 'API_URL'),
"model_name": config.get('Connector_AiAnthropic', 'MODEL_NAME'),
"temperature": float(config.get('Connector_AiAnthropic', 'TEMPERATURE')),
"max_tokens": int(config.get('Connector_AiAnthropic', 'MAX_TOKENS'))
}
class ChatService:
@ -80,7 +80,7 @@ class ChatService:
"temperature": temperature,
"max_tokens": max_tokens
}
response = await self.http_client.post(
self.api_url,
json=payload
@ -268,6 +268,168 @@ class ChatService:
return message_content
def parse_filedata(self, file_paths: List[Dict[str, Any]], prompt_text: str = "", file_contents: Dict[str, str] = None) -> Dict[str, Any]:
"""
Bereitet Dateien für die Anthropic API vor und erstellt ein einheitliches Message-Objekt.
Args:
file_paths: Liste von Dateipfaden mit Metadaten (Dict mit id, name, type, path)
prompt_text: Der Text-Prompt, der zusammen mit den Dateien gesendet werden soll
file_contents: Optional vorgelesene Dateiinhalte als Dict[file_id, content]
Returns:
Ein standardisiertes Message-Objekt, das für beide API-Typen verwendet werden kann
"""
# Basisstruktur für die Nachricht
message = {
"role": "user",
"content": []
}
# Text-Prompt hinzufügen
if prompt_text:
message["content"].append({
"type": "text",
"text": prompt_text
})
# Dateien als Anhänge hinzufügen
for file_info in file_paths:
file_path = file_info.get("path", "")
file_name = file_info.get("name", "")
file_id = file_info.get("id", "")
# Prüfen, ob Dateiinhalt bereits vorhanden ist
if file_contents and file_id in file_contents:
# Nur Kontext-Information hinzufügen
message["content"].append({
"type": "text",
"text": f"Datei: {file_name}\n{file_contents[file_id]}"
})
logger.info(f"Vorverarbeiteter Inhalt für Datei {file_name} verwendet")
continue
# Sonst Datei direkt verarbeiten
if file_path and os.path.exists(file_path):
try:
# Datei als Base64 codieren
with open(file_path, "rb") as f:
file_data = f.read()
base64_data = base64.b64encode(file_data).decode('utf-8')
# MIME-Typ bestimmen mit python-magic, wenn verfügbar
try:
import magic
mime_type = magic.from_buffer(file_data, mime=True)
except ImportError:
# Fallback auf mimetypes, wenn python-magic nicht verfügbar ist
mime_type, _ = mimetypes.guess_type(file_path)
if not mime_type:
# Fallback auf Dateierweiterung
extension = os.path.splitext(file_path)[1].lower()[1:]
mime_type = get_mime_type_from_extension(extension)
# Content-Type bestimmen (image, document, oder allgemein file)
content_type, _ = determine_content_structure(mime_type)
# Füge die Datei als Anhang hinzu
message["content"].append({
"type": content_type,
"source": {
"type": "base64",
"media_type": mime_type,
"data": base64_data
}
})
logger.info(f"Datei {file_name} als {content_type} hinzugefügt")
except Exception as e:
logger.error(f"Fehler beim Hinzufügen der Datei {file_name}: {str(e)}")
message["content"].append({
"type": "text",
"text": f"[Fehler beim Laden der Datei {file_name}: {str(e)}]"
})
else:
# Datei nicht gefunden - Hinweis einfügen
message["content"].append({
"type": "text",
"text": f"[Datei {file_name} nicht verfügbar]"
})
return message
async def close(self):
"""Schließt den HTTP-Client beim Beenden der Anwendung"""
await self.http_client.aclose()
await self.http_client.aclose()
def determine_content_structure(mime_type: str) -> tuple:
"""
Bestimmt den richtigen content_type und die Nachrichtenstruktur basierend auf dem MIME-Typ.
Args:
mime_type: Der MIME-Typ der Datei
Returns:
Tuple mit (content_type, message_structure)
"""
# Bildtypen
if mime_type.startswith("image/"):
return "image", "image"
# Dokumenttypen
document_types = [
"application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", # docx
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", # xlsx
"application/vnd.openxmlformats-officedocument.presentationml.presentation", # pptx
"application/vnd.ms-excel",
"application/vnd.ms-powerpoint",
"application/msword",
"text/csv",
"text/plain",
"application/json",
"application/xml",
"text/html"
]
if any(mime_type.startswith(dt) for dt in document_types) or mime_type in document_types:
return "document", "document"
# Fallback für unbekannte Typen
return "file", "file"
def get_mime_type_from_extension(extension: str) -> str:
"""
Bestimmt den MIME-Typ basierend auf der Dateiendung.
Args:
extension: Die Dateiendung ohne Punkt
Returns:
Der entsprechende MIME-Typ
"""
extension_to_mime = {
"pdf": "application/pdf",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"doc": "application/msword",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"xls": "application/vnd.ms-excel",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"ppt": "application/vnd.ms-powerpoint",
"csv": "text/csv",
"txt": "text/plain",
"json": "application/json",
"xml": "application/xml",
"html": "text/html",
"htm": "text/html",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"png": "image/png",
"gif": "image/gif",
"webp": "image/webp",
"svg": "image/svg+xml"
}
return extension_to_mime.get(extension, "application/octet-stream")

View file

@ -17,10 +17,10 @@ def load_config_data():
config = configload.load_config()
return {
"api_key": config.get('Connector_AiOpenai', 'API_KEY'),
"api_url": config.get('Connector_AiOpenai', 'API_URL', fallback="https://api.openai.com/v1/chat/completions"),
"model_name": config.get('Connector_AiOpenai', 'MODEL_NAME', fallback="gpt-4o"),
"temperature": float(config.get('Connector_AiOpenai', 'TEMPERATURE', fallback="0.2")),
"max_tokens": int(config.get('Connector_AiOpenai', 'MAX_TOKENS', fallback="2000"))
"api_url": config.get('Connector_AiOpenai', 'API_URL'),
"model_name": config.get('Connector_AiOpenai', 'MODEL_NAME'),
"temperature": float(config.get('Connector_AiOpenai', 'TEMPERATURE')),
"max_tokens": int(config.get('Connector_AiOpenai', 'MAX_TOKENS'))
}
class ChatService:
@ -124,15 +124,20 @@ class ChatService:
if not mime_type:
mime_type = "application/octet-stream"
# Füge die Datei als Anhang hinzu
message_content.append({
"type": "file",
"source": {
"type": "base64",
"media_type": mime_type,
"data": base64_data
}
})
# Bei OpenAI werden Bilder anders behandelt als bei Anthropic
if mime_type.startswith("image/"):
message_content.append({
"type": "image_url",
"image_url": {
"url": f"data:{mime_type};base64,{base64_data}"
}
})
else:
# Für nicht-Bilder werden sie als Textbeschreibung hinzugefügt
message_content.append({
"type": "text",
"text": f"[Datei: {file_info.get('name', 'Unbekannt')} (Typ: {mime_type})]"
})
logger.info(f"Datei {file_info.get('name', 'Unbekannt')} als Anhang hinzugefügt")
@ -141,6 +146,157 @@ class ChatService:
return message_content
def parse_filedata(self, file_paths: List[Dict[str, Any]], prompt_text: str = "", file_contents: Dict[str, str] = None) -> Dict[str, Any]:
"""
Bereitet Dateien für die OpenAI API vor und erstellt ein einheitliches Message-Objekt.
Args:
file_paths: Liste von Dateipfaden mit Metadaten (Dict mit id, name, type, path)
prompt_text: Der Text-Prompt, der zusammen mit den Dateien gesendet werden soll
file_contents: Optional vorgelesene Dateiinhalte als Dict[file_id, content]
Returns:
Ein standardisiertes Message-Objekt, das für beide API-Typen verwendet werden kann
"""
# Basisstruktur für die Nachricht in OpenAI-Format
message = {
"role": "user",
"content": []
}
# Text-Prompt hinzufügen
if prompt_text:
message["content"].append({
"type": "text",
"text": prompt_text
})
# Dateien als Anhänge hinzufügen
for file_info in file_paths:
file_path = file_info.get("path", "")
file_name = file_info.get("name", "")
file_id = file_info.get("id", "")
# Prüfen, ob Dateiinhalt bereits vorhanden ist
if file_contents and file_id in file_contents:
# Bereits verarbeiteten Inhalt verwenden
message["content"].append({
"type": "text",
"text": f"Datei: {file_name}\n{file_contents[file_id]}"
})
logger.info(f"Vorverarbeiteter Inhalt für Datei {file_name} verwendet")
continue
# Sonst Datei direkt verarbeiten
if file_path and os.path.exists(file_path):
try:
# Datei einlesen
with open(file_path, "rb") as f:
file_data = f.read()
# MIME-Typ bestimmen mit python-magic, wenn verfügbar
try:
import magic
mime_type = magic.from_buffer(file_data, mime=True)
except ImportError:
# Fallback auf mimetypes, wenn python-magic nicht verfügbar ist
mime_type, _ = mimetypes.guess_type(file_path)
if not mime_type:
# Fallback auf Dateierweiterung
extension = os.path.splitext(file_path)[1].lower()[1:]
mime_type = get_mime_type_from_extension(extension)
# Content-Type bestimmen für OpenAI
if mime_type.startswith("image/"):
# Bild als Base64 kodieren
base64_data = base64.b64encode(file_data).decode('utf-8')
message["content"].append({
"type": "image_url",
"image_url": {
"url": f"data:{mime_type};base64,{base64_data}"
}
})
else:
# Für nicht-Bild-Dateien als Text hinzufügen
try:
# Textdateien direkt als Text extrahieren
if mime_type in ["text/plain", "text/csv", "application/json", "text/html", "application/xml"]:
message["content"].append({
"type": "text",
"text": f"[Datei: {file_name}]\n{file_data.decode('utf-8', errors='replace')}"
})
else:
# Bei Binärdateien nur einen Hinweis einfügen
message["content"].append({
"type": "text",
"text": f"[Datei: {file_name} (Typ: {mime_type}) ist verfügbar, aber kann nicht direkt angezeigt werden]"
})
except UnicodeDecodeError:
# Bei Decodierungsfehlern nur einen Hinweis einfügen
message["content"].append({
"type": "text",
"text": f"[Datei: {file_name} (Typ: {mime_type}) ist binär und kann nicht direkt angezeigt werden]"
})
logger.info(f"Datei {file_name} zum OpenAI-Nachrichtenobjekt hinzugefügt")
except Exception as e:
logger.error(f"Fehler beim Hinzufügen der Datei {file_name}: {str(e)}")
message["content"].append({
"type": "text",
"text": f"[Fehler beim Laden der Datei {file_name}: {str(e)}]"
})
else:
# Datei nicht gefunden - Hinweis einfügen
message["content"].append({
"type": "text",
"text": f"[Datei {file_name} nicht verfügbar]"
})
# Prüfe, ob wir ein leeres content-Array haben und wandle es in einen String um
if not message["content"]:
message["content"] = prompt_text or ""
elif len(message["content"]) == 1 and message["content"][0]["type"] == "text":
# Wenn nur ein Text-Element vorhanden ist, vereinfachen wir die Struktur
message["content"] = message["content"][0]["text"]
return message
async def close(self):
"""Schließt den HTTP-Client beim Beenden der Anwendung"""
await self.http_client.aclose()
await self.http_client.aclose()
def get_mime_type_from_extension(extension: str) -> str:
"""
Bestimmt den MIME-Typ basierend auf der Dateiendung.
Args:
extension: Die Dateiendung ohne Punkt
Returns:
Der entsprechende MIME-Typ
"""
extension_to_mime = {
"pdf": "application/pdf",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"doc": "application/msword",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"xls": "application/vnd.ms-excel",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"ppt": "application/vnd.ms-powerpoint",
"csv": "text/csv",
"txt": "text/plain",
"json": "application/json",
"xml": "application/xml",
"html": "text/html",
"htm": "text/html",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"png": "image/png",
"gif": "image/gif",
"webp": "image/webp",
"svg": "image/svg+xml"
}
return extension_to_mime.get(extension, "application/octet-stream")

View file

@ -1,19 +1,20 @@
import asyncio
import uuid
import os
import json
import logging
import base64
import mimetypes
from typing import List, Dict, Any, Optional
from datetime import datetime
from fastapi import HTTPException
import configload as configload
# Import der Teilmodule
import modules.agentservice_part_filehandling as file_handling
import modules.agentservice_part_agents as agents
import modules.agentservice_part_results as results
# Logger konfigurieren
logger = logging.getLogger(__name__)
# Konfigurationsdaten laden
def load_config_data():
config = configload.load_config()
@ -23,7 +24,7 @@ def load_config_data():
"upload_dir": config.get('Module_AgentserviceInterface', 'UPLOAD_DIR'),
"results_dir": config.get('Module_AgentserviceInterface', 'RESULTS_DIR'),
"max_history": config.get('Module_AgentserviceInterface', 'MAX_HISTORY'),
"ai_provider": config.get('Module_AgentserviceInterface', 'AI_PROVIDER', fallback="openai") # Standard ist OpenAI
"ai_provider": config.get('Module_AgentserviceInterface', 'AI_PROVIDER'),
}
}
# Debug-Modus einstellen
@ -91,7 +92,7 @@ class AgentService:
self,
workflow_id: str,
prompt: str,
agents: List[Dict[str, Any]],
agents_list: List[Dict[str, Any]],
files: List[Dict[str, Any]]
) -> str:
"""
@ -99,8 +100,11 @@ class AgentService:
Anstatt die Agenten der Reihe nach abzuarbeiten, wird ein AI-Moderator verwendet,
der die Agenten basierend auf ihren Fähigkeiten und bisherigen Antworten steuert.
Verwendet parse_filedata aus dem jeweiligen Connector für die Verarbeitung von Dateien,
wobei vorverarbeitete Dateiinhalte effizient wiederverwendet werden.
"""
logger.info(f"Starte Workflow {workflow_id} mit {len(agents)} Agenten und {len(files)} Dateien")
logger.info(f"Starte Workflow {workflow_id} mit {len(agents_list)} Agenten und {len(files)} Dateien")
# Mandanten- und Benutzerkontext in die Workflow-Daten aufnehmen
self.workflows[workflow_id] = {
@ -120,206 +124,46 @@ class AgentService:
self._add_log(workflow_id, "Workflow gestartet", "info")
self._add_log(workflow_id, f"Verarbeite {len(files)} Dateien...", "info")
# Dateikontexte und Inhalte vorbereiten
file_contexts = []
file_contents = {}
# Dateikontexte vorbereiten
file_contexts = file_handling.prepare_file_contexts(files, self.upload_dir)
for file in files:
file_id = file["id"]
file_name = file["name"]
file_type = file["type"]
file_path = file.get("path", "")
# Wenn kein Pfad angegeben ist, versuche, ihn aus dem Upload-Verzeichnis abzuleiten
if not file_path and file_name:
possible_path = os.path.join(self.upload_dir, file_name)
if os.path.exists(possible_path):
file_path = possible_path
logger.debug(f"Pfad für Datei {file_name} gefunden: {file_path}")
file_contexts.append({
"id": file_id,
"name": file_name,
"type": file_type,
"size": file.get("size", "Unbekannt"),
"path": file_path
})
# Dateiinhalt lesen, wenn der Pfad verfügbar ist
if file_path and os.path.exists(file_path):
try:
# Text-basierte Dateien direkt lesen
if file_type == "document":
# Einfache Textdateien
if file_name.endswith(('.txt', '.csv', '.md', '.json')):
with open(file_path, 'r', encoding='utf-8') as f:
file_contents[file_id] = f.read()
self._add_log(workflow_id, f"Datei {file_name} gelesen", "info")
# Excel-Dateien
elif file_name.endswith(('.xlsx', '.xls')):
import pandas as pd
try:
df = pd.read_excel(file_path)
file_contents[file_id] = f"Excel-Datei mit {len(df)} Zeilen und {len(df.columns)} Spalten.\n"
file_contents[file_id] += f"Spalten: {', '.join(df.columns.tolist())}\n"
file_contents[file_id] += "Erste 5 Zeilen:\n"
file_contents[file_id] += df.head(5).to_string()
self._add_log(workflow_id, f"Excel-Datei {file_name} gelesen", "info")
except Exception as e:
self._add_log(workflow_id, f"Fehler beim Lesen der Excel-Datei {file_name}: {str(e)}", "error")
# CSV-Dateien
elif file_name.endswith('.csv'):
import pandas as pd
try:
df = pd.read_csv(file_path)
file_contents[file_id] = f"CSV-Datei mit {len(df)} Zeilen und {len(df.columns)} Spalten.\n"
file_contents[file_id] += f"Spalten: {', '.join(df.columns.tolist())}\n"
file_contents[file_id] += "Erste 5 Zeilen:\n"
file_contents[file_id] += df.head(5).to_string()
self._add_log(workflow_id, f"CSV-Datei {file_name} gelesen", "info")
except Exception as e:
self._add_log(workflow_id, f"Fehler beim Lesen der CSV-Datei {file_name}: {str(e)}", "error")
# PDF-Dateien
elif file_name.endswith('.pdf'):
try:
# Falls PyPDF2 installiert ist
try:
from PyPDF2 import PdfReader
reader = PdfReader(file_path)
text = ""
for page in reader.pages:
text += page.extract_text() + "\n\n"
file_contents[file_id] = f"PDF mit {len(reader.pages)} Seiten.\nInhalt:\n{text[:2000]}..."
self._add_log(workflow_id, f"PDF-Datei {file_name} gelesen", "info")
except ImportError:
self._add_log(workflow_id, "PyPDF2 nicht installiert. PDF-Inhalt kann nicht extrahiert werden.", "warning")
file_contents[file_id] = f"PDF-Datei (Inhalt nicht verfügbar, PyPDF2 fehlt)"
except Exception as e:
self._add_log(workflow_id, f"Fehler beim Lesen der PDF-Datei {file_name}: {str(e)}", "error")
# Andere Dokumenttypen
else:
self._add_log(workflow_id, f"Nicht unterstütztes Dokumentformat: {file_name}", "warning")
file_contents[file_id] = f"Dateiinhalt nicht verfügbar (Nicht unterstütztes Format)"
# Bilddateien werden nicht direkt gelesen, nur Metadaten gespeichert
elif file_type == "image":
file_contents[file_id] = f"Bilddatei: {file_name} (Inhalt nicht als Text verfügbar)"
except Exception as e:
logger.error(f"Fehler beim Lesen der Datei {file_name}: {str(e)}")
self._add_log(workflow_id, f"Fehler beim Lesen der Datei {file_name}: {str(e)}", "error")
else:
if file_path:
self._add_log(workflow_id, f"Datei {file_name} nicht gefunden: {file_path}", "warning")
else:
self._add_log(workflow_id, f"Kein Pfad für Datei {file_name} verfügbar", "warning")
file_contents[file_id] = f"Dateiinhalt nicht verfügbar"
# Dateiinhalte lesen - EINMAL für den gesamten Workflow
file_contents = file_handling.read_file_contents(
file_contexts,
self.upload_dir,
workflow_id,
self._add_log
)
# Erstelle einen Kontext mit Dateiliste und Inhalten für leichteren Zugriff
file_context_text = "Verfügbare Dateien:\n" + "\n".join([f"- {file['name']} ({file['type']}, {file['size']})" for file in file_contexts])
# Füge Dateiinhalte hinzu (mit Längenbegrenzung)
for file_id, content in file_contents.items():
file_name = next((f['name'] for f in file_contexts if f['id'] == file_id), "Unbekannte Datei")
file_context_text += f"\n\n==== DATEIINHALT: {file_name} ====\n"
# Begrenze den Inhalt, um Token-Limits zu respektieren
max_content_length = 5000 # Anpassen je nach Anzahl der Dateien und Umfang
if len(content) > max_content_length:
file_context_text += content[:max_content_length] + "...\n[Dateiinhalt gekürzt aus Platzgründen]"
else:
file_context_text += content
# Erstelle einen formatierten Kontext für die Protokollierung
file_context_text = file_handling.format_file_context_text(file_contexts, file_contents)
logger.debug(f"Dateikontexte erstellt: {len(file_contexts)} Dateien")
self.workflows[workflow_id]["progress"] = 0.1
# Initialisiere den Chatverlauf für den Agenten-Dialog
chat_history = []
# Erstelle das Nachrichtenobjekt für die initialen Dateien und den Prompt
message_content = self.service_aichat.prepare_file_message_content(prompt, file_contexts)
# Füge Dateien als Base64-Anhänge hinzu
for file in file_contexts:
if file["path"] and os.path.exists(file["path"]):
try:
# Datei als Base64 codieren
with open(file["path"], "rb") as f:
file_data = f.read()
base64_data = base64.b64encode(file_data).decode('utf-8')
# MIME-Typ bestimmen
mime_type, _ = mimetypes.guess_type(file["path"])
if not mime_type:
mime_type = "application/octet-stream"
# Füge die Datei als Anhang hinzu
message_content.append({
"type": "file",
"source": {
"type": "base64",
"media_type": mime_type,
"data": base64_data
}
})
except Exception as e:
logger.error(f"Fehler beim Hinzufügen der Datei {file['name']} als Anhang: {str(e)}")
# Nachrichtenobjekt erstellen
initial_message = {
"role": "user",
"content": message_content
}
# Erstelle das standardisierte Nachrichtenobjekt für die initialen Dateien und den Prompt
# Verwende parse_filedata aus dem Connector und übergebe die vorverarbeiteten Dateiinhalte
initial_message = self.service_aichat.parse_filedata(
file_contexts,
prompt,
file_contents # Übergebe die vorverarbeiteten Inhalte
)
# Initialen Prompt zum Chatverlauf hinzufügen
chat_history.append(initial_message)
# Initialisiere die verfügbaren Agenten mit ihren Fähigkeiten
available_agents = {}
for agent in agents:
agent_id = agent["id"]
agent_name = agent["name"]
agent_type = agent["type"]
agent_capabilities = agent.get("capabilities", "")
available_agents[agent_id] = {
"id": agent_id,
"name": agent_name,
"type": agent_type,
"capabilities": agent_capabilities,
"used": False
}
# Initialisiere den Status
available_agents = agents.initialize_agents(agents_list)
# Initialisiere den Status für jeden Agenten
for agent_id in available_agents:
self.workflows[workflow_id]["agent_statuses"][agent_id] = "pending"
# Initialisiere die Moderator-Rolle - Fester Teil
moderator_prompt_base = """
Du bist der Moderator eines Multi-Agent-Systems. Deine Aufgabe ist es, die Zusammenarbeit zwischen verschiedenen spezialisierten Agenten zu koordinieren, um die Anfrage des Benutzers bestmöglich zu erfüllen.
Du sollst:
1. Die Anfrage des Benutzers verstehen und analysieren
2. Den am besten geeigneten Agenten basierend auf seinen Fähigkeiten auswählen
3. Die Antworten der Agenten überwachen und bewerten
4. Falls nötig, weitere Agenten hinzuziehen, um die Anfrage vollständig zu bearbeiten
5. Den Workflow beenden, wenn die Anfrage vollständig erfüllt wurde
Für jeden Schritt sollst du begründen, warum du einen bestimmten Agenten auswählst, und zusammenfassen, was bisher erreicht wurde.
"""
# Dynamischer Teil - Verfügbare Agenten aus den tatsächlich vorhandenen Agenten
agents_description = "Verfügbare Agenten:\n"
for agent_id, agent in available_agents.items():
agents_description += f"- {agent['name']} (Typ: {agent['type']}): {agent['capabilities']}\n"
moderator_prompt_end = """
Beende den Workflow, wenn die Aufgabe erfüllt ist oder keine weiteren Agenten zur Bearbeitung beitragen können.
"""
# Kombiniere alle Teile
moderator_system_prompt = moderator_prompt_base + "\n" + agents_description + "\n" + moderator_prompt_end
# Moderator-Prompt erstellen
moderator_system_prompt = agents.get_moderator_prompt(available_agents)
# Starte den Workflow mit dem Moderator
self._add_log(workflow_id, "Starte Agenten-Tischrunde mit Moderator", "info")
@ -367,142 +211,175 @@ class AgentService:
# Log der Moderator-Entscheidung
self._add_log(workflow_id, f"Moderator-Entscheidung: {moderator_text[:100]}...", "info")
# Finde den nächsten zu verwendenden Agenten
selected_agent_id = agents.find_next_agent(moderator_text, available_agents)
# Prüfe, ob der Workflow beendet werden soll
if any(phrase in moderator_text.lower() for phrase in ["workflow beenden", "aufgabe erfüllt", "beende den workflow", "workflow abschließen"]):
if selected_agent_id == "WORKFLOW_COMPLETE":
self._add_log(workflow_id, "Moderator hat den Workflow beendet", "success")
workflow_complete = True
break
# Extrahiere den ausgewählten Agenten
selected_agent_id = None
# Versuche, den ausgewählten Agenten aus dem Text zu extrahieren
for agent_id, agent in available_agents.items():
if agent["name"] in moderator_text or f"Agent {agent_id}" in moderator_text:
selected_agent_id = agent_id
break
# Prüfe, ob ein Agent ausgewählt wurde
if not selected_agent_id:
self._add_log(workflow_id, "Moderator konnte keinen Agenten identifizieren", "warning")
# Wähle den ersten nicht verwendeten Agenten
for agent_id, agent in available_agents.items():
if not agent["used"]:
selected_agent_id = agent_id
break
# Wenn alle Agenten bereits verwendet wurden, wähle den Initialisierungs-Agenten
if not selected_agent_id:
for agent_id, agent in available_agents.items():
if agent["type"] == "initialisierung":
selected_agent_id = agent_id
break
# Als letztes Mittel wähle einfach den ersten Agenten
if not selected_agent_id and available_agents:
selected_agent_id = list(available_agents.keys())[0]
self._add_log(workflow_id, "Kein Agent ausgewählt. Beende Workflow.", "warning")
workflow_complete = True
continue
if selected_agent_id:
# Agenten aus der Liste markieren
selected_agent = available_agents[selected_agent_id]
selected_agent["used"] = True
# Agenten aus der Liste markieren
selected_agent = available_agents[selected_agent_id]
selected_agent["used"] = True
# Agenten-Status aktualisieren
self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "running"
self._add_log(
workflow_id,
f"Agent '{selected_agent['name']}' beginnt mit der Verarbeitung...",
"start",
selected_agent_id,
selected_agent['name']
)
# Agent-spezifische Anweisungen erstellen
agent_instructions = agents.get_agent_instructions(selected_agent["type"])
# Agent-Prompt erstellen
agent_prompt = agents.create_agent_prompt(selected_agent, agent_instructions)
# Kopie des Chatverlaufs für den Agenten erstellen
agent_chat = [agent_prompt] + chat_history[-self.max_history:]
# Falls der Agent ein Webscraper ist und Scraping notwendig ist
if selected_agent["type"] == "scraper":
self._add_log(workflow_id, "Führe Web-Scraping durch...", "info",
selected_agent_id, selected_agent["name"])
web_data = await self.service_aiscrap.scrape_web_data(prompt)
if web_data:
agent_chat.append({
"role": "system",
"content": f"# Gescrapte Web-Daten\n{web_data}"
})
self._add_log(workflow_id, "Web-Scraping abgeschlossen", "info",
selected_agent_id, selected_agent["name"])
# Agent führt seinen Teil aus
try:
# Initialisiere eine Schleife für mögliche Dateianfragen des Agenten
agent_processing_complete = False
max_file_requests = 5 # Begrenze die Anzahl der Dateianfragen pro Agent
file_request_count = 0
# Agenten-Status aktualisieren
self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "running"
self._add_log(
workflow_id,
f"Agent '{selected_agent['name']}' beginnt mit der Verarbeitung...",
"start",
selected_agent_id,
selected_agent['name']
)
# Agent-spezifische Anweisungen erstellen
agent_instructions = self._get_agent_instructions(selected_agent["type"])
# Agent-Prompt erstellen
agent_prompt = {
"role": "system",
"content": f"""
# Aufgabe
Du bist ein spezialisierter Agent vom Typ {selected_agent['type']} mit dem Namen {selected_agent['name']}.
{agent_instructions}
Bitte analysiere den Chatverlauf und die Dateien und beantworte die Anfrage gemäß deiner Rolle.
Ausgabeformat:
[Agent: {selected_agent['name']}]
Deine Antwort...
"""
}
# Kopie des Chatverlaufs für den Agenten erstellen
agent_chat = [agent_prompt] + chat_history[-self.max_history:]
# Falls der Agent ein Webscraper ist und Scraping notwendig ist
if selected_agent["type"] == "scraper":
self._add_log(workflow_id, "Führe Web-Scraping durch...", "info", selected_agent_id, selected_agent["name"])
web_data = await self.service_aiscrap.scrape_web_data(prompt)
if web_data:
agent_chat.append({
"role": "system",
"content": f"# Gescrapte Web-Daten\n{web_data}"
})
self._add_log(workflow_id, "Web-Scraping abgeschlossen", "info", selected_agent_id, selected_agent["name"])
# Agent führt seinen Teil aus
try:
while not agent_processing_complete and file_request_count < max_file_requests:
# Rufe die API auf
agent_response = await self.service_aichat.call_api(agent_chat)
agent_text = agent_response["choices"][0]["message"]["content"]
# Füge die Antwort des Agenten zum Chatverlauf hinzu
chat_history.append({
"role": "assistant",
"content": agent_text
})
# Prüfe, ob der Agent weitere Dateien anfordert
file_commands = file_handling.parse_file_access_commands(agent_text)
# Agent-Ergebnis erstellen
result = self._create_agent_result(
workflow_id,
selected_agent,
len(self.workflows[workflow_id]["results"]),
prompt,
file_contexts,
agent_text
)
self.workflows[workflow_id]["results"].append(result)
if file_commands:
file_request_count += 1
self._add_log(
workflow_id,
f"Agent '{selected_agent['name']}' fordert zusätzliche Dateiinhalte an (Anfrage {file_request_count})",
"info",
selected_agent_id,
selected_agent["name"]
)
# Verarbeite alle Dateianfragen
file_responses = []
for cmd in file_commands:
file_id = cmd.get('file_id')
complete = cmd.get('complete', False)
start = cmd.get('start')
end = cmd.get('end')
pages = cmd.get('pages')
content = file_handling.load_additional_file_content(
workflow_id,
file_id,
file_contents,
file_contexts,
self._add_log,
read_complete=complete,
start_pos=start,
end_pos=end,
page_numbers=pages
)
if content:
file_name = next((f['name'] for f in file_contexts if f['id'] == file_id),
f"Datei {file_id}")
file_responses.append(f"Zusätzlicher Inhalt für {file_name}:\n\n{content}")
else:
file_responses.append(f"Datei mit ID {file_id} konnte nicht geladen werden.")
# Füge die Antworten zum Chatverlauf hinzu
file_response_text = "\n\n".join(file_responses)
system_response = {
"role": "system",
"content": f"Hier sind die angeforderten Dateiinhalte:\n\n{file_response_text}\n\n" +
f"Bitte fahre nun mit deiner Analyse fort."
}
# Füge Systemantwort zum Chatverlauf und zum Agentenchat hinzu
chat_history.append(system_response)
agent_chat.append(system_response)
# Setze die Schleife fort, um dem Agenten die Möglichkeit zu geben, seine Analyse fortzusetzen
continue
self._add_log(
workflow_id,
f"Agent '{selected_agent['name']}' hat die Verarbeitung abgeschlossen",
"complete",
selected_agent_id,
selected_agent["name"]
)
# Agenten-Status aktualisieren
self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "completed"
except Exception as e:
logger.error(f"Fehler bei der Ausführung von Agent '{selected_agent['name']}': {str(e)}")
self._add_log(
workflow_id,
f"Fehler bei der Ausführung: {str(e)}",
"error",
selected_agent_id,
selected_agent["name"]
)
self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "failed"
# Füge die Fehlermeldung zum Chatverlauf hinzu
chat_history.append({
"role": "assistant",
"content": f"[Fehler bei Agent '{selected_agent['name']}']: {str(e)}"
})
else:
self._add_log(workflow_id, "Kein Agent ausgewählt. Beende Workflow.", "warning")
workflow_complete = True
# Keine Dateianfragen mehr - Agent ist fertig
agent_processing_complete = True
# Füge die endgültige Antwort des Agenten zum Chatverlauf hinzu
chat_history.append({
"role": "assistant",
"content": agent_text
})
# Agent-Ergebnis erstellen
result = results.create_agent_result(
workflow_id,
selected_agent,
len(self.workflows[workflow_id]["results"]),
prompt,
file_contexts,
agent_text,
self.mandate_id,
self.user_id
)
self.workflows[workflow_id]["results"].append(result)
self._add_log(
workflow_id,
f"Agent '{selected_agent['name']}' hat die Verarbeitung abgeschlossen",
"complete",
selected_agent_id,
selected_agent["name"]
)
# Agenten-Status aktualisieren
self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "completed"
except Exception as e:
logger.error(f"Fehler bei der Ausführung von Agent '{selected_agent['name']}': {str(e)}")
self._add_log(
workflow_id,
f"Fehler bei der Ausführung: {str(e)}",
"error",
selected_agent_id,
selected_agent["name"]
)
self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "failed"
# Füge die Fehlermeldung zum Chatverlauf hinzu
chat_history.append({
"role": "assistant",
"content": f"[Fehler bei Agent '{selected_agent['name']}']: {str(e)}"
})
# Fortschritt aktualisieren
self.workflows[workflow_id]["progress"] = min(0.9, 0.1 + (current_round / max_rounds) * 0.8)
@ -526,68 +403,10 @@ class AgentService:
self.workflows[workflow_id]["completed_at"] = datetime.now().isoformat()
# Speichere Ergebnisse in Datei für spätere Verwendung
self._save_workflow_results(workflow_id)
results.save_workflow_results(self.workflows, workflow_id, self.results_dir)
return workflow_id
def _get_agent_instructions(self, agent_type: str) -> str:
"""
Gibt agententypspezifische Anweisungen zurück, die aus der agents.json geladen werden.
Falls die Datei nicht existiert oder der Agententyp nicht gefunden wird,
wird ein Standardagent zurückgegeben.
"""
try:
# Pfad zur agents.json-Datei
agents_file = os.path.join(os.path.dirname(__file__), 'data', 'agents.json')
# Überprüfen, ob die Datei existiert
if not os.path.exists(agents_file):
logger.warning(f"Agents-Definitionen nicht gefunden: {agents_file}")
return self._get_default_agent_instructions()
# Datei lesen
with open(agents_file, 'r', encoding='utf-8') as f:
agents_data = json.load(f)
# Nach dem Agententyp suchen
for agent in agents_data:
if agent.get("type") == agent_type:
# Anweisungen zurückgeben, wenn vorhanden
instructions = agent.get("instructions")
if instructions:
logger.debug(f"Anweisungen für Agent-Typ '{agent_type}' aus agents.json geladen")
return instructions
# Wenn kein passender Agent gefunden wurde, Standardanweisungen verwenden
logger.warning(f"Keine Anweisungen für Agent-Typ '{agent_type}' in agents.json gefunden")
return self._get_default_agent_instructions()
except Exception as e:
logger.error(f"Fehler beim Laden der Agent-Anweisungen aus agents.json: {e}")
return self._get_default_agent_instructions()
def _get_default_agent_instructions(self) -> str:
"""
Gibt Standard-Anweisungen für einen Agenten zurück,
wenn keine spezifischen Anweisungen in der agents.json gefunden wurden.
Diese Funktion gibt generische Anweisungen zurück, unabhängig vom Agententyp.
"""
return """
Als Agent ist es deine Aufgabe, Anfragen zu analysieren und entsprechend deinen Fähigkeiten zu bearbeiten.
Folge diesen allgemeinen Anweisungen:
1. Verstehe die Anfrage gründlich
2. Analysiere relevante Daten und Informationen
3. Liefere präzise und hilfreiche Antworten
4. Strukturiere deine Antwort klar und verständlich
In deiner Antwort:
- Beginne mit einer Zusammenfassung der Anfrage
- Gib gut begründete Antworten oder Empfehlungen
- Führe wichtige Erkenntnisse klar auf
- Schließe mit konkreten nächsten Schritten oder Empfehlungen ab
"""
def _add_log(
self,
workflow_id: str,
@ -597,146 +416,70 @@ class AgentService:
agent_name: Optional[str] = None
) -> None:
"""Fügt einen Protokolleintrag zum Workflow hinzu"""
log_entry = {
"id": f"log_{uuid.uuid4()}",
"mandate_id": self.mandate_id,
"user_id": self.user_id,
"message": message,
"type": log_type,
"timestamp": datetime.now().isoformat(),
"agent_id": agent_id,
"agent_name": agent_name
}
workflow = self.workflows.get(workflow_id)
if workflow:
workflow["logs"].append(log_entry)
logger.info(f"Workflow {workflow_id}: {message}")
def _create_agent_result(
self,
workflow_id: str,
agent: Dict[str, Any],
index: int,
prompt: str,
file_contexts: List[Dict[str, Any]],
content: str
) -> Dict[str, Any]:
"""Erstellt ein Ergebnisobjekt basierend auf dem Agententyp und der API-Antwort"""
agent_type = agent["type"]
agent_id = agent["id"]
agent_name = agent["name"]
# Grundlegende Ergebnisstruktur
result = {
"id": f"result_{workflow_id}_{index}",
"mandate_id": self.mandate_id,
"user_id": self.user_id,
"agent_id": agent_id,
"agent_name": agent_name,
"timestamp": datetime.now().isoformat(),
"type": "text", # Standardtyp
"metadata": {
"files_processed": [file["name"] for file in file_contexts],
"prompt": prompt
}
}
# Titel und Inhalt basierend auf dem Agententyp anpassen
if agent_type == "analyzer":
result.update({
"title": "Datenanalyse-Ergebnis",
"content": content,
})
elif agent_type == "visualizer":
result.update({
"title": "Visualisierungsvorschlag",
"content": content,
"type": "chart" # Auch wenn kein echtes Diagramm, markieren wir es als solches
})
elif agent_type == "writer":
result.update({
"title": "Zusammenfassung und Empfehlungen",
"content": content,
})
elif agent_type == "scraper":
result.update({
"title": "Web-Recherche Ergebnisse",
"content": content,
})
elif agent_type == "initialisierung":
result.update({
"title": "Direkte Antwort",
"content": content,
})
elif agent_type == "organisator":
result.update({
"title": "Aufgabenstrukturierung",
"content": content,
})
elif agent_type == "entwickler":
result.update({
"title": "Code und Ausführungsergebnisse",
"content": content,
})
else:
result.update({
"title": f"Ergebnis von {agent_name}",
"content": content,
})
return result
def _save_workflow_results(self, workflow_id: str) -> None:
"""Speichert die Workflow-Ergebnisse in einer Datei"""
workflow = self.workflows.get(workflow_id)
if workflow:
try:
file_path = os.path.join(self.results_dir, f"workflow_{workflow_id}.json")
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(workflow, f, indent=2, ensure_ascii=False)
logger.info(f"Workflow-Ergebnisse gespeichert: {file_path}")
except Exception as e:
logger.error(f"Fehler beim Speichern der Workflow-Ergebnisse: {e}")
results.add_log(
self.workflows,
workflow_id,
message,
log_type,
agent_id,
agent_name,
self.mandate_id,
self.user_id
)
def get_workflow_status(self, workflow_id: str) -> Optional[Dict[str, Any]]:
"""Gibt den Status eines Workflows zurück"""
workflow = self.workflows.get(workflow_id)
if not workflow:
return None
return {
"id": workflow["id"],
"mandate_id": workflow.get("mandate_id"),
"user_id": workflow.get("user_id"),
"status": workflow["status"],
"progress": workflow["progress"],
"started_at": workflow["started_at"],
"completed_at": workflow["completed_at"],
"agent_statuses": workflow["agent_statuses"]
}
return results.get_workflow_status(self.workflows, workflow_id)
def get_workflow_logs(self, workflow_id: str) -> Optional[List[Dict[str, Any]]]:
"""Gibt die Protokolle eines Workflows zurück"""
workflow = self.workflows.get(workflow_id)
if not workflow:
return None
return workflow["logs"]
return results.get_workflow_logs(self.workflows, workflow_id)
def get_workflow_results(self, workflow_id: str) -> Optional[List[Dict[str, Any]]]:
"""Gibt die Ergebnisse eines Workflows zurück"""
workflow = self.workflows.get(workflow_id)
if not workflow:
return None
return workflow["results"]
return results.get_workflow_results(self.workflows, workflow_id)
async def close(self):
"""Schließt die HTTP-Clients beim Beenden der Anwendung"""
await self.service_aichat.close()
await self.service_aiscrap.close()
def stop_workflow(self, workflow_id: str) -> bool:
"""
Stoppt einen laufenden Workflow.
Args:
workflow_id: ID des zu stoppenden Workflows
Returns:
bool: True, wenn der Workflow erfolgreich gestoppt wurde, False wenn nicht gefunden
"""
logger.info(f"Stoppe Workflow {workflow_id}")
if workflow_id not in self.workflows:
logger.warning(f"Workflow {workflow_id} nicht gefunden")
return False
# Prüfen, ob der Workflow bereits beendet ist
current_status = self.workflows[workflow_id]["status"]
if current_status not in ["running", "pending"]:
logger.info(f"Workflow {workflow_id} ist bereits im Status {current_status}")
return False
# Workflow-Status auf "stopped" setzen
self.workflows[workflow_id]["status"] = "stopped"
self.workflows["progress"] = 1.0
self.workflows[workflow_id]["completed_at"] = datetime.now().isoformat()
# Log-Eintrag für das Stoppen hinzufügen
self._add_log(workflow_id, "Workflow manuell gestoppt", "warning")
# Ergebnisse speichern
results.save_workflow_results(self.workflows, workflow_id, self.results_dir)
logger.info(f"Workflow {workflow_id} wurde gestoppt")
return True
# Singleton-Factory für AgentService-Instanzen pro Kontext
_agent_service_instances = {}

View file

@ -0,0 +1,230 @@
import os
import json
import logging
from typing import Dict, Any, List, Optional
# Logger konfigurieren
logger = logging.getLogger(__name__)
def get_agent_instructions(agent_type: str) -> str:
"""
Gibt agententypspezifische Anweisungen zurück, die aus der agents.json geladen werden.
Erweitert um Hinweise zur Dateianforderung.
Args:
agent_type: Typ des Agenten, für den Anweisungen geladen werden sollen
Returns:
Die geladenen oder Standard-Anweisungen mit Dateizugriffsinformationen
"""
try:
# Pfad zur agents.json-Datei
agents_file = os.path.join(os.path.dirname(__file__), 'data', 'agents.json')
# Überprüfen, ob die Datei existiert
if not os.path.exists(agents_file):
logger.warning(f"Agents-Definitionen nicht gefunden: {agents_file}")
instructions = get_default_agent_instructions()
else:
# Datei lesen
with open(agents_file, 'r', encoding='utf-8') as f:
agents_data = json.load(f)
# Nach dem Agententyp suchen
instructions = None
for agent in agents_data:
if agent.get("type") == agent_type:
# Anweisungen zurückgeben, wenn vorhanden
instructions = agent.get("instructions")
if instructions:
logger.debug(f"Anweisungen für Agent-Typ '{agent_type}' aus agents.json geladen")
break
# Wenn kein passender Agent gefunden wurde, Standardanweisungen verwenden
if not instructions:
logger.warning(f"Keine Anweisungen für Agent-Typ '{agent_type}' in agents.json gefunden")
instructions = get_default_agent_instructions()
# Füge Hinweise zur Dateianforderung hinzu
file_access_instructions = """
# Weitere Dateiinhalte anfordern
Falls du mehr Details aus einer Datei benötigst, kannst du zusätzliche Dateiinhalte mit folgendem Befehl anfordern:
[[FILE:load_file(file_id=ID, complete=True/False, start=N, end=M, pages=[1,2,3])]]
Parameter:
- file_id: Die ID der Datei (erforderlich)
- complete: Wenn 'true', wird die gesamte Datei geladen
- start, end: Startet und Endposition (in Zeichen) für Textdateien
- pages: Liste von Seitennummern für PDFs (z.B. [1,3,5])
Beispiele:
[[FILE:load_file(file_id="doc1", complete=true)]]
[[FILE:load_file(file_id="doc2", pages=[1,2,3])]]
Der angeforderte Dateiinhalt wird dir als Antwort bereitgestellt, bevor du deine Analyse fortsetzen kannst.
"""
return instructions + file_access_instructions
except Exception as e:
logger.error(f"Fehler beim Laden der Agent-Anweisungen aus agents.json: {e}")
return get_default_agent_instructions()
def get_default_agent_instructions() -> str:
"""
Gibt Standard-Anweisungen für einen Agenten zurück,
wenn keine spezifischen Anweisungen in der agents.json gefunden wurden.
Diese Funktion gibt generische Anweisungen zurück, unabhängig vom Agententyp.
"""
return """
Als Agent ist es deine Aufgabe, Anfragen zu analysieren und entsprechend deinen Fähigkeiten zu bearbeiten.
Folge diesen allgemeinen Anweisungen:
1. Verstehe die Anfrage gründlich
2. Analysiere relevante Daten und Informationen
3. Liefere präzise und hilfreiche Antworten
4. Strukturiere deine Antwort klar und verständlich
In deiner Antwort:
- Beginne mit einer Zusammenfassung der Anfrage
- Gib gut begründete Antworten oder Empfehlungen
- Führe wichtige Erkenntnisse klar auf
- Schließe mit konkreten nächsten Schritten oder Empfehlungen ab
"""
def initialize_agents(agents: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
"""
Initialisiert die Agenten mit ihren Fähigkeiten und Status
Args:
agents: Liste der Agenten aus dem Workflow
Returns:
Dictionary mit Agent-IDs als Schlüssel und Agent-Informationen
"""
available_agents = {}
for agent in agents:
agent_id = agent["id"]
agent_name = agent["name"]
agent_type = agent["type"]
agent_capabilities = agent.get("capabilities", "")
available_agents[agent_id] = {
"id": agent_id,
"name": agent_name,
"type": agent_type,
"capabilities": agent_capabilities,
"used": False
}
return available_agents
def get_moderator_prompt(available_agents: Dict[str, Dict[str, Any]]) -> str:
"""
Erstellt den Prompt für den Moderator des Multi-Agent-Systems
Args:
available_agents: Dictionary mit verfügbaren Agenten
Returns:
Der vollständige Moderator-Prompt
"""
# Basis-Prompt für den Moderator
moderator_prompt_base = """
Du bist der Moderator eines Multi-Agent-Systems. Deine Aufgabe ist es, die Zusammenarbeit zwischen verschiedenen spezialisierten Agenten zu koordinieren, um die Anfrage des Benutzers bestmöglich zu erfüllen.
Du sollst:
1. Die Anfrage des Benutzers verstehen und analysieren
2. Den am besten geeigneten Agenten basierend auf seinen Fähigkeiten auswählen
3. Die Antworten der Agenten überwachen und bewerten
4. Falls nötig, weitere Agenten hinzuziehen, um die Anfrage vollständig zu bearbeiten
5. Den Workflow beenden, wenn die Anfrage vollständig erfüllt wurde
Für jeden Schritt sollst du begründen, warum du einen bestimmten Agenten auswählst, und zusammenfassen, was bisher erreicht wurde.
"""
# Dynamischer Teil - Verfügbare Agenten aus den tatsächlich vorhandenen Agenten
agents_description = "Verfügbare Agenten:\n"
for agent_id, agent in available_agents.items():
agents_description += f"- {agent['name']} (Typ: {agent['type']}): {agent['capabilities']}\n"
moderator_prompt_end = """
Beende den Workflow, wenn die Aufgabe erfüllt ist oder keine weiteren Agenten zur Bearbeitung beitragen können.
"""
# Kombiniere alle Teile
return moderator_prompt_base + "\n" + agents_description + "\n" + moderator_prompt_end
def create_agent_prompt(agent: Dict[str, Any], agent_instructions: str) -> Dict[str, str]:
"""
Erstellt den Prompt für einen spezifischen Agenten
Args:
agent: Agent-Informationen
agent_instructions: Anweisungen für den Agententyp
Returns:
Der formatierte Agent-Prompt als Dictionary
"""
return {
"role": "system",
"content": f"""
# Aufgabe
Du bist ein spezialisierter Agent vom Typ {agent['type']} mit dem Namen {agent['name']}.
{agent_instructions}
Bitte analysiere den Chatverlauf und die Dateien und beantworte die Anfrage gemäß deiner Rolle.
Ausgabeformat:
[Agent: {agent['name']}]
Deine Antwort...
"""
}
def find_next_agent(moderator_text: str, available_agents: Dict[str, Dict[str, Any]]) -> Optional[str]:
"""
Findet den vom Moderator ausgewählten Agenten anhand des Moderator-Textes
Args:
moderator_text: Die Antwort des Moderators
available_agents: Dictionary mit verfügbaren Agenten
Returns:
Die ID des ausgewählten Agenten oder None, wenn kein Agent gefunden wurde
"""
# Prüfe, ob der Workflow beendet werden soll
if any(phrase in moderator_text.lower() for phrase in [
"workflow beenden", "aufgabe erfüllt", "beende den workflow", "workflow abschließen"
]):
return "WORKFLOW_COMPLETE"
# Versuche, den ausgewählten Agenten aus dem Text zu extrahieren
for agent_id, agent in available_agents.items():
if agent["name"] in moderator_text or f"Agent {agent_id}" in moderator_text:
return agent_id
# Keine direkte Erwähnung gefunden - wähle den ersten nicht verwendeten Agenten
for agent_id, agent in available_agents.items():
if not agent["used"]:
return agent_id
# Wenn alle Agenten bereits verwendet wurden, wähle den Initialisierungs-Agenten
for agent_id, agent in available_agents.items():
if agent["type"] == "initialisierung":
return agent_id
# Als letztes Mittel wähle einfach den ersten Agenten
if available_agents:
return list(available_agents.keys())[0]
# Keine Agenten vorhanden oder keine Auswahl möglich
return None

View file

@ -0,0 +1,437 @@
import os
import logging
import pandas as pd
from typing import Dict, Any, List, Optional, Tuple
# Logger konfigurieren
logger = logging.getLogger(__name__)
def read_file_contents(
file_contexts: List[Dict[str, Any]],
upload_dir: str,
workflow_id: str = None,
add_log_func = None
) -> Dict[str, str]:
"""
Liest die Inhalte aller Dateien und bereitet sie für die Verwendung vor.
Args:
file_contexts: Liste der Dateikontexte mit Metadaten
upload_dir: Verzeichnis für Uploads
workflow_id: Optional ID des Workflows für Logging
add_log_func: Optionale Funktion zum Hinzufügen von Logs
Returns:
Dictionary mit Dateiinhalten (file_id -> Inhalt)
"""
file_contents = {}
for file in file_contexts:
file_id = file["id"]
file_name = file["name"]
file_type = file["type"]
file_path = file.get("path", "")
# Wenn Pfad nicht gesetzt, versuche ihn aus dem Upload-Verzeichnis abzuleiten
if not file_path and file_name:
possible_path = os.path.join(upload_dir, file_name)
if os.path.exists(possible_path):
file_path = possible_path
logger.debug(f"Pfad für Datei {file_name} gefunden: {file_path}")
# Dateiinhalt lesen, wenn der Pfad verfügbar ist
if file_path and os.path.exists(file_path):
try:
# Text-basierte Dateien direkt lesen
if file_type == "document":
# Einfache Textdateien
if file_name.endswith(('.txt', '.csv', '.md', '.json')):
with open(file_path, 'r', encoding='utf-8') as f:
file_contents[file_id] = f.read()
_log(add_log_func, workflow_id, f"Datei {file_name} gelesen", "info")
# Excel-Dateien
elif file_name.endswith(('.xlsx', '.xls')):
try:
df = pd.read_excel(file_path)
file_contents[file_id] = f"Excel-Datei mit {len(df)} Zeilen und {len(df.columns)} Spalten.\n"
file_contents[file_id] += f"Spalten: {', '.join(df.columns.tolist())}\n"
file_contents[file_id] += "Erste 5 Zeilen:\n"
file_contents[file_id] += df.head(5).to_string()
_log(add_log_func, workflow_id, f"Excel-Datei {file_name} gelesen", "info")
except Exception as e:
_log(add_log_func, workflow_id, f"Fehler beim Lesen der Excel-Datei {file_name}: {str(e)}", "error")
# CSV-Dateien
elif file_name.endswith('.csv'):
try:
df = pd.read_csv(file_path)
file_contents[file_id] = f"CSV-Datei mit {len(df)} Zeilen und {len(df.columns)} Spalten.\n"
file_contents[file_id] += f"Spalten: {', '.join(df.columns.tolist())}\n"
file_contents[file_id] += "Erste 5 Zeilen:\n"
file_contents[file_id] += df.head(5).to_string()
_log(add_log_func, workflow_id, f"CSV-Datei {file_name} gelesen", "info")
except Exception as e:
_log(add_log_func, workflow_id, f"Fehler beim Lesen der CSV-Datei {file_name}: {str(e)}", "error")
# PDF-Dateien
elif file_name.endswith('.pdf'):
try:
# Falls PyPDF2 installiert ist
try:
from PyPDF2 import PdfReader
reader = PdfReader(file_path)
text = ""
for page in reader.pages:
text += page.extract_text() + "\n\n"
file_contents[file_id] = f"PDF mit {len(reader.pages)} Seiten.\nInhalt:\n{text[:2000]}..."
_log(add_log_func, workflow_id, f"PDF-Datei {file_name} gelesen", "info")
except ImportError:
_log(add_log_func, workflow_id,
"PyPDF2 nicht installiert. PDF-Inhalt kann nicht extrahiert werden.", "warning")
file_contents[file_id] = f"PDF-Datei (Inhalt nicht verfügbar, PyPDF2 fehlt)"
except Exception as e:
_log(add_log_func, workflow_id, f"Fehler beim Lesen der PDF-Datei {file_name}: {str(e)}", "error")
# Andere Dokumenttypen
else:
_log(add_log_func, workflow_id, f"Nicht unterstütztes Dokumentformat: {file_name}", "warning")
file_contents[file_id] = f"Dateiinhalt nicht verfügbar (Nicht unterstütztes Format)"
# Bilddateien werden nicht direkt gelesen, nur Metadaten gespeichert
elif file_type == "image":
file_contents[file_id] = f"Bilddatei: {file_name} (Inhalt nicht als Text verfügbar)"
except Exception as e:
logger.error(f"Fehler beim Lesen der Datei {file_name}: {str(e)}")
_log(add_log_func, workflow_id, f"Fehler beim Lesen der Datei {file_name}: {str(e)}", "error")
else:
if file_path:
_log(add_log_func, workflow_id, f"Datei {file_name} nicht gefunden: {file_path}", "warning")
else:
_log(add_log_func, workflow_id, f"Kein Pfad für Datei {file_name} verfügbar", "warning")
file_contents[file_id] = f"Dateiinhalt nicht verfügbar"
return file_contents
def load_additional_file_content(
workflow_id: str,
file_id: str,
file_contents: Dict[str, str],
file_contexts: List[Dict[str, Any]],
add_log_func = None,
read_complete: bool = False,
start_pos: int = None,
end_pos: int = None,
page_numbers: List[int] = None
) -> Optional[str]:
"""
Lädt zusätzliche Dateiinhalte für einen Agenten nach.
Args:
workflow_id: ID des aktuellen Workflows für Logging
file_id: ID der Datei, deren Inhalt nachgeladen werden soll
file_contents: Dictionary mit bereits geladenen Dateiinhalten
file_contexts: Liste der Dateikontexte mit Metadaten
add_log_func: Funktion zum Hinzufügen von Logs
read_complete: Wenn True, wird die gesamte Datei geladen
start_pos: Startposition für einen Teilauszug (nur für Textdateien)
end_pos: Endposition für einen Teilauszug (nur für Textdateien)
page_numbers: Liste von Seitennummern für PDFs (1-basiert)
Returns:
Der nachgeladene Dateiinhalt oder None bei Fehler
"""
# Finde Dateikontext zur gegebenen file_id
file_context = next((f for f in file_contexts if f.get("id") == file_id), None)
if not file_context:
_log(add_log_func, workflow_id, f"Datei mit ID {file_id} nicht gefunden", "error")
return None
file_name = file_context.get("name", "Unbekannte Datei")
file_path = file_context.get("path", "")
file_type = file_context.get("type", "")
# Prüfe, ob Dateipfad existiert
if not file_path or not os.path.exists(file_path):
_log(add_log_func, workflow_id, f"Dateipfad für {file_name} nicht gefunden", "error")
return None
try:
# Behandlung je nach Dateityp
if file_name.endswith(('.txt', '.csv', '.md', '.json')):
# Einfache Textdateien
with open(file_path, 'r', encoding='utf-8') as f:
if read_complete:
content = f.read()
_log(add_log_func, workflow_id, f"Vollständige Datei {file_name} nachgeladen", "info")
elif start_pos is not None and end_pos is not None:
f.seek(start_pos)
content = f.read(end_pos - start_pos)
_log(add_log_func, workflow_id, f"Teilinhalt von {file_name} nachgeladen (Pos {start_pos}-{end_pos})", "info")
else:
# Standardverhalten: Lade ersten Teil der Datei
content = f.read(10000) # Größerer Ausschnitt als ursprünglich
_log(add_log_func, workflow_id, f"Erweiterten Teilinhalt von {file_name} nachgeladen", "info")
return content
elif file_name.endswith(('.xlsx', '.xls')):
# Excel-Dateien
df = pd.read_excel(file_path)
if read_complete:
content = f"Excel-Datei mit {len(df)} Zeilen und {len(df.columns)} Spalten.\n"
content += f"Spalten: {', '.join(df.columns.tolist())}\n\n"
content += df.to_string() # Komplette Tabelle
_log(add_log_func, workflow_id, f"Vollständige Excel-Datei {file_name} nachgeladen", "info")
else:
# Erweiterte Vorschau (mehr Zeilen als ursprünglich)
rows_to_show = min(20, len(df)) # Zeige bis zu 20 Zeilen
content = f"Excel-Datei mit {len(df)} Zeilen und {len(df.columns)} Spalten.\n"
content += f"Spalten: {', '.join(df.columns.tolist())}\n\n"
content += f"Erste {rows_to_show} Zeilen:\n"
content += df.head(rows_to_show).to_string()
_log(add_log_func, workflow_id, f"Erweiterte Vorschau der Excel-Datei {file_name} nachgeladen", "info")
return content
elif file_name.endswith('.csv'):
# CSV-Dateien
df = pd.read_csv(file_path)
if read_complete:
content = f"CSV-Datei mit {len(df)} Zeilen und {len(df.columns)} Spalten.\n"
content += f"Spalten: {', '.join(df.columns.tolist())}\n\n"
content += df.to_string() # Komplette Tabelle
_log(add_log_func, workflow_id, f"Vollständige CSV-Datei {file_name} nachgeladen", "info")
else:
# Erweiterte Vorschau
rows_to_show = min(20, len(df)) # Zeige bis zu 20 Zeilen
content = f"CSV-Datei mit {len(df)} Zeilen und {len(df.columns)} Spalten.\n"
content += f"Spalten: {', '.join(df.columns.tolist())}\n\n"
content += f"Erste {rows_to_show} Zeilen:\n"
content += df.head(rows_to_show).to_string()
_log(add_log_func, workflow_id, f"Erweiterte Vorschau der CSV-Datei {file_name} nachgeladen", "info")
return content
elif file_name.endswith('.pdf'):
# PDF-Dateien
try:
from PyPDF2 import PdfReader
reader = PdfReader(file_path)
num_pages = len(reader.pages)
if read_complete:
# Komplette PDF lesen
text = ""
for page in reader.pages:
text += page.extract_text() + "\n\n"
content = f"PDF mit {num_pages} Seiten.\nVollständiger Inhalt:\n{text}"
_log(add_log_func, workflow_id, f"Vollständige PDF-Datei {file_name} nachgeladen", "info")
elif page_numbers:
# Spezifische Seiten lesen
text = ""
valid_pages = [p-1 for p in page_numbers if 0 < p <= num_pages] # 0-basierter Index
for page_idx in valid_pages:
text += f"--- Seite {page_idx + 1} ---\n"
text += reader.pages[page_idx].extract_text() + "\n\n"
content = f"PDF mit {num_pages} Seiten.\nAngeforderte Seiten ({', '.join(map(str, page_numbers))}):\n{text}"
_log(add_log_func, workflow_id, f"Spezifische Seiten von PDF {file_name} nachgeladen", "info")
else:
# Erweiterte Vorschau (mehr Inhalt als ursprünglich)
text = ""
pages_to_show = min(5, num_pages) # Zeige bis zu 5 Seiten
for i in range(pages_to_show):
text += f"--- Seite {i + 1} ---\n"
text += reader.pages[i].extract_text() + "\n\n"
content = f"PDF mit {num_pages} Seiten.\nInhalt der ersten {pages_to_show} Seiten:\n{text}"
_log(add_log_func, workflow_id, f"Erweiterte Vorschau der PDF-Datei {file_name} nachgeladen", "info")
return content
except ImportError:
_log(add_log_func, workflow_id, "PyPDF2 nicht installiert. PDF-Inhalt kann nicht extrahiert werden.", "warning")
return "PDF-Datei (Inhalt nicht verfügbar, PyPDF2 fehlt)"
else:
# Andere Dokumenttypen - versuche generische Textextraktion
try:
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
content = f.read(10000 if not read_complete else None)
_log(add_log_func, workflow_id, f"Datei {file_name} als Text nachgeladen", "info")
return content
except:
_log(add_log_func, workflow_id, f"Datei {file_name} konnte nicht als Text gelesen werden", "warning")
return f"Dateiinhalt von {file_name} kann nicht gelesen werden (Nicht unterstütztes Format)"
except Exception as e:
_log(add_log_func, workflow_id, f"Fehler beim Nachladen von {file_name}: {str(e)}", "error")
return f"Fehler beim Nachladen der Datei {file_name}: {str(e)}"
def parse_file_access_commands(agent_text: str) -> List[Dict[str, Any]]:
"""
Erkennt und parst Befehle zum Nachladen von Dateien im Text eines Agenten.
Die Befehle haben folgende Syntax:
[[FILE:load_file(file_id, complete=True/False, start=N, end=M, pages=[1,2,3])]]
Args:
agent_text: Der Text des Agenten
Returns:
Liste der erkannten und geparsten Dateizugriffsbefehle
"""
import re
# Muster für Dateizugriffsbefehle
pattern = r'\[\[FILE:load_file\(([^)]+)\)\]\]'
# Finde alle Treffer im Text
matches = re.findall(pattern, agent_text)
commands = []
for match in matches:
try:
# Parse die Parameter
command = {"complete": False, "start": None, "end": None, "pages": None}
# Teile nach Komma, aber beachte Listen wie [1,2,3]
params = []
param_str = ""
bracket_count = 0
for char in match + ',': # Komma am Ende hinzufügen für einfacheres Parsen
if char == ',' and bracket_count == 0:
params.append(param_str.strip())
param_str = ""
else:
param_str += char
if char == '[':
bracket_count += 1
elif char == ']':
bracket_count -= 1
# Verarbeite jeden Parameter
for param in params:
if '=' in param:
key, value = param.split('=', 1)
key = key.strip()
value = value.strip()
if key == 'file_id':
command['file_id'] = value.strip('"\'')
elif key == 'complete':
command['complete'] = value.lower() == 'true'
elif key == 'start':
command['start'] = int(value)
elif key == 'end':
command['end'] = int(value)
elif key == 'pages':
# Konvertiere String "[1,2,3]" zu Liste [1,2,3]
if value.startswith('[') and value.endswith(']'):
page_nums = []
for p in value[1:-1].split(','):
try:
page_nums.append(int(p.strip()))
except ValueError:
pass
command['pages'] = page_nums
else:
# Wenn nur file_id angegeben ist ohne key=value Format
command['file_id'] = param.strip('"\'')
# Nur hinzufügen, wenn file_id vorhanden ist
if 'file_id' in command:
commands.append(command)
except Exception as e:
logger.error(f"Fehler beim Parsen des Dateizugriffsbefehls: {str(e)}")
continue
return commands
def format_file_context_text(file_contexts: List[Dict[str, Any]], file_contents: Dict[str, str]) -> str:
"""
Erstellt eine formatierte Textdarstellung aller Dateien und ihrer Inhalte
Args:
file_contexts: Liste der Dateikontexte mit Metadaten
file_contents: Dictionary mit Dateiinhalten
Returns:
Formatierter Text mit Dateiliste und Inhaltsauszügen
"""
# Erstelle einen Kontext mit Dateiliste und Inhalten für leichteren Zugriff
file_context_text = "Verfügbare Dateien:\n" + "\n".join([
f"- {file['name']} ({file['type']}, {file['size']}, ID: {file['id']})"
for file in file_contexts
])
# Füge Dateiinhalte hinzu (mit Längenbegrenzung)
for file_id, content in file_contents.items():
file_name = next((f['name'] for f in file_contexts if f['id'] == file_id), "Unbekannte Datei")
file_context_text += f"\n\n==== DATEIINHALT: {file_name} (ID: {file_id}) ====\n"
# Begrenze den Inhalt, um Token-Limits zu respektieren
max_content_length = 5000 # Anpassen je nach Anzahl der Dateien und Umfang
if len(content) > max_content_length:
file_context_text += content[:max_content_length] + "...\n[Dateiinhalt gekürzt aus Platzgründen]"
else:
file_context_text += content
return file_context_text
def prepare_file_contexts(files: List[Dict[str, Any]], upload_dir: str) -> List[Dict[str, Any]]:
"""
Bereitet die Dateikontexte vor und ermittelt die vollen Dateipfade
Args:
files: Liste von Dateien mit Metadaten (Dict mit id, name, type)
upload_dir: Verzeichnis für Uploads
Returns:
Liste von Dateikontexten mit vollständigen Pfaden
"""
file_contexts = []
for file in files:
file_id = file["id"]
file_name = file["name"]
file_type = file["type"]
file_path = file.get("path", "")
# Wenn kein Pfad angegeben ist, versuche, ihn aus dem Upload-Verzeichnis abzuleiten
if not file_path and file_name:
possible_path = os.path.join(upload_dir, file_name)
if os.path.exists(possible_path):
file_path = possible_path
logger.debug(f"Pfad für Datei {file_name} gefunden: {file_path}")
file_contexts.append({
"id": file_id,
"name": file_name,
"type": file_type,
"size": file.get("size", "Unbekannt"),
"path": file_path
})
return file_contexts
def _log(add_log_func, workflow_id, message, log_type, agent_id=None, agent_name=None):
"""Hilfsfunktion zum Loggen mit unterschiedlichen Log-Funktionen"""
# Log über die Logger-Instanz
if log_type == "error":
logger.error(message)
elif log_type == "warning":
logger.warning(message)
else:
logger.info(message)
# Log über die bereitgestellte Log-Funktion (falls vorhanden)
if add_log_func and workflow_id:
add_log_func(workflow_id, message, log_type, agent_id, agent_name)

View file

@ -0,0 +1,221 @@
import os
import json
import logging
import uuid
from datetime import datetime
from typing import Dict, Any, List, Optional
# Logger konfigurieren
logger = logging.getLogger(__name__)
def create_agent_result(
workflow_id: str,
agent: Dict[str, Any],
index: int,
prompt: str,
file_contexts: List[Dict[str, Any]],
content: str,
mandate_id: int = None,
user_id: int = None
) -> Dict[str, Any]:
"""
Erstellt ein Ergebnisobjekt basierend auf dem Agententyp und der API-Antwort
Args:
workflow_id: ID des Workflows
agent: Agent-Informationen
index: Index des Ergebnisses
prompt: Ursprünglicher Prompt
file_contexts: Liste der Dateikontexte
content: Inhalt der Agent-Antwort
mandate_id: Optional ID des Mandanten
user_id: Optional ID des Benutzers
Returns:
Ein Ergebnisobjekt für den Workflow
"""
agent_type = agent["type"]
agent_id = agent["id"]
agent_name = agent["name"]
# Grundlegende Ergebnisstruktur
result = {
"id": f"result_{workflow_id}_{index}",
"mandate_id": mandate_id,
"user_id": user_id,
"agent_id": agent_id,
"agent_name": agent_name,
"timestamp": datetime.now().isoformat(),
"type": "text", # Standardtyp
"metadata": {
"files_processed": [file["name"] for file in file_contexts],
"prompt": prompt
}
}
# Titel und Inhalt basierend auf dem Agententyp anpassen
if agent_type == "analyzer":
result.update({
"title": "Datenanalyse-Ergebnis",
"content": content,
})
elif agent_type == "visualizer":
result.update({
"title": "Visualisierungsvorschlag",
"content": content,
"type": "chart" # Auch wenn kein echtes Diagramm, markieren wir es als solches
})
elif agent_type == "writer":
result.update({
"title": "Zusammenfassung und Empfehlungen",
"content": content,
})
elif agent_type == "scraper":
result.update({
"title": "Web-Recherche Ergebnisse",
"content": content,
})
elif agent_type == "initialisierung":
result.update({
"title": "Direkte Antwort",
"content": content,
})
elif agent_type == "organisator":
result.update({
"title": "Aufgabenstrukturierung",
"content": content,
})
elif agent_type == "entwickler":
result.update({
"title": "Code und Ausführungsergebnisse",
"content": content,
})
else:
result.update({
"title": f"Ergebnis von {agent_name}",
"content": content,
})
return result
def add_log(
workflows: Dict[str, Dict[str, Any]],
workflow_id: str,
message: str,
log_type: str,
agent_id: Optional[str] = None,
agent_name: Optional[str] = None,
mandate_id: int = None,
user_id: int = None
) -> None:
"""
Fügt einen Protokolleintrag zum Workflow hinzu
Args:
workflows: Dictionary mit Workflow-Informationen
workflow_id: ID des Workflows
message: Log-Nachricht
log_type: Typ des Logs (info, warning, error, etc.)
agent_id: Optional ID des Agenten
agent_name: Optional Name des Agenten
mandate_id: Optional ID des Mandanten
user_id: Optional ID des Benutzers
"""
log_entry = {
"id": f"log_{uuid.uuid4()}",
"mandate_id": mandate_id,
"user_id": user_id,
"message": message,
"type": log_type,
"timestamp": datetime.now().isoformat(),
"agent_id": agent_id,
"agent_name": agent_name
}
workflow = workflows.get(workflow_id)
if workflow:
workflow["logs"].append(log_entry)
logger.info(f"Workflow {workflow_id}: {message}")
else:
logger.warning(f"Versuch, einem nicht existierenden Workflow Logs hinzuzufügen: {workflow_id}")
def save_workflow_results(workflows: Dict[str, Dict[str, Any]], workflow_id: str, results_dir: str) -> None:
"""
Speichert die Workflow-Ergebnisse in einer Datei
Args:
workflows: Dictionary mit Workflow-Informationen
workflow_id: ID des Workflows
results_dir: Verzeichnis für Ergebnisse
"""
workflow = workflows.get(workflow_id)
if workflow:
try:
file_path = os.path.join(results_dir, f"workflow_{workflow_id}.json")
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(workflow, f, indent=2, ensure_ascii=False)
logger.info(f"Workflow-Ergebnisse gespeichert: {file_path}")
except Exception as e:
logger.error(f"Fehler beim Speichern der Workflow-Ergebnisse: {e}")
else:
logger.error(f"Workflow nicht gefunden: {workflow_id}")
def get_workflow_status(workflows: Dict[str, Dict[str, Any]], workflow_id: str) -> Optional[Dict[str, Any]]:
"""
Gibt den Status eines Workflows zurück
Args:
workflows: Dictionary mit Workflow-Informationen
workflow_id: ID des Workflows
Returns:
Ein Dictionary mit Statusinformationen oder None, wenn der Workflow nicht existiert
"""
workflow = workflows.get(workflow_id)
if not workflow:
return None
return {
"id": workflow["id"],
"mandate_id": workflow.get("mandate_id"),
"user_id": workflow.get("user_id"),
"status": workflow["status"],
"progress": workflow["progress"],
"started_at": workflow["started_at"],
"completed_at": workflow["completed_at"],
"agent_statuses": workflow["agent_statuses"]
}
def get_workflow_logs(workflows: Dict[str, Dict[str, Any]], workflow_id: str) -> Optional[List[Dict[str, Any]]]:
"""
Gibt die Protokolle eines Workflows zurück
Args:
workflows: Dictionary mit Workflow-Informationen
workflow_id: ID des Workflows
Returns:
Eine Liste mit Logs oder None, wenn der Workflow nicht existiert
"""
workflow = workflows.get(workflow_id)
if not workflow:
return None
return workflow["logs"]
def get_workflow_results(workflows: Dict[str, Dict[str, Any]], workflow_id: str) -> Optional[List[Dict[str, Any]]]:
"""
Gibt die Ergebnisse eines Workflows zurück
Args:
workflows: Dictionary mit Workflow-Informationen
workflow_id: ID des Workflows
Returns:
Eine Liste mit Ergebnissen oder None, wenn der Workflow nicht existiert
"""
workflow = workflows.get(workflow_id)
if not workflow:
return None
return workflow["results"]

View file

@ -0,0 +1,285 @@
import logging
from typing import Dict, Any, List
# Logger konfigurieren
logger = logging.getLogger(__name__)
def convert_to_anthropic_format(openai_messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Konvertiert Nachrichten vom OpenAI-Format ins Anthropic-Format.
OpenAI verwendet:
[{"role": "system", "content": "..."},
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."}]
Anthropic verwendet:
[{"role": "user", "content": [{"type": "text", "text": "..."}]},
{"role": "assistant", "content": [{"type": "text", "text": "..."}]}]
Anmerkung: Anthropic hat kein direktes System-Message-Äquivalent,
daher fügen wir System-Nachrichten in die erste User-Nachricht ein.
"""
anthropic_messages = []
system_content = ""
# Extrahiere zuerst alle System-Nachrichten
for msg in openai_messages:
if msg.get("role") == "system":
if isinstance(msg.get("content"), str):
system_content += msg.get("content", "") + "\n\n"
# Falls content bereits ein Array ist (selten bei system messages)
elif isinstance(msg.get("content"), list):
for part in msg.get("content", []):
if part.get("type") == "text":
system_content += part.get("text", "") + "\n\n"
# Konvertiere die restlichen Nachrichten
for i, msg in enumerate(openai_messages):
role = msg.get("role")
content = msg.get("content", "")
# System-Nachrichten überspringen (bereits extrahiert)
if role == "system":
continue
# Anthropic unterstützt nur "user" und "assistant" als Rollen
if role not in ["user", "assistant"]:
role = "user"
# Standardisiertes Nachrichtenformat erstellen
anthropic_msg = {"role": role}
# Content-Formatierung
if isinstance(content, str):
# String in ein Anthropic-kompatibles Array umwandeln
# Für die erste User-Nachricht: System-Inhalte voranstellen, falls vorhanden
if role == "user" and system_content and not any(m.get("role") == "user" for m in anthropic_messages):
text_content = system_content + content
else:
text_content = content
anthropic_msg["content"] = [{"type": "text", "text": text_content}]
elif isinstance(content, list):
# Bei Array-Content (multimodal)
transformed_content = []
# Für die erste User-Nachricht: System-Inhalte dem ersten Text-Element voranstellen
if role == "user" and system_content and not any(m.get("role") == "user" for m in anthropic_messages):
first_text_added = False
for part in content:
if part.get("type") == "text" and not first_text_added:
transformed_content.append({
"type": "text",
"text": system_content + part.get("text", "")
})
first_text_added = True
elif part.get("type") == "image_url":
# OpenAI image_url in Anthropic image umwandeln
image_url = part.get("image_url", {}).get("url", "")
if image_url.startswith("data:"):
# Base64-kodiertes Bild
parts = image_url.split(",", 1)
if len(parts) == 2:
media_type = parts[0].split(":")[1].split(";")[0]
base64_data = parts[1]
transformed_content.append({
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": base64_data
}
})
else:
# URL-Bild - nicht direkt unterstützt von Anthropic in dieser Form
logger.warning("Externe Bild-URLs werden von Anthropic nicht direkt unterstützt")
else:
transformed_content.append(part)
# Falls kein Text-Element gefunden wurde, füge System-Content als separates Element hinzu
if system_content and not first_text_added:
transformed_content.insert(0, {"type": "text", "text": system_content})
else:
# Wenn es nicht die erste User-Nachricht ist oder kein System-Content vorhanden ist
for part in content:
if part.get("type") == "image_url":
# OpenAI image_url in Anthropic image umwandeln
image_url = part.get("image_url", {}).get("url", "")
if image_url.startswith("data:"):
# Base64-kodiertes Bild
parts = image_url.split(",", 1)
if len(parts) == 2:
media_type = parts[0].split(":")[1].split(";")[0]
base64_data = parts[1]
transformed_content.append({
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": base64_data
}
})
else:
# URL-Bild - nicht direkt unterstützt von Anthropic in dieser Form
logger.warning("Externe Bild-URLs werden von Anthropic nicht direkt unterstützt")
else:
transformed_content.append(part)
anthropic_msg["content"] = transformed_content
anthropic_messages.append(anthropic_msg)
return anthropic_messages
def convert_to_openai_format(anthropic_messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Konvertiert Nachrichten vom Anthropic-Format ins OpenAI-Format.
Anthropic:
[{"role": "user", "content": [{"type": "text", "text": "..."}]},
{"role": "assistant", "content": [{"type": "text", "text": "..."}]}]
OpenAI:
[{"role": "system", "content": "..."},
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."}]
"""
openai_messages = []
for msg in anthropic_messages:
role = msg.get("role", "user")
content = msg.get("content", [])
# Erstelle OpenAI-Message
openai_msg = {"role": role}
# Content-Behandlung
if isinstance(content, list):
# Multimodaler Inhalt von Anthropic
if all(isinstance(part, dict) and part.get("type") == "text" for part in content):
# Wenn alle Elemente Text sind, vereinfache zu einem einzelnen String
openai_msg["content"] = "\n".join(part.get("text", "") for part in content if part.get("type") == "text")
else:
# Mischung aus Text und Bildern/Dokumenten
openai_content = []
for part in content:
part_type = part.get("type", "")
if part_type == "text":
openai_content.append({
"type": "text",
"text": part.get("text", "")
})
elif part_type == "image":
# Anthropic image in OpenAI image_url umwandeln
source = part.get("source", {})
if source.get("type") == "base64":
media_type = source.get("media_type", "image/jpeg")
base64_data = source.get("data", "")
openai_content.append({
"type": "image_url",
"image_url": {
"url": f"data:{media_type};base64,{base64_data}"
}
})
# Anthropic Dokumente können nicht direkt in OpenAI-Format übersetzt werden
elif part_type == "document":
# Versuche, Dokumente als Text zu behandeln
openai_content.append({
"type": "text",
"text": f"[Dokument wurde übermittelt, kann aber nicht direkt in OpenAI-Format konvertiert werden]"
})
openai_msg["content"] = openai_content
else:
# Einfacher String-Inhalt
openai_msg["content"] = content
openai_messages.append(openai_msg)
return openai_messages
def convert_anthropic_response_to_openai_format(anthropic_response: Dict[str, Any]) -> Dict[str, Any]:
"""
Konvertiert eine Antwort vom Anthropic-Format ins OpenAI-Format.
Args:
anthropic_response: Antwort im Anthropic-Format
Returns:
Die Antwort im OpenAI-Format
"""
# Extrahiere Inhalt aus Anthropic-Antwort
content = ""
if "content" in anthropic_response:
if isinstance(anthropic_response["content"], list):
# Inhalt ist eine Liste von Teilen (bei neueren API-Versionen)
for part in anthropic_response["content"]:
if part.get("type") == "text":
content += part.get("text", "")
else:
# Direkter Inhalt als String (bei älteren API-Versionen)
content = anthropic_response["content"]
# Erstelle OpenAI-formatierte Antwort
return {
"id": anthropic_response.get("id", ""),
"object": "chat.completion",
"created": anthropic_response.get("created", 0),
"model": anthropic_response.get("model", ""),
"choices": [
{
"message": {
"role": "assistant",
"content": content
},
"index": 0,
"finish_reason": "stop"
}
]
}
def convert_openai_response_to_anthropic_format(openai_response: Dict[str, Any]) -> Dict[str, Any]:
"""
Konvertiert eine Antwort vom OpenAI-Format ins Anthropic-Format.
Args:
openai_response: Antwort im OpenAI-Format
Returns:
Die Antwort im Anthropic-Format (nur die relevanten Felder)
"""
# Extrahiere Inhalt aus OpenAI-Antwort
content = []
if "choices" in openai_response and openai_response["choices"]:
choice = openai_response["choices"][0]
message = choice.get("message", {})
message_content = message.get("content", "")
if isinstance(message_content, str):
content.append({
"type": "text",
"text": message_content
})
elif isinstance(message_content, list):
# Multimodaler Inhalt (selten in Antworten)
for part in message_content:
if part.get("type") == "text":
content.append({
"type": "text",
"text": part.get("text", "")
})
# Bilder in Antworten würden hier auch verarbeitet werden
# Erstelle Anthropic-formatierte Antwort
return {
"id": openai_response.get("id", ""),
"model": openai_response.get("model", ""),
"content": content,
"type": "message",
"role": "assistant"
}

View file

@ -127,9 +127,12 @@ class Workspace(BaseModel):
class WorkflowRequest(BaseModel):
"""Anforderung zur Ausführung eines Workflows"""
id: int = Field(description="Eindeutige ID der Anforderung")
mandate_id: int = Field(description="ID des zugehörigen Mandanten")
user_id: int = Field(description="ID des Erstellers")
# Make these fields optional since they'll be populated from user context
id: Optional[int] = Field(None, description="Eindeutige ID der Anforderung")
mandate_id: Optional[int] = Field(None, description="ID des zugehörigen Mandanten")
user_id: Optional[int] = Field(None, description="ID des Erstellers")
# Keep these fields required
workspace_id: int = Field(description="ID des zugehörigen Workspaces")
prompt: str = Field(description="Zu verwendender Prompt")
agents: List[int] = Field(description="Liste von Agent-IDs")

View file

@ -29,6 +29,11 @@ async def run_workflow(
"""Führt einen Workflow mit den ausgewählten Agenten und Dateien aus"""
mandate_id, user_id = await get_user_context(current_user)
# Populate the fields from user context
workflow_request.mandate_id = mandate_id
workflow_request.user_id = user_id
workflow_request.id = str(uuid.uuid4()) # Generate a unique ID
# LucyDOM-Interface mit Benutzerkontext initialisieren
lucy_interface = get_lucydom_interface(mandate_id, user_id)
@ -145,4 +150,28 @@ async def get_workflow_results(
detail=f"Workflow mit ID {workflow_id} nicht gefunden"
)
return results
return results
@router.post("/{workflow_id}/stop", response_model=Dict[str, Any])
async def stop_workflow(
workflow_id: str = Path(..., description="ID des zu stoppenden Workflows"),
current_user: Dict[str, Any] = Depends(get_current_active_user)
):
"""Stoppt einen laufenden Workflow"""
mandate_id, user_id = await get_user_context(current_user)
# AgentService mit Benutzerkontext initialisieren
agent_service = get_agentservice_interface(mandate_id, user_id)
result = agent_service.stop_workflow(workflow_id)
if not result:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Workflow mit ID {workflow_id} nicht gefunden oder bereits beendet"
)
return {
"workflow_id": workflow_id,
"status": "stopped",
"message": "Workflow wurde gestoppt"
}

View file

@ -0,0 +1,115 @@
import asyncio
import sys
import os
import json
import uuid
from pprint import pprint
# Stelle sicher, dass das Verzeichnis mit den Modulen im Pfad ist
sys.path.append(os.path.abspath(os.path.dirname(__file__)))
# Importiere das Hauptmodul
import modules.agentservice_interface as agi
async def test_agentservice():
"""
Einfacher Test für den AgentService mit Beispieldaten.
Führt einen Workflow mit einem einfachen Text-Prompt und optionalen Dateien aus.
"""
print("Starte Test des AgentService...")
# Erstelle einen neuen AgentService
service = agi.get_agentservice_interface(mandate_id=1, user_id=1)
# Erstelle eine Test-Workflow-ID
workflow_id = f"test_{uuid.uuid4()}"
# Beispiel-Prompt
prompt = "Erstelle eine Zusammenfassung der wichtigsten Punkte aus den verfügbaren Dateien."
# Beispiel-Agenten
agents = [
{
"id": "analyzer_1",
"name": "Datenanalyst",
"type": "analyzer",
"capabilities": "Analysiert Daten, erstellt Statistiken und erkennt Muster."
},
{
"id": "writer_1",
"name": "Zusammenfasser",
"type": "writer",
"capabilities": "Erstellt klare, prägnante Zusammenfassungen komplexer Inhalte."
},
{
"id": "init_1",
"name": "Initialisierer",
"type": "initialisierung",
"capabilities": "Gibt einen ersten Überblick und strukturiert die Aufgabe."
}
]
# Optional: Beispiel-Dateien
# Wenn du Dateien testen möchtest, passe diese Pfade an und setze use_files=True
files = []
use_files = False
if use_files:
files = [
{
"id": "text_1",
"name": "beispiel.txt",
"type": "document",
"path": "pfad/zu/beispiel.txt", # Passe diesen Pfad an
"size": "1KB"
},
# Weitere Dateien hier hinzufügen
]
# Führe den Workflow aus
try:
print(f"Starte Workflow {workflow_id}...")
result_workflow_id = await service.execute_workflow(workflow_id, prompt, agents, files)
print(f"Workflow {result_workflow_id} gestartet.")
# Warte und hole regelmäßig den Status ab
MAX_CHECKS = 30
check_interval = 2 # Sekunden
for i in range(MAX_CHECKS):
status = service.get_workflow_status(workflow_id)
print(f"Status nach {i*check_interval}s: {status['status']}, Fortschritt: {status['progress']*100:.1f}%")
if status['status'] in ['completed', 'failed']:
print("Workflow beendet!")
break
await asyncio.sleep(check_interval)
# Hole die Logs
logs = service.get_workflow_logs(workflow_id)
print("\n=== Logs ===")
for log in logs:
print(f"{log['timestamp'][:19]} [{log['type']}] {log['message']}")
# Hole die Ergebnisse
results = service.get_workflow_results(workflow_id)
print("\n=== Ergebnisse ===")
for result in results:
print(f"\nErgebnis von {result['agent_name']} ({result['agent_id']}):")
print(f"Titel: {result['title']}")
print("-" * 50)
print(result['content'][:500] + ("..." if len(result['content']) > 500 else ""))
print("-" * 50)
except Exception as e:
print(f"Fehler bei der Ausführung des Workflows: {str(e)}")
finally:
# Schließe den Service
await service.close()
print("Test abgeschlossen.")
if __name__ == "__main__":
# Führe den Test aus
asyncio.run(test_agentservice())

View file

@ -1,122 +0,0 @@
from anthropic import Anthropic
import base64
import magic
import os
from typing import Dict, Any, Union, List
def create_message_with_document(file_path: str, prompt_text: str = "Bitte analysiere dieses Dokument:") -> Dict[str, Any]:
"""
Erstellt ein Message-Objekt für die Anthropic API, das ein Dokument enthält.
Args:
file_path: Pfad zur Datei
prompt_text: Text, der zusammen mit dem Dokument gesendet werden soll
Returns:
Ein Message-Objekt für die Anthropic API
"""
# Datei einlesen und als Base64 kodieren
with open(file_path, "rb") as file:
file_content = file.read()
base64_file = base64.b64encode(file_content).decode('utf-8')
# Mime-Typ der Datei mit python-magic erkennen
mime_type = magic.from_buffer(file_content, mime=True)
# Fallback auf Dateiendung, wenn magic keine klare Erkennung liefert
if mime_type == "application/octet-stream":
extension = os.path.splitext(file_path)[1].lower()[1:]
mime_type = get_mime_type_from_extension(extension)
# Message-Objekt erstellen
content_type, message_structure = determine_content_structure(mime_type)
message = {
"role": "user",
"content": [
{
"type": "text",
"text": prompt_text
},
{
"type": content_type,
"source": {
"type": "base64",
"media_type": mime_type,
"data": base64_file
}
}
]
}
return message
def determine_content_structure(mime_type: str) -> tuple[str, str]:
"""
Bestimmt den richtigen content_type und die Nachrichtenstruktur basierend auf dem MIME-Typ.
Args:
mime_type: Der MIME-Typ der Datei
Returns:
Tuple mit (content_type, message_structure)
"""
# Bildtypen
if mime_type.startswith("image/"):
return "image", "image"
# Dokumenttypen
document_types = [
"application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", # docx
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", # xlsx
"application/vnd.openxmlformats-officedocument.presentationml.presentation", # pptx
"application/vnd.ms-excel",
"application/vnd.ms-powerpoint",
"application/msword",
"text/csv",
"text/plain",
"application/json",
"application/xml",
"text/html"
]
if any(mime_type.startswith(dt) for dt in document_types) or mime_type in document_types:
return "document", "document"
# Fallback für unbekannte Typen
return "document", "document"
def get_mime_type_from_extension(extension: str) -> str:
"""
Bestimmt den MIME-Typ basierend auf der Dateiendung.
Args:
extension: Die Dateiendung ohne Punkt
Returns:
Der entsprechende MIME-Typ
"""
extension_to_mime = {
"pdf": "application/pdf",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"doc": "application/msword",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"xls": "application/vnd.ms-excel",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"ppt": "application/vnd.ms-powerpoint",
"csv": "text/csv",
"txt": "text/plain",
"json": "application/json",
"xml": "application/xml",
"html": "text/html",
"htm": "text/html",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"png": "image/png",
"gif": "image/gif",
"webp": "image/webp",
"svg": "image/svg+xml"
}
return extension_to_mime.get(extension, "application/octet-stream")