img extraction direct and from pdf in all modes
This commit is contained in:
parent
d7cea0398f
commit
78c583d500
11 changed files with 617 additions and 661 deletions
|
|
@ -13,12 +13,23 @@
|
||||||
{
|
{
|
||||||
"mandate_id": 1,
|
"mandate_id": 1,
|
||||||
"user_id": 1,
|
"user_id": 1,
|
||||||
"name": "test.pdf",
|
"name": "test_bild.pdf",
|
||||||
"type": "document",
|
"type": "document",
|
||||||
"content_type": "application/pdf",
|
"content_type": "application/pdf",
|
||||||
"size": 299729,
|
"size": 299729,
|
||||||
"path": "./_uploads\\c30faecc-c041-4225-805f-741328a04bba.pdf",
|
"path": "./_uploads\\b73afc5a-131a-4db8-9572-be2a00c71d91.pdf",
|
||||||
"upload_date": "2025-03-24T16:58:50.089708",
|
"upload_date": "2025-03-25T17:42:03.548097",
|
||||||
"id": 2
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mandate_id": 1,
|
||||||
|
"user_id": 1,
|
||||||
|
"name": "20240419_093309.jpg",
|
||||||
|
"type": "image",
|
||||||
|
"content_type": "image/jpeg",
|
||||||
|
"size": 286163,
|
||||||
|
"path": "./_uploads\\46a65f6f-a30c-4cf2-b0e2-c24709c3282e.jpg",
|
||||||
|
"upload_date": "2025-03-25T18:52:16.203894",
|
||||||
|
"id": 3
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -29,7 +29,7 @@ import modules.gateway_model as gateway_model
|
||||||
|
|
||||||
# Konfiguration des Loggers
|
# Konfiguration des Loggers
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.DEBUG,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
handlers=[logging.StreamHandler()]
|
handlers=[logging.StreamHandler()]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ DEBUG = True
|
||||||
UPLOAD_DIR = ./_uploads
|
UPLOAD_DIR = ./_uploads
|
||||||
RESULTS_DIR = ./_results
|
RESULTS_DIR = ./_results
|
||||||
MAX_HISTORY = 50
|
MAX_HISTORY = 50
|
||||||
AI_PROVIDER = anthropic ; openai oder anthropic
|
AI_PROVIDER = openai ; openai oder anthropic
|
||||||
|
|
||||||
[Connector_AiOpenai]
|
[Connector_AiOpenai]
|
||||||
API_KEY = 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
|
API_KEY = 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
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import io
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
import httpx
|
import httpx
|
||||||
import base64
|
import base64
|
||||||
|
|
@ -7,7 +9,10 @@ import mimetypes
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
import configload as configload
|
import configload as configload
|
||||||
|
import pandas as pd
|
||||||
|
from PIL import Image
|
||||||
|
import PyPDF2
|
||||||
|
import fitz
|
||||||
|
|
||||||
# Logger konfigurieren
|
# Logger konfigurieren
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -91,72 +96,173 @@ class ChatService:
|
||||||
logger.error(f"Fehler beim Aufruf der OpenAI API: {str(e)}")
|
logger.error(f"Fehler beim Aufruf der OpenAI API: {str(e)}")
|
||||||
raise HTTPException(status_code=500, detail=f"Fehler beim Aufruf der OpenAI API: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Fehler beim Aufruf der OpenAI API: {str(e)}")
|
||||||
|
|
||||||
def prepare_file_message_content(self, prompt_text: str, file_paths: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
async def close(self):
|
||||||
|
"""Schließt den HTTP-Client beim Beenden der Anwendung"""
|
||||||
|
await self.http_client.close()
|
||||||
|
|
||||||
|
async def analyze_image(self, image_path: str, prompt: str) -> str:
|
||||||
"""
|
"""
|
||||||
Bereitet eine Nachricht mit Dateien für OpenAI API vor.
|
Analysiert ein Bild mit der OpenAI Vision API.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
prompt_text: Der Text-Prompt
|
image_path: Pfad zum Bild
|
||||||
file_paths: Liste von Dateipfaden mit Metadaten (Dict mit id, name, type, path)
|
prompt: Der Prompt für die Analyse
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Eine für OpenAI-API formatierte content-Liste
|
Die Antwort der OpenAI Vision API als Text
|
||||||
"""
|
"""
|
||||||
message_content = [
|
try:
|
||||||
{
|
# Bild einlesen und als Base64 kodieren
|
||||||
"type": "text",
|
with open(image_path, "rb") as f:
|
||||||
"text": prompt_text
|
image_data = f.read()
|
||||||
}
|
|
||||||
]
|
# In Base64-String konvertieren
|
||||||
|
base64_data = base64.b64encode(image_data).decode('utf-8')
|
||||||
# Füge Dateien als Base64-Anhänge hinzu
|
|
||||||
for file_info in file_paths:
|
# MIME-Typ bestimmen
|
||||||
file_path = file_info.get("path", "")
|
mime_type, _ = mimetypes.guess_type(image_path)
|
||||||
if file_path and os.path.exists(file_path):
|
if not mime_type:
|
||||||
try:
|
# Standard ist PNG, wenn Typ nicht bestimmt werden kann
|
||||||
# Datei als Base64 codieren
|
mime_type = "image/png"
|
||||||
with open(file_path, "rb") as f:
|
|
||||||
file_data = f.read()
|
# Vision-fähiges Modell für die Anfrage verwenden
|
||||||
base64_data = base64.b64encode(file_data).decode('utf-8')
|
vision_model = "gpt-4o" # oder aus der Konfiguration holen
|
||||||
|
|
||||||
# MIME-Typ bestimmen
|
# Bereite den Payload für die Vision API vor
|
||||||
mime_type, _ = mimetypes.guess_type(file_path)
|
messages = [
|
||||||
if not mime_type:
|
{
|
||||||
mime_type = "application/octet-stream"
|
"role": "user",
|
||||||
|
"content": [
|
||||||
# Bei OpenAI werden Bilder anders behandelt als bei Anthropic
|
{"type": "text", "text": prompt},
|
||||||
if mime_type.startswith("image/"):
|
{
|
||||||
message_content.append({
|
|
||||||
"type": "image_url",
|
"type": "image_url",
|
||||||
"image_url": {
|
"image_url": {
|
||||||
"url": f"data:{mime_type};base64,{base64_data}"
|
"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})]"
|
# Verwende die bestehende call_api Funktion mit dem Vision-Modell
|
||||||
})
|
response = await self.call_api(messages)
|
||||||
|
|
||||||
logger.info(f"Datei {file_info.get('name', 'Unbekannt')} als Anhang hinzugefügt")
|
# Inhalt extrahieren und zurückgeben
|
||||||
|
return response["choices"][0]["message"]["content"]
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Fehler beim Hinzufügen der Datei {file_info.get('name', 'Unbekannt')}: {str(e)}")
|
|
||||||
|
|
||||||
return message_content
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler bei der Bildanalyse {image_path}: {str(e)}", exc_info=True)
|
||||||
def parse_filedata(self, file_paths: List[Dict[str, Any]], prompt_text: str = "", file_contents: Dict[str, str] = None) -> Dict[str, Any]:
|
return f"[Fehler bei der Bildanalyse: {str(e)}]"
|
||||||
|
|
||||||
|
|
||||||
|
async def extract_and_analyze_pdf_images(self, pdf_path: str, prompt: str) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Bereitet Dateien für die OpenAI API vor und erstellt ein einheitliches Message-Objekt.
|
Extrahiert Bilder aus einer PDF-Datei mit PyMuPDF und analysiert sie mit der Vision API.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
file_paths: Liste von Dateipfaden mit Metadaten (Dict mit id, name, type, path)
|
pdf_path: Pfad zur PDF-Datei
|
||||||
prompt_text: Der Text-Prompt, der zusammen mit den Dateien gesendet werden soll
|
prompt: Der Prompt für die Bildanalyse
|
||||||
file_contents: Optional vorgelesene Dateiinhalte als Dict[file_id, content]
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Ein standardisiertes Message-Objekt, das für beide API-Typen verwendet werden kann
|
Eine Liste mit Analyseergebnissen für jedes Bild
|
||||||
|
"""
|
||||||
|
import uuid
|
||||||
|
import os
|
||||||
|
image_responses = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# PDF öffnen
|
||||||
|
doc = fitz.open(pdf_path)
|
||||||
|
logger.info(f"PDF geöffnet: {pdf_path} mit {len(doc)} Seiten")
|
||||||
|
|
||||||
|
for page_num, page in enumerate(doc, 1):
|
||||||
|
# Alle Bilder auf der Seite finden
|
||||||
|
image_list = page.get_images(full=True)
|
||||||
|
|
||||||
|
if image_list:
|
||||||
|
logger.info(f"Seite {page_num}: {len(image_list)} Bilder gefunden")
|
||||||
|
|
||||||
|
for img_index, img in enumerate(image_list):
|
||||||
|
try:
|
||||||
|
# Bild-Referenz
|
||||||
|
xref = img[0]
|
||||||
|
|
||||||
|
# Bild und Metadaten extrahieren
|
||||||
|
base_image = doc.extract_image(xref)
|
||||||
|
image_bytes = base_image["image"] # Tatsächliche Bilddaten
|
||||||
|
image_ext = base_image["ext"] # Dateiendung (jpg, png, etc.)
|
||||||
|
|
||||||
|
# Speichere als temporäre Datei
|
||||||
|
unique_id = uuid.uuid4()
|
||||||
|
temp_img_path = f"temp_img_{page_num}_{img_index}_{unique_id}.{image_ext}"
|
||||||
|
|
||||||
|
with open(temp_img_path, "wb") as img_file:
|
||||||
|
img_file.write(image_bytes)
|
||||||
|
|
||||||
|
logger.debug(f"Bild temporär gespeichert: {temp_img_path}")
|
||||||
|
|
||||||
|
# Analysiere mit Vision API
|
||||||
|
try:
|
||||||
|
analysis_result = await self.analyze_image(temp_img_path, prompt)
|
||||||
|
logger.debug(f"Bildanalyse für Bild {img_index} auf Seite {page_num} abgeschlossen")
|
||||||
|
except Exception as analyze_error:
|
||||||
|
logger.error(f"Fehler bei der Bildanalyse: {str(analyze_error)}")
|
||||||
|
analysis_result = f"[Fehler bei der Bildanalyse: {str(analyze_error)}]"
|
||||||
|
|
||||||
|
# Ergebnis speichern
|
||||||
|
try:
|
||||||
|
# Versuche zuerst, die Größe aus base_image zu bekommen
|
||||||
|
if 'width' in base_image and 'height' in base_image:
|
||||||
|
image_size = f"{base_image['width']}x{base_image['height']}"
|
||||||
|
else:
|
||||||
|
# Alternative: Öffne das temporäre Bild, um die Größe zu bestimmen
|
||||||
|
with Image.open(temp_img_path) as img:
|
||||||
|
width, height = img.size
|
||||||
|
image_size = f"{width}x{height}"
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Konnte Bildgröße nicht ermitteln: {str(e)}")
|
||||||
|
image_size = "unbekannt"
|
||||||
|
|
||||||
|
image_responses.append({
|
||||||
|
"page": page_num,
|
||||||
|
"image_index": img_index,
|
||||||
|
"format": image_ext,
|
||||||
|
"image_size": image_size, # Der key wird immer gesetzt, entweder mit tatsächlicher Größe oder "unbekannt"
|
||||||
|
"response": analysis_result
|
||||||
|
})
|
||||||
|
|
||||||
|
# Temporäre Datei löschen
|
||||||
|
try:
|
||||||
|
if os.path.exists(temp_img_path):
|
||||||
|
os.remove(temp_img_path)
|
||||||
|
logger.debug(f"Temporäre Bilddatei entfernt: {temp_img_path}")
|
||||||
|
except Exception as cleanup_error:
|
||||||
|
logger.warning(f"Temporäre Datei konnte nicht entfernt werden: {cleanup_error}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Fehler bei der Extraktion von Bild {img_index} auf Seite {page_num}: {str(e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.info(f"Extrahiert und analysiert: {len(image_responses)} Bilder aus PDF {os.path.basename(pdf_path)}")
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
logger.error("PyMuPDF (fitz) ist nicht installiert. Installiere es mit 'pip install pymupdf'")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Extrahieren von PDF-Bildern: {str(e)}")
|
||||||
|
return image_responses
|
||||||
|
|
||||||
|
|
||||||
|
async def parse_filedata(self, file_contexts: List[Dict[str, Any]], prompt_text: str, file_contents: Dict[str, str] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Bereitet eine vollständige Nachricht mit allen Dateiinhalten für das AI-Modell vor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_contexts: Liste der Dateikontexte mit Metadaten
|
||||||
|
prompt_text: Der Text-Prompt
|
||||||
|
file_contents: Dictionary mit bereits geladenen Dateiinhalten
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Eine vollständig formatierte Nachricht für das AI-Modell
|
||||||
"""
|
"""
|
||||||
# Basisstruktur für die Nachricht in OpenAI-Format
|
# Basisstruktur für die Nachricht in OpenAI-Format
|
||||||
message = {
|
message = {
|
||||||
|
|
@ -171,101 +277,116 @@ class ChatService:
|
||||||
"text": prompt_text
|
"text": prompt_text
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if not file_contents:
|
||||||
|
file_contents = {}
|
||||||
|
|
||||||
# Dateien als Anhänge hinzufügen
|
# Dateien als Anhänge hinzufügen
|
||||||
for file_info in file_paths:
|
for file_info in file_contexts:
|
||||||
file_path = file_info.get("path", "")
|
file_path = file_info.get("path", "")
|
||||||
file_name = file_info.get("name", "")
|
file_name = file_info.get("name", "")
|
||||||
file_id = file_info.get("id", "")
|
file_id = file_info.get("id", "")
|
||||||
|
file_type = file_info.get("type", "")
|
||||||
|
|
||||||
# Prüfen, ob Dateiinhalt bereits vorhanden ist
|
# Prüfen, ob Dateiinhalt bereits vorhanden ist
|
||||||
if file_contents and file_id in file_contents:
|
if file_id in file_contents:
|
||||||
# Bereits verarbeiteten Inhalt verwenden
|
content = file_contents[file_id]
|
||||||
|
|
||||||
|
# Problematische Unicode-Zeichen ersetzen
|
||||||
|
if isinstance(content, str):
|
||||||
|
content = content.encode('utf-8', errors='replace').decode('utf-8')
|
||||||
|
else:
|
||||||
|
content = str(content)
|
||||||
|
|
||||||
|
# Besondere Verarbeitung für PDF-Dateien mit Bildern
|
||||||
|
if file_name.endswith('.pdf') and file_path and os.path.exists(file_path):
|
||||||
|
try:
|
||||||
|
# Bildanalyse der PDF durchführen
|
||||||
|
image_prompt = prompt_text or "Beschreibe detailliert, was du in diesem Bild siehst."
|
||||||
|
image_responses = await self.extract_and_analyze_pdf_images(file_path, image_prompt)
|
||||||
|
|
||||||
|
# Nur wenn Bilder gefunden wurden, füge die Analyse hinzu
|
||||||
|
if image_responses:
|
||||||
|
image_details = "\n\n".join([
|
||||||
|
f"Bild auf Seite {resp['page']} (Größe: {resp['image_size']}): {resp['response']}"
|
||||||
|
for resp in image_responses
|
||||||
|
])
|
||||||
|
logger.debug("Image description: "+image_details)
|
||||||
|
message["content"].append({
|
||||||
|
"type": "text",
|
||||||
|
"text": f"PDF-Bildanalyse für {file_name}:\n{image_details}"
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler bei der PDF-Bildanalyse für {file_name}: {str(e)}")
|
||||||
|
|
||||||
|
# Text zur Nachricht hinzufügen
|
||||||
message["content"].append({
|
message["content"].append({
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"text": f"Datei: {file_name}\n{file_contents[file_id]}"
|
"text": f"--- DATEI: {file_name} ---\n\n{content}"
|
||||||
})
|
})
|
||||||
logger.info(f"Vorverarbeiteter Inhalt für Datei {file_name} verwendet")
|
logger.info(f"Inhalt für Datei {file_name} zur Nachricht hinzugefügt")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Sonst Datei direkt verarbeiten
|
# Wenn kein Inhalt vorhanden ist, füge einen Hinweis hinzu
|
||||||
if file_path and os.path.exists(file_path):
|
if not file_path or not os.path.exists(file_path):
|
||||||
|
message["content"].append({
|
||||||
|
"type": "text",
|
||||||
|
"text": f"[Datei {file_name} wurde nicht gefunden oder ist nicht zugänglich]"
|
||||||
|
})
|
||||||
|
logger.warning(f"Datei {file_name} nicht gefunden: {file_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Direktes Hinzufügen von Bildern bei OpenAI (für Bilder außerhalb von PDFs)
|
||||||
|
if file_type == "image" or file_name.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
|
||||||
try:
|
try:
|
||||||
# Datei einlesen
|
# Bild als Base64 kodieren
|
||||||
with open(file_path, "rb") as f:
|
with open(file_path, "rb") as f:
|
||||||
file_data = f.read()
|
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')
|
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"
|
||||||
|
|
||||||
|
# Bild zur Nachricht hinzufügen
|
||||||
|
if mime_type.startswith("image/"):
|
||||||
|
# Füge zunächst die Bildanalyse als Text hinzu
|
||||||
|
image_prompt = prompt_text or "Beschreibe detailliert, was du in diesem Bild siehst."
|
||||||
|
analysis_result = await self.analyze_image(file_path, image_prompt)
|
||||||
|
|
||||||
|
message["content"].append({
|
||||||
|
"type": "text",
|
||||||
|
"text": f"Bildanalyse für {file_name}:\n{analysis_result}"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Dann füge das Bild selbst hinzu
|
||||||
message["content"].append({
|
message["content"].append({
|
||||||
"type": "image_url",
|
"type": "image_url",
|
||||||
"image_url": {
|
"image_url": {
|
||||||
"url": f"data:{mime_type};base64,{base64_data}"
|
"url": f"data:{mime_type};base64,{base64_data}"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
else:
|
|
||||||
# Für nicht-Bild-Dateien als Text hinzufügen
|
logger.info(f"Bild {file_name} analysiert und zur Nachricht hinzugefügt")
|
||||||
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:
|
except Exception as e:
|
||||||
logger.error(f"Fehler beim Hinzufügen der Datei {file_name}: {str(e)}")
|
logger.error(f"Fehler beim Hinzufügen des Bildes {file_name}: {str(e)}")
|
||||||
message["content"].append({
|
message["content"].append({
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"text": f"[Fehler beim Laden der Datei {file_name}: {str(e)}]"
|
"text": f"[Fehler beim Laden des Bildes {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
|
# Optimiere das Message-Format für die API basierend auf dem Inhalt
|
||||||
if not message["content"]:
|
if not message["content"]:
|
||||||
|
# Leerer Inhalt - setze auf leeren String
|
||||||
message["content"] = prompt_text or ""
|
message["content"] = prompt_text or ""
|
||||||
elif len(message["content"]) == 1 and message["content"][0]["type"] == "text":
|
elif len(message["content"]) == 1 and message["content"][0]["type"] == "text":
|
||||||
# Wenn nur ein Text-Element vorhanden ist, vereinfachen wir die Struktur
|
# Nur ein Text-Element - vereinfache die Struktur
|
||||||
message["content"] = message["content"][0]["text"]
|
message["content"] = message["content"][0]["text"]
|
||||||
|
|
||||||
|
# Bei komplizierten Inhalten (Mischung aus Text und Bildern) wird das Array-Format beibehalten
|
||||||
|
|
||||||
|
logger.debug(f"Message-Objekt für OpenAI erstellt mit {len(message['content']) if isinstance(message['content'], list) else 'String'}-Inhalt")
|
||||||
return message
|
return message
|
||||||
|
|
||||||
async def close(self):
|
|
||||||
"""Schließt den HTTP-Client beim Beenden der Anwendung"""
|
|
||||||
await self.http_client.aclose()
|
|
||||||
|
|
||||||
|
|
||||||
def get_mime_type_from_extension(extension: str) -> str:
|
def get_mime_type_from_extension(extension: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import configload as configload
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
# Logger konfigurieren
|
# Logger konfigurieren
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,8 @@ class AgentService:
|
||||||
Anstatt die Agenten der Reihe nach abzuarbeiten, wird ein AI-Moderator verwendet,
|
Anstatt die Agenten der Reihe nach abzuarbeiten, wird ein AI-Moderator verwendet,
|
||||||
der die Agenten basierend auf ihren Fähigkeiten und bisherigen Antworten steuert.
|
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,
|
Verwendet file_handling.prepare_message_for_ai für die Vorbereitung der Nachrichten,
|
||||||
wobei vorverarbeitete Dateiinhalte effizient wiederverwendet werden.
|
wobei alle Dateiinhalte vollständig gelesen werden.
|
||||||
"""
|
"""
|
||||||
logger.info(f"Starte Workflow {workflow_id} mit {len(agents_list)} Agenten und {len(files)} Dateien")
|
logger.info(f"Starte Workflow {workflow_id} mit {len(agents_list)} Agenten und {len(files)} Dateien")
|
||||||
|
|
||||||
|
|
@ -126,7 +126,9 @@ class AgentService:
|
||||||
|
|
||||||
# Dateikontexte vorbereiten
|
# Dateikontexte vorbereiten
|
||||||
file_contexts = file_handling.prepare_file_contexts(files, self.upload_dir)
|
file_contexts = file_handling.prepare_file_contexts(files, self.upload_dir)
|
||||||
|
for fc in file_contexts:
|
||||||
|
logger.debug(f"Dateikontext: ID={fc['id']}, Name={fc['name']}, Typ={fc.get('type', 'unbekannt')}")
|
||||||
|
|
||||||
# Dateiinhalte lesen - EINMAL für den gesamten Workflow
|
# Dateiinhalte lesen - EINMAL für den gesamten Workflow
|
||||||
file_contents = file_handling.read_file_contents(
|
file_contents = file_handling.read_file_contents(
|
||||||
file_contexts,
|
file_contexts,
|
||||||
|
|
@ -145,11 +147,12 @@ class AgentService:
|
||||||
chat_history = []
|
chat_history = []
|
||||||
|
|
||||||
# Erstelle das standardisierte Nachrichtenobjekt für die initialen Dateien und den Prompt
|
# Erstelle das standardisierte Nachrichtenobjekt für die initialen Dateien und den Prompt
|
||||||
# Verwende parse_filedata aus dem Connector und übergebe die vorverarbeiteten Dateiinhalte
|
# Verwende file_handling.prepare_message_for_ai mit dem Service
|
||||||
initial_message = self.service_aichat.parse_filedata(
|
initial_message = await file_handling.prepare_message_for_ai(
|
||||||
file_contexts,
|
file_contexts,
|
||||||
prompt,
|
prompt,
|
||||||
file_contents # Übergebe die vorverarbeiteten Inhalte
|
file_contents,
|
||||||
|
self.service_aichat
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialen Prompt zum Chatverlauf hinzufügen
|
# Initialen Prompt zum Chatverlauf hinzufügen
|
||||||
|
|
@ -199,7 +202,7 @@ class AgentService:
|
||||||
|
|
||||||
# Moderator trifft die Entscheidung
|
# Moderator trifft die Entscheidung
|
||||||
try:
|
try:
|
||||||
moderator_chat = self._sanitize_messages(moderator_chat)
|
moderator_chat = await self._sanitize_messages(moderator_chat)
|
||||||
moderator_decision = await self.service_aichat.call_api(moderator_chat)
|
moderator_decision = await self.service_aichat.call_api(moderator_chat)
|
||||||
moderator_text = moderator_decision["choices"][0]["message"]["content"]
|
moderator_text = moderator_decision["choices"][0]["message"]["content"]
|
||||||
logger.debug(f"Full moderator decision text: {moderator_text}")
|
logger.debug(f"Full moderator decision text: {moderator_text}")
|
||||||
|
|
@ -243,7 +246,7 @@ class AgentService:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Agent-spezifische Anweisungen erstellen
|
# Agent-spezifische Anweisungen erstellen
|
||||||
agent_instructions = agents.get_agent_instructions(selected_agent["type"], selected_agent)
|
agent_instructions = agents.get_agent_instructions(selected_agent["type"], selected_agent, file_contexts)
|
||||||
|
|
||||||
# Agent-Prompt erstellen
|
# Agent-Prompt erstellen
|
||||||
agent_prompt = agents.create_agent_prompt(selected_agent, agent_instructions)
|
agent_prompt = agents.create_agent_prompt(selected_agent, agent_instructions)
|
||||||
|
|
@ -268,74 +271,10 @@ class AgentService:
|
||||||
|
|
||||||
# Agent führt seinen Teil aus
|
# Agent führt seinen Teil aus
|
||||||
try:
|
try:
|
||||||
# Initialisiere eine Schleife für mögliche Dateianfragen des Agenten
|
# Da wir keine partielle Dateiladung benötigen, können wir den Agenten direkt aufrufen
|
||||||
agent_processing_complete = False
|
agent_chat = await self._sanitize_messages(agent_chat)
|
||||||
max_file_requests = 5 # Begrenze die Anzahl der Dateianfragen pro Agent
|
agent_response = await self.service_aichat.call_api(agent_chat)
|
||||||
file_request_count = 0
|
agent_text = agent_response["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
while not agent_processing_complete and file_request_count < max_file_requests:
|
|
||||||
# Rufe die API auf
|
|
||||||
agent_chat = self._sanitize_messages(agent_chat)
|
|
||||||
agent_response = await self.service_aichat.call_api(agent_chat)
|
|
||||||
agent_text = agent_response["choices"][0]["message"]["content"]
|
|
||||||
|
|
||||||
# Prüfe, ob der Agent weitere Dateien anfordert
|
|
||||||
file_commands = file_handling.parse_file_access_commands(agent_text)
|
|
||||||
|
|
||||||
if file_commands:
|
|
||||||
file_request_count += 1
|
|
||||||
self._add_log(
|
|
||||||
workflow_id,
|
|
||||||
f"Agent '{selected_agent['name']}' fordert zusätzliche Dateiinhalte an (Anfrage {file_request_count})",
|
|
||||||
"info",
|
|
||||||
selected_agent_id,
|
|
||||||
selected_agent["name"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verarbeite alle Dateianfragen
|
|
||||||
file_responses = []
|
|
||||||
for cmd in file_commands:
|
|
||||||
file_id = cmd.get('file_id')
|
|
||||||
complete = cmd.get('complete', False)
|
|
||||||
start = cmd.get('start')
|
|
||||||
end = cmd.get('end')
|
|
||||||
pages = cmd.get('pages')
|
|
||||||
|
|
||||||
content = file_handling.load_additional_file_content(
|
|
||||||
workflow_id,
|
|
||||||
file_id,
|
|
||||||
file_contents,
|
|
||||||
file_contexts,
|
|
||||||
self._add_log,
|
|
||||||
read_complete=complete,
|
|
||||||
start_pos=start,
|
|
||||||
end_pos=end,
|
|
||||||
page_numbers=pages
|
|
||||||
)
|
|
||||||
|
|
||||||
if content:
|
|
||||||
file_name = next((f['name'] for f in file_contexts if f['id'] == file_id),
|
|
||||||
f"Datei {file_id}")
|
|
||||||
file_responses.append(f"Zusätzlicher Inhalt für {file_name}:\n\n{content}")
|
|
||||||
else:
|
|
||||||
file_responses.append(f"Datei mit ID {file_id} konnte nicht geladen werden.")
|
|
||||||
|
|
||||||
# Füge die Antworten zum Chatverlauf hinzu
|
|
||||||
file_response_text = "\n\n".join(file_responses)
|
|
||||||
system_response = {
|
|
||||||
"role": "system",
|
|
||||||
"content": (f"Hier sind die angeforderten Dateiinhalte:\n\n{file_response_text}\n\n" +
|
|
||||||
f"Bitte fahre nun mit deiner Analyse fort.").strip() }
|
|
||||||
|
|
||||||
# Füge Systemantwort zum Chatverlauf und zum Agentenchat hinzu
|
|
||||||
chat_history.append(system_response)
|
|
||||||
agent_chat.append(system_response)
|
|
||||||
|
|
||||||
# Setze die Schleife fort, um dem Agenten die Möglichkeit zu geben, seine Analyse fortzusetzen
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Keine Dateianfragen mehr - Agent ist fertig
|
|
||||||
agent_processing_complete = True
|
|
||||||
|
|
||||||
# Füge die endgültige Antwort des Agenten zum Chatverlauf hinzu
|
# Füge die endgültige Antwort des Agenten zum Chatverlauf hinzu
|
||||||
chat_history.append({
|
chat_history.append({
|
||||||
|
|
@ -472,7 +411,7 @@ class AgentService:
|
||||||
|
|
||||||
# Workflow-Status auf "stopped" setzen
|
# Workflow-Status auf "stopped" setzen
|
||||||
self.workflows[workflow_id]["status"] = "stopped"
|
self.workflows[workflow_id]["status"] = "stopped"
|
||||||
self.workflows["progress"] = 1.0
|
self.workflows[workflow_id]["progress"] = 1.0 # Korrigierter Zugriff auf den Workflow
|
||||||
self.workflows[workflow_id]["completed_at"] = datetime.now().isoformat()
|
self.workflows[workflow_id]["completed_at"] = datetime.now().isoformat()
|
||||||
|
|
||||||
# Log-Eintrag für das Stoppen hinzufügen
|
# Log-Eintrag für das Stoppen hinzufügen
|
||||||
|
|
@ -484,25 +423,39 @@ class AgentService:
|
||||||
logger.info(f"Workflow {workflow_id} wurde gestoppt")
|
logger.info(f"Workflow {workflow_id} wurde gestoppt")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _sanitize_message_content(self, content):
|
async def _sanitize_messages(self, messages):
|
||||||
"""Ensures content has no trailing whitespace."""
|
|
||||||
if isinstance(content, str):
|
|
||||||
return content.rstrip()
|
|
||||||
return content
|
|
||||||
|
|
||||||
def _sanitize_messages(self, messages):
|
|
||||||
"""Sanitizes all messages to prevent API errors."""
|
"""Sanitizes all messages to prevent API errors."""
|
||||||
if not messages:
|
if not messages:
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
sanitized = []
|
sanitized = []
|
||||||
for message in messages:
|
for message in messages:
|
||||||
sanitized_message = message.copy()
|
# Create a deep copy of the message
|
||||||
if "content" in sanitized_message:
|
sanitized_message = message.copy() if isinstance(message, dict) else message
|
||||||
|
|
||||||
|
if isinstance(sanitized_message, dict) and "content" in sanitized_message:
|
||||||
sanitized_message["content"] = self._sanitize_message_content(sanitized_message["content"])
|
sanitized_message["content"] = self._sanitize_message_content(sanitized_message["content"])
|
||||||
|
|
||||||
sanitized.append(sanitized_message)
|
sanitized.append(sanitized_message)
|
||||||
|
|
||||||
return sanitized
|
return sanitized
|
||||||
|
|
||||||
|
def _sanitize_message_content(self, content):
|
||||||
|
"""Ensures content has no trailing whitespace."""
|
||||||
|
if isinstance(content, str):
|
||||||
|
return content.rstrip()
|
||||||
|
|
||||||
|
# If content is a list (for multimodal messages), process each item
|
||||||
|
if isinstance(content, list):
|
||||||
|
return [
|
||||||
|
{**item, 'text': item['text'].rstrip() if isinstance(item.get('text'), str) else item.get('text')}
|
||||||
|
if isinstance(item, dict) and item.get('type') == 'text'
|
||||||
|
else item
|
||||||
|
for item in content
|
||||||
|
]
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
# Singleton-Factory für AgentService-Instanzen pro Kontext
|
# Singleton-Factory für AgentService-Instanzen pro Kontext
|
||||||
_agent_service_instances = {}
|
_agent_service_instances = {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,76 @@
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, Any, List, Optional
|
import re
|
||||||
|
from typing import Dict, Any, List, Optional, Tuple
|
||||||
|
|
||||||
# Logger konfigurieren
|
# Logger konfigurieren
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def get_agent_instructions(agent_type: str, agent: Dict[str, Any] = None) -> str:
|
def get_agent_instructions(agent_type: str, agent: Dict[str, Any] = None, file_contexts: List[Dict[str, Any]] = None) -> str:
|
||||||
"""Minimalist agent instructions"""
|
"""
|
||||||
if agent and agent.get("instructions"):
|
Liefert Anweisungen für einen Agenten basierend auf seinen Attributen.
|
||||||
instructions = agent.get("instructions")
|
|
||||||
else:
|
Diese Version fügt explizite Informationen zu Datei-IDs hinzu.
|
||||||
instructions = """
|
|
||||||
Analysiere die Anfrage gründlich.
|
Args:
|
||||||
Liefere präzise und hilfreiche Antworten.
|
agent_type: Typ des Agenten
|
||||||
Strukturiere deine Antwort klar.
|
agent: Agent-Konfiguration mit allen Attributen
|
||||||
|
file_contexts: Liste der verfügbaren Dateien
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatierte Anweisungen für den Agenten
|
||||||
|
"""
|
||||||
|
# Basis-Instruktionen aus dem Agenten-Profil extrahieren
|
||||||
|
base_instructions = ""
|
||||||
|
|
||||||
|
if agent:
|
||||||
|
if agent.get("instructions"):
|
||||||
|
base_instructions += agent.get("instructions").strip() + "\n\n"
|
||||||
|
if agent.get("description"):
|
||||||
|
base_instructions += "Kontext: " + agent.get("description").strip() + "\n\n"
|
||||||
|
if agent.get("capabilities"):
|
||||||
|
base_instructions += "Deine Fähigkeiten: " + agent.get("capabilities").strip() + "\n\n"
|
||||||
|
|
||||||
|
# Wenn keine Instruktionen gefunden wurden, verwende eine generische Anweisung
|
||||||
|
if not base_instructions:
|
||||||
|
base_instructions = """
|
||||||
|
Analysiere die Anfrage gründlich und liefere ein konkretes, nützliches Ergebnis.
|
||||||
|
Strukturiere deine Antwort klar und beantworte alle Aspekte der Anfrage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
file_access = """
|
# Anweisung zur Selbstdeklaration des Ergebnisstatus hinzufügen
|
||||||
Dateibefehl: [[FILE:load_file(file_id=ID, complete=true)]]
|
status_declaration = """
|
||||||
|
WICHTIG: Deklariere am Ende deiner Antwort den Status deines Ergebnisses mit einem der folgenden Tags:
|
||||||
|
|
||||||
|
[STATUS: ERGEBNIS] - Wenn du ein konkretes, vollständiges Ergebnis geliefert hast
|
||||||
|
[STATUS: TEILWEISE] - Wenn du ein teilweises Ergebnis geliefert hast, das noch ergänzt werden sollte
|
||||||
|
[STATUS: PLAN] - Wenn du hauptsächlich einen Plan oder eine Vorgehensweise vorgeschlagen hast
|
||||||
|
|
||||||
|
Diese Deklaration hilft dem Moderator zu entscheiden, ob weitere Agentenarbeit erforderlich ist.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return instructions + file_access
|
# Konkrete Dateiinformationen bereitstellen
|
||||||
|
file_info = ""
|
||||||
|
if file_contexts:
|
||||||
|
file_info = "Verfügbare Dateien:\n"
|
||||||
|
for file in file_contexts:
|
||||||
|
file_info += f"- {file['name']} (Typ: {file.get('type', 'unbekannt')}, ID: {file['id']})\n"
|
||||||
|
file_info += "\n"
|
||||||
|
|
||||||
|
# Hinweise zum Dateiaufrufen
|
||||||
|
file_access = f"""
|
||||||
|
{file_info}
|
||||||
|
Um mehr Dateiinhalte anzufordern, verwende einen der folgenden Befehle:
|
||||||
|
|
||||||
|
[[FILE:load_file(file_id=DATEI_ID, complete=true)]] - für den vollständigen Dateiinhalt
|
||||||
|
[[FILE:load_file(file_id=DATEI_ID, pages=[1,2,3])]] - für spezifische Seiten einer PDF
|
||||||
|
[[FILE:load_file(file_id=DATEI_ID, start=0, end=5000)]] - für Textabschnitte
|
||||||
|
|
||||||
|
Ersetze DATEI_ID mit der tatsächlichen ID aus der Dateiliste oben (nur die ID-Nummer, keine Anführungszeichen).
|
||||||
|
"""
|
||||||
|
|
||||||
|
return base_instructions + status_declaration + file_access
|
||||||
|
|
||||||
def get_default_agent_instructions() -> str:
|
def get_default_agent_instructions() -> str:
|
||||||
"""
|
"""
|
||||||
|
|
@ -44,6 +92,9 @@ def get_default_agent_instructions() -> str:
|
||||||
- Gib gut begründete Antworten oder Empfehlungen
|
- Gib gut begründete Antworten oder Empfehlungen
|
||||||
- Führe wichtige Erkenntnisse klar auf
|
- Führe wichtige Erkenntnisse klar auf
|
||||||
- Schließe mit konkreten nächsten Schritten oder Empfehlungen ab
|
- Schließe mit konkreten nächsten Schritten oder Empfehlungen ab
|
||||||
|
|
||||||
|
WICHTIG: Deklariere am Ende deiner Antwort den Status deines Ergebnisses:
|
||||||
|
[STATUS: ERGEBNIS], [STATUS: TEILWEISE] oder [STATUS: PLAN]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -68,6 +119,7 @@ def initialize_agents(agents: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]
|
||||||
# Kopiere alle Felder vom Original-Agenten und füge used-Status hinzu
|
# Kopiere alle Felder vom Original-Agenten und füge used-Status hinzu
|
||||||
agent_data = agent.copy()
|
agent_data = agent.copy()
|
||||||
agent_data["used"] = False
|
agent_data["used"] = False
|
||||||
|
agent_data["last_result_status"] = None # Für die Speicherung des Ergebnisstatus
|
||||||
|
|
||||||
available_agents[agent_id] = agent_data
|
available_agents[agent_id] = agent_data
|
||||||
|
|
||||||
|
|
@ -81,30 +133,63 @@ def initialize_agents(agents: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
def get_moderator_prompt(available_agents: Dict[str, Dict[str, Any]]) -> str:
|
def get_moderator_prompt(available_agents: Dict[str, Dict[str, Any]]) -> str:
|
||||||
"""Streamlined moderator prompt"""
|
"""
|
||||||
base = "Du bist Moderator eines Multi-Agent-Systems. Koordiniere die Agenten, um die Anfrage zu erfüllen."
|
Erstellt einen Moderator-Prompt, der die Status-Deklarationen der Agenten berücksichtigt.
|
||||||
|
|
||||||
agents_list = "\nAgenten:\n"
|
Args:
|
||||||
|
available_agents: Dictionary mit verfügbaren Agenten
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatierter Prompt für den Moderator
|
||||||
|
"""
|
||||||
|
base = """Du bist Moderator eines Multi-Agent-Systems. Deine Aufgabe ist es, die Agenten zu koordinieren,
|
||||||
|
um die Anfrage vollständig zu erfüllen und ein konkretes Endergebnis zu liefern.
|
||||||
|
|
||||||
|
WICHTIG: Der Workflow darf erst beendet werden, wenn TATSÄCHLICHE ERGEBNISSE geliefert wurden."""
|
||||||
|
|
||||||
|
agents_list = "\nVerfügbare Agenten:\n"
|
||||||
for agent_id, agent in available_agents.items():
|
for agent_id, agent in available_agents.items():
|
||||||
status = "✓" if agent["used"] else "✗"
|
status = "✓ Bereits verwendet" if agent["used"] else "✗ Noch nicht verwendet"
|
||||||
agents_list += f"- {agent['name']} ({agent['type']}): {status}\n"
|
result_status = ""
|
||||||
|
if agent["used"] and agent.get("last_result_status"):
|
||||||
|
result_status = f" (Letzte Antwort: {agent.get('last_result_status')})"
|
||||||
|
|
||||||
|
description = agent.get("description", "")
|
||||||
|
capabilities = agent.get("capabilities", "")
|
||||||
|
agents_list += f"- {agent['name']} ({agent['type']}): {capabilities}\n {description}\n Status: {status}{result_status}\n"
|
||||||
|
|
||||||
instructions = """
|
instructions = """
|
||||||
Pro Runde: Wähle EINEN Agenten ODER beende den Workflow nur wenn die Anfrage vollständig beantwortet ist.
|
Berücksichtige die STATUS-Deklarationen der Agenten bei deiner Entscheidung:
|
||||||
|
|
||||||
Bei Agentenwahl: "Ich wähle [Agentname]"
|
- [STATUS: ERGEBNIS] - Der Agent hat ein vollständiges Ergebnis geliefert
|
||||||
Bei Abschluss: "Workflow beenden"
|
- [STATUS: TEILWEISE] - Der Agent hat ein teilweises Ergebnis geliefert, weitere Arbeit ist nötig
|
||||||
"""
|
- [STATUS: PLAN] - Der Agent hat einen Plan geliefert, keine konkreten Ergebnisse
|
||||||
|
|
||||||
|
Mögliche Entscheidungen:
|
||||||
|
- Bei Agent-Auswahl: "Ich wähle [Agentname], um [konkrete Aufgabe]"
|
||||||
|
- Bei Abschluss (nur wenn [STATUS: ERGEBNIS] vorliegt): "Workflow beenden - vollständiges Ergebnis erreicht"
|
||||||
|
|
||||||
|
WICHTIG: Du darfst den Workflow NUR beenden, wenn mindestens ein Agent ein konkretes [STATUS: ERGEBNIS] geliefert hat!
|
||||||
|
"""
|
||||||
|
|
||||||
return base + agents_list + instructions
|
return base + agents_list + instructions
|
||||||
|
|
||||||
|
|
||||||
def create_agent_prompt(agent: Dict[str, Any], agent_instructions: str) -> Dict[str, str]:
|
def create_agent_prompt(agent: Dict[str, Any], agent_instructions: str, file_contexts: List[Dict[str, Any]] = None) -> Dict[str, str]:
|
||||||
"""Minimal agent prompt"""
|
"""Verbesserter Agent-Prompt mit Datei-IDs"""
|
||||||
|
# Füge Datei-ID-Infos hinzu, wenn verfügbar
|
||||||
|
file_info = ""
|
||||||
|
if file_contexts:
|
||||||
|
file_info = "\nVerfügbare Dateien:\n"
|
||||||
|
for file in file_contexts:
|
||||||
|
file_info += f"- {file['name']} (ID: {file['id']})\n"
|
||||||
|
|
||||||
content = f"""
|
content = f"""
|
||||||
Du bist Agent {agent['name']} ({agent['type']}).
|
Du bist Agent {agent['name']} ({agent['type']}).
|
||||||
{agent_instructions}
|
{agent_instructions}
|
||||||
|
|
||||||
|
{file_info}
|
||||||
|
|
||||||
Format: [Agent: {agent['name']}] Deine Antwort...
|
Format: [Agent: {agent['name']}] Deine Antwort...
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
|
|
@ -112,28 +197,138 @@ def create_agent_prompt(agent: Dict[str, Any], agent_instructions: str) -> Dict[
|
||||||
|
|
||||||
|
|
||||||
def find_next_agent(moderator_text: str, available_agents: Dict[str, Dict[str, Any]]) -> Optional[str]:
|
def find_next_agent(moderator_text: str, available_agents: Dict[str, Dict[str, Any]]) -> Optional[str]:
|
||||||
"""Simplified agent selection logic"""
|
"""
|
||||||
|
Findet den nächsten Agenten basierend auf der Moderator-Entscheidung.
|
||||||
|
Berücksichtigt die Anweisung zum Workflow-Abschluss nur, wenn ein Ergebnis vorliegt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
moderator_text: Text der Moderator-Entscheidung
|
||||||
|
available_agents: Dictionary mit verfügbaren Agenten
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ID des nächsten Agenten oder "WORKFLOW_COMPLETE" zum Beenden
|
||||||
|
"""
|
||||||
text = moderator_text.lower()
|
text = moderator_text.lower()
|
||||||
|
|
||||||
# Check for workflow completion
|
# Prüfe, ob der Workflow beendet werden soll - nur wenn vollständige Ergebnisse vorliegen
|
||||||
if "workflow beenden" in text:
|
workflow_complete_phrases = [
|
||||||
return "WORKFLOW_COMPLETE"
|
"workflow beenden - vollständiges ergebnis erreicht",
|
||||||
|
"workflow beenden - vollständiges ergebnis",
|
||||||
|
"vollständiges ergebnis erreicht"
|
||||||
|
]
|
||||||
|
|
||||||
# Look for "ich wähle" pattern
|
# Prüfen, ob ein Agent ein Ergebnis geliefert hat
|
||||||
|
result_exists = False
|
||||||
|
for agent_id, agent in available_agents.items():
|
||||||
|
if agent.get("used") and agent.get("last_result_status") == "ERGEBNIS":
|
||||||
|
result_exists = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Suche nach exakten Phrasen für Workflow-Beendigung
|
||||||
|
if "workflow beenden" in text:
|
||||||
|
# Wenn ein eindeutiges Ergebnis vorliegt oder eine spezifische Phrase gefunden wurde
|
||||||
|
if result_exists or any(phrase in text for phrase in workflow_complete_phrases):
|
||||||
|
return "WORKFLOW_COMPLETE"
|
||||||
|
# Sonst: In den Logs warnen, dass der Moderator versucht, ohne Ergebnis zu beenden
|
||||||
|
else:
|
||||||
|
logger.warning("Moderator versuchte, Workflow ohne vollständiges Ergebnis zu beenden")
|
||||||
|
|
||||||
|
# Suche nach "ich wähle" Pattern für Agentenwahl
|
||||||
if "ich wähle" in text:
|
if "ich wähle" in text:
|
||||||
for agent_id, agent in available_agents.items():
|
for agent_id, agent in available_agents.items():
|
||||||
if agent["name"].lower() in text:
|
agent_name_lower = agent["name"].lower()
|
||||||
|
if agent_name_lower in text:
|
||||||
return agent_id
|
return agent_id
|
||||||
|
|
||||||
# Simple name matching
|
# Fallback: Direktes Name-Matching
|
||||||
for agent_id, agent in available_agents.items():
|
for agent_id, agent in available_agents.items():
|
||||||
if agent["name"].lower() in text:
|
agent_name_lower = agent["name"].lower()
|
||||||
|
if agent_name_lower in text or f"agent {agent_name_lower}" in text:
|
||||||
return agent_id
|
return agent_id
|
||||||
|
|
||||||
# Fallback: first unused agent
|
# Wenn kein Agent explizit gewählt wurde: Wähle den ersten unbenutzten Agenten
|
||||||
for agent_id, agent in available_agents.items():
|
for agent_id, agent in available_agents.items():
|
||||||
if not agent["used"]:
|
if not agent["used"]:
|
||||||
return agent_id
|
return agent_id
|
||||||
|
|
||||||
# Last resort: first agent
|
# Letzter Ausweg: Ersten Agenten wiederverwenden
|
||||||
return list(available_agents.keys())[0] if available_agents else None
|
if available_agents:
|
||||||
|
return list(available_agents.keys())[0]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def update_agent_results_with_status(content: str) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Extrahiert den deklarierten Status aus einem Agenten-Ergebnis.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Der vom Agenten gelieferte Ergebnistext
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple mit (bereinigter Text, Status)
|
||||||
|
"""
|
||||||
|
# Standard-Status, falls keine Deklaration gefunden wird
|
||||||
|
status = "UNBEKANNT"
|
||||||
|
|
||||||
|
# Suche nach Status-Deklaration
|
||||||
|
status_pattern = r'\[STATUS:\s*(ERGEBNIS|TEILWEISE|PLAN)\]'
|
||||||
|
match = re.search(status_pattern, content, re.IGNORECASE)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
# Extrahiere den Status
|
||||||
|
status = match.group(1).upper()
|
||||||
|
|
||||||
|
# Entferne die Status-Deklaration aus dem Text
|
||||||
|
content = re.sub(status_pattern, '', content, flags=re.IGNORECASE).strip()
|
||||||
|
|
||||||
|
return content, status
|
||||||
|
|
||||||
|
|
||||||
|
def extract_summary(text: str, max_length: int = 200) -> str:
|
||||||
|
"""
|
||||||
|
Extrahiert eine kurze Zusammenfassung aus einem Text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: Der zu extrahierende Text
|
||||||
|
max_length: Maximale Länge der Zusammenfassung
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Eine kurze Zusammenfassung des Textes
|
||||||
|
"""
|
||||||
|
# Erste Zeilen oder Absätze extrahieren
|
||||||
|
lines = text.split('\n')
|
||||||
|
paragraphs = []
|
||||||
|
current_para = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
if current_para:
|
||||||
|
paragraphs.append(' '.join(current_para))
|
||||||
|
current_para = []
|
||||||
|
else:
|
||||||
|
current_para.append(line)
|
||||||
|
|
||||||
|
if current_para:
|
||||||
|
paragraphs.append(' '.join(current_para))
|
||||||
|
|
||||||
|
# Versuche, den ersten oder zweiten Absatz als Zusammenfassung zu verwenden
|
||||||
|
if paragraphs:
|
||||||
|
summary = paragraphs[0]
|
||||||
|
|
||||||
|
# Falls der erste Absatz zu kurz ist, versuche den zweiten hinzuzufügen
|
||||||
|
if len(summary) < 100 and len(paragraphs) > 1:
|
||||||
|
summary += " " + paragraphs[1]
|
||||||
|
|
||||||
|
# Kürze auf die maximale Länge
|
||||||
|
if len(summary) > max_length:
|
||||||
|
summary = summary[:max_length-3] + "..."
|
||||||
|
|
||||||
|
return summary
|
||||||
|
|
||||||
|
# Fallback auf die ersten Zeichen des Textes
|
||||||
|
if text:
|
||||||
|
return text[:max_length-3] + "..." if len(text) > max_length else text
|
||||||
|
|
||||||
|
return "Keine Zusammenfassung verfügbar"
|
||||||
|
|
@ -45,7 +45,7 @@ def read_file_contents(
|
||||||
# Text-basierte Dateien direkt lesen
|
# Text-basierte Dateien direkt lesen
|
||||||
if file_type == "document":
|
if file_type == "document":
|
||||||
# Einfache Textdateien
|
# Einfache Textdateien
|
||||||
if file_name.endswith(('.txt', '.csv', '.md', '.json')):
|
if file_name.endswith(('.txt', '.md', '.json')):
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
file_contents[file_id] = f.read()
|
file_contents[file_id] = f.read()
|
||||||
_log(add_log_func, workflow_id, f"Datei {file_name} gelesen", "info")
|
_log(add_log_func, workflow_id, f"Datei {file_name} gelesen", "info")
|
||||||
|
|
@ -56,8 +56,7 @@ def read_file_contents(
|
||||||
df = pd.read_excel(file_path)
|
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"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] += f"Spalten: {', '.join(df.columns.tolist())}\n"
|
||||||
file_contents[file_id] += "Erste 5 Zeilen:\n"
|
file_contents[file_id] += df.to_string() # Vollständige Tabelle
|
||||||
file_contents[file_id] += df.head(5).to_string()
|
|
||||||
_log(add_log_func, workflow_id, f"Excel-Datei {file_name} gelesen", "info")
|
_log(add_log_func, workflow_id, f"Excel-Datei {file_name} gelesen", "info")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_log(add_log_func, workflow_id, f"Fehler beim Lesen der Excel-Datei {file_name}: {str(e)}", "error")
|
_log(add_log_func, workflow_id, f"Fehler beim Lesen der Excel-Datei {file_name}: {str(e)}", "error")
|
||||||
|
|
@ -68,8 +67,7 @@ def read_file_contents(
|
||||||
df = pd.read_csv(file_path)
|
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"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] += f"Spalten: {', '.join(df.columns.tolist())}\n"
|
||||||
file_contents[file_id] += "Erste 5 Zeilen:\n"
|
file_contents[file_id] += df.to_string() # Vollständige Tabelle
|
||||||
file_contents[file_id] += df.head(5).to_string()
|
|
||||||
_log(add_log_func, workflow_id, f"CSV-Datei {file_name} gelesen", "info")
|
_log(add_log_func, workflow_id, f"CSV-Datei {file_name} gelesen", "info")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_log(add_log_func, workflow_id, f"Fehler beim Lesen der CSV-Datei {file_name}: {str(e)}", "error")
|
_log(add_log_func, workflow_id, f"Fehler beim Lesen der CSV-Datei {file_name}: {str(e)}", "error")
|
||||||
|
|
@ -84,7 +82,7 @@ def read_file_contents(
|
||||||
text = ""
|
text = ""
|
||||||
for page in reader.pages:
|
for page in reader.pages:
|
||||||
text += page.extract_text() + "\n\n"
|
text += page.extract_text() + "\n\n"
|
||||||
file_contents[file_id] = f"PDF mit {len(reader.pages)} Seiten.\nInhalt:\n{text[:2000]}..."
|
file_contents[file_id] = f"PDF mit {len(reader.pages)} Seiten.\nInhalt:\n{text}"
|
||||||
_log(add_log_func, workflow_id, f"PDF-Datei {file_name} gelesen", "info")
|
_log(add_log_func, workflow_id, f"PDF-Datei {file_name} gelesen", "info")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_log(add_log_func, workflow_id,
|
_log(add_log_func, workflow_id,
|
||||||
|
|
@ -113,246 +111,6 @@ def read_file_contents(
|
||||||
|
|
||||||
return file_contents
|
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:
|
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
|
Erstellt eine formatierte Textdarstellung aller Dateien und ihrer Inhalte
|
||||||
|
|
@ -370,21 +128,14 @@ def format_file_context_text(file_contexts: List[Dict[str, Any]], file_contents:
|
||||||
for file in file_contexts
|
for file in file_contexts
|
||||||
])
|
])
|
||||||
|
|
||||||
# Füge Dateiinhalte hinzu (mit Längenbegrenzung)
|
# Füge Dateiinhalte hinzu (ohne Längenbegrenzung)
|
||||||
for file_id, content in file_contents.items():
|
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_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"
|
file_context_text += f"\n\n==== DATEIINHALT: {file_name} (ID: {file_id}) ====\n"
|
||||||
|
file_context_text += content
|
||||||
# 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
|
return file_context_text
|
||||||
|
|
||||||
|
|
||||||
def prepare_file_contexts(files: List[Dict[str, Any]], upload_dir: str) -> List[Dict[str, Any]]:
|
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
|
Bereitet die Dateikontexte vor und ermittelt die vollen Dateipfade
|
||||||
|
|
@ -421,6 +172,27 @@ def prepare_file_contexts(files: List[Dict[str, Any]], upload_dir: str) -> List[
|
||||||
|
|
||||||
return file_contexts
|
return file_contexts
|
||||||
|
|
||||||
|
async def prepare_message_for_ai(
|
||||||
|
file_contexts: List[Dict[str, Any]],
|
||||||
|
prompt_text: str,
|
||||||
|
file_contents: Dict[str, str],
|
||||||
|
service_aichat
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Bereitet eine vollständige Nachricht mit allen Dateiinhalten für das AI-Modell vor.
|
||||||
|
Benutzt den AI-Connector, um spezielle Datei-Analysen (wie Bild-Analysen) auszuführen.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_contexts: Liste der Dateikontexte mit Metadaten
|
||||||
|
prompt_text: Der Text-Prompt
|
||||||
|
file_contents: Dictionary mit bereits geladenen Dateiinhalten
|
||||||
|
service_aichat: Die AI-Service-Instanz für spezielle Analysen
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Eine vollständig formatierte Nachricht für das AI-Modell
|
||||||
|
"""
|
||||||
|
# Rufe die Methode des AI-Connectors auf, um die Nachricht zu erstellen
|
||||||
|
return await service_aichat.parse_filedata(file_contexts, prompt_text, file_contents)
|
||||||
|
|
||||||
def _log(add_log_func, workflow_id, message, log_type, agent_id=None, agent_name=None):
|
def _log(add_log_func, workflow_id, message, log_type, agent_id=None, agent_name=None):
|
||||||
"""Hilfsfunktion zum Loggen mit unterschiedlichen Log-Funktionen"""
|
"""Hilfsfunktion zum Loggen mit unterschiedlichen Log-Funktionen"""
|
||||||
|
|
@ -434,4 +206,27 @@ def _log(add_log_func, workflow_id, message, log_type, agent_id=None, agent_name
|
||||||
|
|
||||||
# Log über die bereitgestellte Log-Funktion (falls vorhanden)
|
# Log über die bereitgestellte Log-Funktion (falls vorhanden)
|
||||||
if add_log_func and workflow_id:
|
if add_log_func and workflow_id:
|
||||||
add_log_func(workflow_id, message, log_type, agent_id, agent_name)
|
add_log_func(workflow_id, message, log_type, agent_id, agent_name)
|
||||||
|
|
||||||
|
# Die folgenden Funktionen werden nicht mehr benötigt, da partielle Dateiladungen entfallen
|
||||||
|
# Sie sind hier auskommentiert, könnten später aber wieder aktiviert werden
|
||||||
|
|
||||||
|
"""
|
||||||
|
def parse_file_access_commands(agent_text: str) -> List[Dict[str, Any]]:
|
||||||
|
# Diese Funktion wird vorerst nicht benötigt
|
||||||
|
return []
|
||||||
|
|
||||||
|
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]:
|
||||||
|
# Diese Funktion wird vorerst nicht benötigt
|
||||||
|
return None
|
||||||
|
"""
|
||||||
|
|
@ -19,7 +19,8 @@ def create_agent_result(
|
||||||
user_id: int = None
|
user_id: int = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Erstellt ein Ergebnisobjekt basierend auf dem Agententyp und der API-Antwort
|
Erstellt ein Ergebnisobjekt basierend auf dem Agententyp und der API-Antwort.
|
||||||
|
Diese Version berücksichtigt die Status-Deklaration des Agenten.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
workflow_id: ID des Workflows
|
workflow_id: ID des Workflows
|
||||||
|
|
@ -34,10 +35,39 @@ def create_agent_result(
|
||||||
Returns:
|
Returns:
|
||||||
Ein Ergebnisobjekt für den Workflow
|
Ein Ergebnisobjekt für den Workflow
|
||||||
"""
|
"""
|
||||||
|
# Importiere die Hilfsfunktionen aus dem agents-Modul
|
||||||
|
from modules.agentservice_part_agents import update_agent_results_with_status, extract_summary
|
||||||
|
|
||||||
agent_type = agent["type"]
|
agent_type = agent["type"]
|
||||||
agent_id = agent["id"]
|
agent_id = agent["id"]
|
||||||
agent_name = agent["name"]
|
agent_name = agent["name"]
|
||||||
|
|
||||||
|
# Extrahiere den Status aus dem Ergebnis
|
||||||
|
cleaned_content, result_status = update_agent_results_with_status(content)
|
||||||
|
|
||||||
|
# Speichere den Status im Agenten für späteren Zugriff
|
||||||
|
agent["last_result_status"] = result_status
|
||||||
|
|
||||||
|
# Entferne Agent-Präfix aus der Antwort, falls vorhanden
|
||||||
|
agent_prefix = f"[Agent: {agent_name}]"
|
||||||
|
if cleaned_content.startswith(agent_prefix):
|
||||||
|
cleaned_content = cleaned_content[len(agent_prefix):].strip()
|
||||||
|
|
||||||
|
# Titel basierend auf Agent-Type und deklariertem Status erstellen
|
||||||
|
title_prefix = {
|
||||||
|
"ERGEBNIS": "Ergebnis",
|
||||||
|
"TEILWEISE": "Teilweises Ergebnis",
|
||||||
|
"PLAN": "Vorgeschlagener Plan",
|
||||||
|
"UNBEKANNT": "Antwort"
|
||||||
|
}.get(result_status, "Antwort")
|
||||||
|
|
||||||
|
# Titel basierend auf Agent-Attributen erstellen
|
||||||
|
agent_desc = agent.get("description", f"Agent {agent_name}")
|
||||||
|
title = f"{title_prefix}: {agent_desc}"
|
||||||
|
|
||||||
|
# Extrahiere Dateinamen für die Metadaten
|
||||||
|
file_names = [file["name"] for file in file_contexts]
|
||||||
|
|
||||||
# Grundlegende Ergebnisstruktur
|
# Grundlegende Ergebnisstruktur
|
||||||
result = {
|
result = {
|
||||||
"id": f"result_{workflow_id}_{index}",
|
"id": f"result_{workflow_id}_{index}",
|
||||||
|
|
@ -47,57 +77,20 @@ def create_agent_result(
|
||||||
"agent_name": agent_name,
|
"agent_name": agent_name,
|
||||||
"timestamp": datetime.now().isoformat(),
|
"timestamp": datetime.now().isoformat(),
|
||||||
"type": "text", # Standardtyp
|
"type": "text", # Standardtyp
|
||||||
|
"title": title,
|
||||||
|
"content": cleaned_content,
|
||||||
|
"summary": extract_summary(cleaned_content, max_length=200),
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"files_processed": [file["name"] for file in file_contexts],
|
"files_processed": file_names,
|
||||||
"prompt": prompt
|
"prompt": prompt,
|
||||||
|
"agent_type": agent_type,
|
||||||
|
"result_status": result_status,
|
||||||
|
"capabilities": agent.get("capabilities", "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# 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
|
return result
|
||||||
|
|
||||||
|
|
||||||
def add_log(
|
def add_log(
|
||||||
workflows: Dict[str, Dict[str, Any]],
|
workflows: Dict[str, Dict[str, Any]],
|
||||||
workflow_id: str,
|
workflow_id: str,
|
||||||
|
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
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())
|
|
||||||
|
|
@ -46,6 +46,7 @@ numpy>=1.24.2
|
||||||
openpyxl>=3.1.2 # For Excel file support
|
openpyxl>=3.1.2 # For Excel file support
|
||||||
xlrd>=2.0.1 # For legacy Excel file support
|
xlrd>=2.0.1 # For legacy Excel file support
|
||||||
PyPDF2>=3.0.1 # For PDF file support
|
PyPDF2>=3.0.1 # For PDF file support
|
||||||
|
pymupdf==1.22.5 # PDF picture extraction with module fitz
|
||||||
|
|
||||||
# === Agent Service Interface Dependencies ===
|
# === Agent Service Interface Dependencies ===
|
||||||
# HTTP Client and Web Scraping
|
# HTTP Client and Web Scraping
|
||||||
|
|
@ -55,6 +56,7 @@ beautifulsoup4>=4.12.2 # For web scraping
|
||||||
lxml>=4.9.2 # For faster HTML parsing
|
lxml>=4.9.2 # For faster HTML parsing
|
||||||
html5lib>=1.1 # Alternative HTML parser
|
html5lib>=1.1 # Alternative HTML parser
|
||||||
aiofiles>=23.1.0 # Async file operations
|
aiofiles>=23.1.0 # Async file operations
|
||||||
|
Pillow==10.2.0 # image explaining
|
||||||
|
|
||||||
# AI and NLP
|
# AI and NLP
|
||||||
openai>=0.27.4 # OpenAI API client
|
openai>=0.27.4 # OpenAI API client
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue