img extraction direct and from pdf in all modes

This commit is contained in:
valueon 2025-03-25 21:23:21 +01:00
parent d7cea0398f
commit 78c583d500
11 changed files with 617 additions and 661 deletions

View file

@ -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
} }
] ]

View file

@ -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()]
) )

View file

@ -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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

View file

@ -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:
""" """

View file

@ -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__)

View file

@ -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 = {}

View file

@ -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"

View file

@ -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
"""

View file

@ -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,

View file

@ -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())

View file

@ -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