running frontend - basic agent
This commit is contained in:
parent
06e6d09629
commit
19e44da2f8
17 changed files with 1999 additions and 667 deletions
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"workspaces": 1,
|
||||
"agents": 1,
|
||||
"prompts": 1
|
||||
"prompts": 1,
|
||||
"files": 1
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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")
|
||||
|
|
@ -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 = {}
|
||||
|
|
|
|||
230
gwserver/modules/agentservice_part_agents.py
Normal file
230
gwserver/modules/agentservice_part_agents.py
Normal 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
|
||||
437
gwserver/modules/agentservice_part_filehandling.py
Normal file
437
gwserver/modules/agentservice_part_filehandling.py
Normal 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)
|
||||
221
gwserver/modules/agentservice_part_results.py
Normal file
221
gwserver/modules/agentservice_part_results.py
Normal 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"]
|
||||
285
gwserver/modules/format_converter.py
Normal file
285
gwserver/modules/format_converter.py
Normal 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"
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
115
gwserver/test_agentservice.py
Normal file
115
gwserver/test_agentservice.py
Normal 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())
|
||||
|
|
@ -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")
|
||||
Loading…
Reference in a new issue