prod demo

This commit is contained in:
valueon 2025-03-26 13:02:18 +01:00
parent 14f820e27b
commit 982d1e8468
7 changed files with 873 additions and 318 deletions

View file

@ -5,6 +5,9 @@ Agentenauswahlfenster einfacher und mit klick auf Namen
----------------------- OPEN
Issue: Workspace wählen --> Workflow hat nicht die nötigen Agenten und ggf. Prompts
Issue: Prompt-Dropdown - Den Titel dort eintragen, nicht den Text
DOKUS
Doku des Systems für Investoren (Hi-level Struktur, Integrationsfähigkeit und Skalierbarkeit)

View file

@ -13,7 +13,7 @@
{
"mandate_id": 1,
"user_id": 1,
"name": "test_bild.pdf",
"name": "legende_schema.pdf",
"type": "document",
"content_type": "application/pdf",
"size": 299729,

View file

@ -4,7 +4,7 @@
"user_id": 0,
"content": "Recherchiere die aktuellen Markttrends und Entwicklungen im Bereich [THEMA]. Sammle Informationen zu führenden Unternehmen, innovativen Produkten oder Dienstleistungen und aktuellen Herausforderungen. Präsentiere die Ergebnisse in einer strukturierten Übersicht mit relevanten Daten und Quellen.",
"workspace_id": 1,
"created_at": "2025-03-23T19:07:42.838103",
"created_at": "2025-03-26T09:49:59.028815",
"name": "Web Research: Marktforschung",
"id": 1
},
@ -13,7 +13,7 @@
"user_id": 0,
"content": "Analysiere den beigefügten Datensatz zu [THEMA] und identifiziere die wichtigsten Trends, Muster und Auffälligkeiten. Führe statistische Berechnungen durch, um deine Erkenntnisse zu untermauern. Stelle die Ergebnisse in einer klar strukturierten Analyse dar und ziehe relevante Schlussfolgerungen.",
"workspace_id": 1,
"created_at": "2025-03-23T19:07:42.838103",
"created_at": "2025-03-26T09:49:59.029794",
"name": "Analyse: Datenanalyse",
"id": 2
},
@ -22,7 +22,7 @@
"user_id": 0,
"content": "Erstelle ein detailliertes Protokoll unserer Besprechung zum Thema [THEMA]. Erfasse alle besprochenen Punkte, getroffenen Entscheidungen und vereinbarten Maßnahmen. Strukturiere das Protokoll übersichtlich mit Tagesordnungspunkten, Teilnehmerliste und klaren Verantwortlichkeiten für die Follow-up-Aktionen.",
"workspace_id": 1,
"created_at": "2025-03-23T19:07:42.838103",
"created_at": "2025-03-26T09:49:59.029794",
"name": "Protokoll: Besprechungsprotokoll",
"id": 3
},
@ -31,7 +31,7 @@
"user_id": 0,
"content": "Entwickle ein UI/UX-Designkonzept für [ANWENDUNG/WEBSITE]. Berücksichtige die Zielgruppe, Hauptfunktionen und die Markenidentität. Beschreibe die visuelle Gestaltung, Navigation, Interaktionsmuster und Informationsarchitektur. Erläutere, wie das Design die Benutzerfreundlichkeit und das Nutzererlebnis optimiert.",
"workspace_id": 1,
"created_at": "2025-03-23T19:07:42.838103",
"created_at": "2025-03-26T09:49:59.029794",
"name": "Design: UI/UX Design",
"id": 4
},
@ -40,7 +40,7 @@
"user_id": 1,
"content": "Recherchiere die aktuellen Markttrends und Entwicklungen im Bereich [THEMA]. Sammle Informationen zu führenden Unternehmen, innovativen Produkten oder Dienstleistungen und aktuellen Herausforderungen. Präsentiere die Ergebnisse in einer strukturierten Übersicht mit relevanten Daten und Quellen.",
"workspace_id": 1,
"created_at": "2025-03-23T21:43:42.713851",
"created_at": "2025-03-26T09:50:02.181566",
"name": "Web Research: Marktforschung",
"id": 5
},
@ -49,7 +49,7 @@
"user_id": 1,
"content": "Analysiere den beigefügten Datensatz zu [THEMA] und identifiziere die wichtigsten Trends, Muster und Auffälligkeiten. Führe statistische Berechnungen durch, um deine Erkenntnisse zu untermauern. Stelle die Ergebnisse in einer klar strukturierten Analyse dar und ziehe relevante Schlussfolgerungen.",
"workspace_id": 1,
"created_at": "2025-03-23T21:43:42.713851",
"created_at": "2025-03-26T09:50:02.182566",
"name": "Analyse: Datenanalyse",
"id": 6
},
@ -58,16 +58,17 @@
"user_id": 1,
"content": "Erstelle ein detailliertes Protokoll unserer Besprechung zum Thema [THEMA]. Erfasse alle besprochenen Punkte, getroffenen Entscheidungen und vereinbarten Maßnahmen. Strukturiere das Protokoll übersichtlich mit Tagesordnungspunkten, Teilnehmerliste und klaren Verantwortlichkeiten für die Follow-up-Aktionen.",
"workspace_id": 1,
"created_at": "2025-03-23T21:43:42.713851",
"created_at": "2025-03-26T09:50:02.182566",
"name": "Protokoll: Besprechungsprotokoll",
"id": 7
},
{
"mandate_id": 1,
"user_id": 1,
"content": "",
"workspace_id": "2",
"created_at": "2025-03-24T11:50:11.029846",
"id": 9
"content": "Es geht um den Bau von Elektro-Schaltschränken. Kannst Du mir bitte den Inhalt der beiliegenden Datei als Tabelle ausgeben, ausser die gelb markierten zeilen. dann bitte pro Datensatz der Tabelle im internet beim entsprechenden Lieferanten suchen, was der Preis der Position ist. Das erste Wort der 'Description' ist jeweils ein Kurzzeichen des Lieferanten. Dann bitte zusammenrechnen, was der Inhalt der Tabelle kostet. Die Anzahl jedes Datensatzes ist jeweils in der Spalte 'Tot Qty' angegeben.",
"workspace_id": 2,
"created_at": "2025-03-26T09:50:02.182566",
"name": "Offertanafrage",
"id": 8
}
]

View file

@ -6,6 +6,13 @@
"created_at": "2025-03-23T19:07:42.818571",
"id": 1
},
{
"mandate_id": 1,
"user_id": 1,
"name": "Fa. Althaus Use Case",
"created_at": "2025-03-23T21:43:42.694845",
"id": 2
},
{
"mandate_id": 1,
"user_id": 1,

View file

@ -1,7 +1,7 @@
import logging
import re
import requests
from typing import List, Dict, Any, Optional
from typing import List, Dict, Any, Optional, Tuple
from bs4 import BeautifulSoup
import json
import os
@ -314,36 +314,63 @@ class WebScrapingService:
except Exception as e:
logger.error(f"Error during web search: {e}")
return []
# Enhanced connector_aiweb_webscraping.py modifications
# Focus on the scrape_web_data method to ensure consistent behavior
async def scrape_web_data(self, prompt: str) -> str:
"""
Führt Web-Scraping basierend auf dem Prompt durch
Enhanced web scraping function that ensures consistent behavior.
Always performs scraping for prompts and returns structured results.
Args:
prompt: Der Benutzer-Prompt
prompt: The user prompt
Returns:
Gescrapte Webdaten als Text
Scraped web data as text
"""
try:
# Ensure prompt is a string
if isinstance(prompt, list):
prompt = " ".join(prompt) if all(isinstance(item, str) for item in prompt) else str(prompt)
elif not isinstance(prompt, str):
prompt = str(prompt)
# Log the scraping attempt
logger.info(f"Starting web scraping with prompt: {prompt[:400]}...")
# First check for explicit URLs in the prompt
urls = self.extract_urls(prompt)
explicit_urls = self.extract_urls(prompt)
# If no URLs found, perform a search
# Always perform search, even if explicit URLs are found
# This ensures more comprehensive results
keywords = self.extract_keywords(prompt)
logger.info(f"Using keywords for search: {keywords}")
# Search for relevant URLs
search_urls = await self.search_web(keywords)
# Combine explicit URLs with search results, prioritizing explicit URLs
urls = []
# Add explicit URLs first
for url in explicit_urls:
if url not in urls:
urls.append(url)
# Then add search results, avoiding duplicates
for url in search_urls:
if url not in urls:
urls.append(url)
# If no URLs found after both methods, try a simplified search
if not urls:
# Extract keywords for search
keywords = self.extract_keywords(prompt)
logger.info(f"Verwende Keywords für Suche: {keywords}")
# Search for relevant URLs
search_urls = await self.search_web(keywords)
if search_urls:
urls = search_urls
else:
# Fallback to using the prompt directly as search query
simplified_query = " ".join(prompt.split()[:8]) # Use first 8 words
urls = await self.search_web(simplified_query)
simplified_query = " ".join(prompt.split()[:8]) # Use first 8 words
simplified_urls = await self.search_web(simplified_query)
for url in simplified_urls:
if url not in urls:
urls.append(url)
# Scrape content from URLs
results = []
@ -354,7 +381,7 @@ class WebScrapingService:
for url in urls[:self.max_urls]:
try:
# Add a delay between requests to avoid overwhelming servers
# Add a delay between requests
time.sleep(random.uniform(self.min_delay, self.max_delay))
content = self.scrape_url(url)
@ -367,41 +394,105 @@ class WebScrapingService:
except Exception as e:
logger.error(f"Error scraping {url}: {e}")
# Create the final result
# Create the final result with improved structure
if results:
logger.info(f"Successfully scraped {scraped_count} pages")
return "\n\n---\n\n".join(results).strip()
# Format the results in a structured way for better agent understanding
structured_result = f"# Web Scraping Results\n\nScraped {scraped_count} web sources based on: \"{prompt}\"\n\n"
for i, result in enumerate(results):
structured_result += f"## Source {i+1}\n\n{result}\n\n---\n\n"
return structured_result.strip()
else:
# If no real content was scraped, provide simulated data to keep the workflow going
# If no real content was scraped, provide simulated data with clear indication
logger.warning("No content scraped, using simulated data")
simulated_data = f"""
# Simulierte Recherche-Ergebnisse für: {prompt}
# Simulated Web Research Results for: {prompt}
## Markttrends und Entwicklungen
- Die neuesten Analysen zeigen signifikantes Wachstum im Bereich digitaler Transformation
- Experten prognostizieren weiterhin eine positive Entwicklung für Cloud-basierte Lösungen
- Aktuelle Technologien verbessern die Effizienz um durchschnittlich 23%
## Notice
The web scraping system was unable to retrieve real data from the web.
The following information is provided as a placeholder to continue the workflow.
## Führende Unternehmen im Sektor
1. TechInnovators GmbH - Marktanteil 28%
2. FutureWave AG - Marktanteil 22%
3. ProgressTech Ltd. - Marktanteil 17%
## Market Trends and Developments
- Latest analyses show significant growth in digital transformation
- Experts continue to forecast positive development for cloud-based solutions
- Current technologies improve efficiency by an average of 23%
## Innovationen und neue Produkte
- Smart-Integration-Lösungen für bestehende Systeme
- KI-gestützte Automatisierungsprozesse
- Verbesserte Nachhaltigkeitsstandards durch neue Materialien
## Leading Companies in the Sector
1. TechInnovators GmbH - Market share 28%
2. FutureWave AG - Market share 22%
3. ProgressTech Ltd. - Market share 17%
*Hinweis: Dies sind simulierte Daten, da kein echtes Web-Scraping möglich war.*
## Innovations and New Products
- Smart integration solutions for existing systems
- AI-powered automation processes
- Improved sustainability standards through new materials
*Note: This is simulated data provided because no actual web scraping was possible.*
""".strip()
return simulated_data
except Exception as e:
logger.error(f"Fehler beim Web-Scraping: {e}")
error_message = f"Web-Scraping konnte nicht durchgeführt werden: {str(e)}"
return error_message.strip() # Ensure no trailing whitespace
logger.error(f"Error during web scraping: {e}")
error_message = f"Web scraping could not be performed: {str(e)}"
return error_message.strip()
# Additional helper method to ensure the scraper agent always triggers web scraping
async def ensure_scraper_agent_scraping(agent_type: str, moderator_text: str, prompt: str, aiweb_scraper) -> Tuple[bool, str]:
"""
Helper function to ensure scraper agent always triggers web scraping.
To be called from the _run_moderator_cycle method when a scraper agent is selected.
Args:
agent_type: Type of the selected agent
moderator_text: Text from the moderator
prompt: The original prompt
aiweb_scraper: Web scraper service instance
Returns:
Tuple of (was_scraping_performed, scraped_data)
"""
if agent_type != "scraper":
return False, ""
try:
# Log that web scraping is being performed for scraper agent
logger.info(f"Ensuring web scraping for scraper agent with prompt: {prompt[:100]}...")
# Extract a search query from the moderator text if possible
search_query = prompt
if moderator_text:
# Try to extract a more specific query from moderator instructions
query_patterns = [
r"search for [\"'](.+?)[\"']",
r"find information about [\"'](.+?)[\"']",
r"research [\"'](.+?)[\"']",
r"look up [\"'](.+?)[\"']"
]
for pattern in query_patterns:
match = re.search(pattern, moderator_text, re.IGNORECASE)
if match:
extracted_query = match.group(1)
if len(extracted_query) > 10: # Ensure it's a meaningful query
search_query = extracted_query
logger.info(f"Extracted search query from moderator: {search_query}")
break
# Always perform the web scraping
scraped_data = await aiweb_scraper.scrape_web_data(search_query)
# Mark that scraping was performed
return True, scraped_data
except Exception as e:
logger.error(f"Error ensuring web scraping for scraper agent: {e}")
return True, f"Web scraping failed: {str(e)}"
async def close(self):
"""
Schließt alle offenen Ressourcen.

View file

@ -139,7 +139,7 @@ class AgentService:
logger.debug(f"Dateikontext: ID={fc['id']}, Name={fc['name']}, Typ={fc.get('type', 'unbekannt')}")
# Dateiinhalte lesen - EINMAL für den gesamten Workflow
file_contents = file_handling.read_file_contents(
file_contents = await file_handling.read_file_contents(
file_contexts,
self.upload_dir,
workflow_id,
@ -265,205 +265,204 @@ class AgentService:
user_message: str = None
) -> Tuple[bool, bool, str]:
"""
Führt einen Moderator-Zyklus durch: Moderator trifft Entscheidung, Agent wird ausgewählt,
und der ausgewählte Agent wird ausgeführt.
Args:
workflow_id: ID des Workflows
chat_history: Der bisherige Chat-Verlauf
available_agents: Verfügbare Agenten
file_contexts: Dateikontexte
file_contents: Dateiinhalte
is_user_input_continuation: Gibt an, ob dieser Zyklus eine Fortsetzung nach Benutzereingabe ist
user_message: Die Nachricht des Benutzers (falls is_user_input_continuation=True)
Returns:
Tuple mit (workflow_complete, waiting_for_user_input, selected_agent_id)
Improved moderator cycle with structured agent selection and robust user agent handling
"""
# Initialisiere Rückgabewerte
# Initialize return values
workflow_complete = False
waiting_for_user_input = False
selected_agent_id = None
# Moderator-Prompt erstellen
base_prompt = agents.get_moderator_prompt(available_agents)
# Create a structured moderator prompt
moderator_system_prompt = self._create_structured_moderator_prompt(
available_agents,
is_user_input_continuation
)
# Ergänze mit einem Hinweis zur Benutzereingabe, falls zutreffend
if is_user_input_continuation:
moderator_system_prompt = base_prompt + """
Wichtig: Der User Agent hat soeben geantwortet. Berücksichtige diese Antwort in deiner Entscheidung.
Wähle nun den nächsten Agenten aus oder beende den Workflow, wenn die Aufgabe erfüllt ist.
"""
else:
moderator_system_prompt = base_prompt
# Der Moderator wählt den nächsten Agenten aus
# Moderator prompt as system message
moderator_prompt = {
"role": "system",
"content": moderator_system_prompt
}
# Kopie des Chatverlaufs für den Moderator erstellen
# Create a copy of chat history for the moderator
moderator_chat = [moderator_prompt] + chat_history[-self.max_history:]
# Füge eine Zusammenfassung der verfügbaren Agenten hinzu
agent_info = "Verfügbare Agenten:\n"
# Add summary of available agents with their status
agent_info = "Available agents:\n"
for agent_id, agent in available_agents.items():
status = "Bereits verwendet" if agent["used"] else "✗ Noch nicht verwendet"
agent_info += f"- {agent['name']} (Typ: {agent['type']}): {agent.get('capabilities', '')}\n Status: {status}\n"
status = "Used" if agent["used"] else "✗ Not used yet"
agent_info += f"- {agent['name']} (Type: {agent['type']}): {agent.get('capabilities', '')}\n Status: {status}\n"
moderator_chat.append({
"role": "system",
"content": agent_info + "\nWähle den nächsten Agenten aus oder beende den Workflow, wenn die Aufgabe erfüllt ist."
"content": agent_info + "\nSelect the next agent or end the workflow if the task is complete."
})
# Moderator trifft die Entscheidung
# Let the moderator make the decision
try:
moderator_chat = await self._sanitize_messages(moderator_chat)
moderator_decision = await self.service_aichat.call_api(moderator_chat)
moderator_text = moderator_decision["choices"][0]["message"]["content"]
logger.debug(f"Full moderator decision text: {moderator_text}")
# Datenstatistik aktualisieren
# Update data statistics
request_size = len(json.dumps(moderator_chat))
response_size = len(json.dumps(moderator_decision))
self.workflows[workflow_id]["data_stats"]["sent_bytes"] += request_size
self.workflows[workflow_id]["data_stats"]["received_bytes"] += response_size
# Füge die Entscheidung des Moderators zum Chatverlauf hinzu
# Try to parse structured agent selection from the moderator
selected_agent_id = self._parse_structured_agent_selection(
moderator_text,
available_agents
)
# Add the moderator's decision to chat history
logger.debug(f"pmd1 moderator_text (type: {type(moderator_text)}): {moderator_text}")
logger.debug(f"pmd1 chat_history (type: {type(chat_history)}): {chat_history}")
if not isinstance(moderator_text, str):
moderator_text = str(moderator_text)
chat_history.append({
"role": "assistant",
"content": f"[Moderator] {moderator_text}"
})
# Log der Moderator-Entscheidung
self._add_log(workflow_id, f"Moderator-Entscheidung: {moderator_text}", "info")
# Log the moderator's decision
self._add_log(workflow_id, f"Moderator decision: {moderator_text}", "info")
# Finde den nächsten zu verwendenden Agenten
selected_agent_id = agents.find_next_agent(moderator_text, available_agents)
# Prüfe, ob der Moderator eine Anfrage an den User Agent stellt
if selected_agent_id != "user_agent": # Nur prüfen, wenn nicht explizit der User Agent ausgewählt wurde
is_user_agent_query = self._check_for_user_agent_query(moderator_text)
if is_user_agent_query:
self._add_log(workflow_id, "Moderator stellt eine Frage an den User Agent", "info")
selected_agent_id = "user_agent"
waiting_for_user_input = True
# Prüfe, ob der Workflow beendet werden soll
# Check if the workflow should be completed
if selected_agent_id == "WORKFLOW_COMPLETE":
self._add_log(workflow_id, "Moderator hat den Workflow beendet", "success")
self._add_log(workflow_id, "Moderator has ended the workflow", "success")
workflow_complete = True
return workflow_complete, waiting_for_user_input, selected_agent_id
# Prüfe, ob der User-Agent ausgewählt wurde
# Check if User Agent is selected - CRITICAL PATH FOR USER INPUT
if selected_agent_id == "user_agent":
self._add_log(workflow_id, "Warte auf Benutzereingabe für den User Agent", "info", "user_agent", "User Agent")
# Markiere, dass wir auf eine Benutzereingabe warten
self._add_log(
workflow_id,
"Waiting for user input for User Agent",
"info",
"user_agent",
"User Agent"
)
# Mark that we're waiting for user input - this blocks further execution
waiting_for_user_input = True
self.workflows[workflow_id]["waiting_for_user"] = True
# Benutzeranfrage zum Chatverlauf hinzufügen
# Add request to chat history
chat_history.append({
"role": "assistant",
"content": f"[Moderator zu User Agent] {moderator_text}"
"content": f"[Moderator to User Agent] {moderator_text}"
})
# Chat-Verlauf im Workflow aktualisieren
# Update chat history in workflow
self.workflows[workflow_id]["chat_history"] = chat_history
# Workflow-Status speichern
# Save workflow state
results.save_workflow_results(self.workflows, workflow_id, self.results_dir)
# Return immediately to prevent further processing
return workflow_complete, waiting_for_user_input, selected_agent_id
# Prüfe, ob ein anderer Agent ausgewählt wurde
# If no agent was selected, end the workflow
if not selected_agent_id:
self._add_log(workflow_id, "Kein Agent ausgewählt. Beende Workflow.", "warning")
self._add_log(workflow_id, "No agent selected. Ending workflow.", "warning")
workflow_complete = True
return workflow_complete, waiting_for_user_input, selected_agent_id
# Agenten aus der Liste markieren
# Mark the selected agent as used
selected_agent = available_agents[selected_agent_id]
selected_agent["used"] = True
# Agenten-Status aktualisieren
# Update agent status
self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "running"
self._add_log(
workflow_id,
f"Agent '{selected_agent['name']}' beginnt mit der Verarbeitung...",
f"Agent '{selected_agent['name']}' starts processing...",
"start",
selected_agent_id,
selected_agent['name']
)
# Agent-spezifische Anweisungen erstellen
agent_instructions = agents.get_agent_instructions(selected_agent["type"], selected_agent, file_contexts)
# Agent-Prompt erstellen
# Create agent instructions
agent_instructions = agents.get_agent_instructions(
selected_agent["type"],
selected_agent,
file_contexts
)
# Create agent prompt
agent_prompt = agents.create_agent_prompt(selected_agent, agent_instructions)
# Kopie des Chatverlaufs für den Agenten erstellen
# Create agent chat history
agent_chat = [agent_prompt] + chat_history[-self.max_history:]
# Falls der Agent ein Webscraper ist und Scraping notwendig ist
# Handle web scraper agent type specifically
if selected_agent["type"] == "scraper":
self._add_log(workflow_id, "Führe Web-Scraping durch...", "info",
selected_agent_id, selected_agent["name"])
self._add_log(
workflow_id,
"Performing web scraping...",
"info",
selected_agent_id,
selected_agent["name"]
)
# Verwende die Benutzereingabe als Prompt für Scraping, falls verfügbar
# Use user message or initial prompt for scraping
scrape_prompt = user_message if is_user_input_continuation and user_message else \
(chat_history[0]["content"] if chat_history else "")
web_data = await self.service_aiscrap.scrape_web_data(scrape_prompt)
if web_data:
# Ensure web_data has no trailing whitespace
web_data = web_data.strip() if isinstance(web_data, str) else web_data
agent_chat.append({
"role": "system",
"content": f"# Gescrapte Web-Daten\n{web_data}".strip()
})
self._add_log(workflow_id, "Web-Scraping abgeschlossen", "info",
selected_agent_id, selected_agent["name"])
logger.debug("Web Scrape Prompt: "+str(scrape_prompt))
# Ensure scrape_prompt is a string
if not isinstance(scrape_prompt, str):
scrape_prompt = str(scrape_prompt)
# Always perform web scraping for scraper agent type
try:
web_data = await self.service_aiscrap.scrape_web_data(scrape_prompt)
if web_data:
web_data = web_data.strip() if isinstance(web_data, str) else web_data
agent_chat.append({
"role": "system",
"content": f"# Scraped Web Data\n{web_data}".strip()
})
self._add_log(
workflow_id,
"Web scraping completed",
"info",
selected_agent_id,
selected_agent["name"]
)
except Exception as e:
logger.error(f"Error during web scraping: {str(e)}")
self._add_log(
workflow_id,
f"Error during web scraping: {str(e)}",
"error",
selected_agent_id,
selected_agent["name"]
)
# Agent führt seinen Teil aus
# Execute the agent
try:
# Da wir keine partielle Dateiladung benötigen, können wir den Agenten direkt aufrufen
agent_chat = await self._sanitize_messages(agent_chat)
agent_response = await self.service_aichat.call_api(agent_chat)
agent_text = agent_response["choices"][0]["message"]["content"]
# Datenstatistik aktualisieren
# Update data statistics
request_size = len(json.dumps(agent_chat))
response_size = len(json.dumps(agent_response))
self.workflows[workflow_id]["data_stats"]["sent_bytes"] += request_size
self.workflows[workflow_id]["data_stats"]["received_bytes"] += response_size
# Füge die endgültige Antwort des Agenten zum Chatverlauf hinzu
# Add agent's response to chat history
chat_history.append({
"role": "assistant",
"content": agent_text
})
# Prüfe, ob die Antwort des Agenten eine Anfrage an den User Agent enthält
is_user_agent_query = self._check_for_user_agent_query(agent_text)
if is_user_agent_query:
self._add_log(
workflow_id,
f"Agent '{selected_agent['name']}' stellt eine Frage an den User Agent",
"info"
)
# Markiere, dass wir auf eine Benutzereingabe warten
waiting_for_user_input = True
self.workflows[workflow_id]["waiting_for_user"] = True
# Workflow-Status speichern und Chat-Verlauf aktualisieren
self.workflows[workflow_id]["chat_history"] = chat_history
results.save_workflow_results(self.workflows, workflow_id, self.results_dir)
return workflow_complete, waiting_for_user_input, selected_agent_id
# Agent-Ergebnis erstellen - Prompt basierend auf Fortsetzungsart wählen
# Create agent result
prompt_for_result = user_message if is_user_input_continuation else \
(chat_history[0]["content"] if chat_history else "")
@ -481,42 +480,256 @@ class AgentService:
self._add_log(
workflow_id,
f"Agent '{selected_agent['name']}' hat die Verarbeitung abgeschlossen",
f"Agent '{selected_agent['name']}' has completed processing",
"complete",
selected_agent_id,
selected_agent["name"]
)
# Agenten-Status aktualisieren
# Update agent status
self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "completed"
except Exception as e:
logger.error(f"Fehler bei der Ausführung von Agent '{selected_agent['name']}': {str(e)}")
logger.error(f"Error executing agent '{selected_agent['name']}': {str(e)}")
self._add_log(
workflow_id,
f"Fehler bei der Ausführung: {str(e)}",
f"Error during execution: {str(e)}",
"error",
selected_agent_id,
selected_agent["name"]
)
self.workflows[workflow_id]["agent_statuses"][selected_agent_id] = "failed"
# Füge die Fehlermeldung zum Chatverlauf hinzu
# Add error message to chat history
chat_history.append({
"role": "assistant",
"content": f"[Fehler bei Agent '{selected_agent['name']}']: {str(e)}"
"content": f"[Error with agent '{selected_agent['name']}']: {str(e)}"
})
except Exception as e:
logger.error(f"Fehler in der Moderator-Phase: {str(e)}")
self._add_log(workflow_id, f"Fehler in der Moderator-Phase: {str(e)}", "error")
logger.error(f"Error in moderator phase: {str(e)}")
self._add_log(workflow_id, f"Error in moderator phase: {str(e)}", "error")
# Aktualisiere den Chat-Verlauf im Workflow
# Update chat history in workflow
self.workflows[workflow_id]["chat_history"] = chat_history
return workflow_complete, waiting_for_user_input, selected_agent_id
def _create_structured_moderator_prompt(self, available_agents, is_user_input_continuation):
"""
Creates an improved moderator prompt that requests a structured format for agent selection
and enforces proper user agent interaction
"""
# Check if User Agent has been used and confirmed
user_agent_used = False
user_agent_confirmed = False
if "user_agent" in available_agents:
user_agent_used = available_agents["user_agent"].get("used", False)
if user_agent_used:
for chat_entry in available_agents["user_agent"].get("chat_entries", []):
if any(confirmation in chat_entry.lower() for confirmation in
["ja", "yes", "bestätige", "confirm", "stimme zu", "agree", "akzeptiere", "accept"]):
user_agent_confirmed = True
break
base = """You are a moderator of a multi-agent system. Your task is to coordinate the agents
to fully address the request and deliver a concrete final result.
First step is always to ask an agento to coordinate the necessary steps.
IMPORTANT: The workflow should only end when ACTUAL RESULTS have been delivered"""
# Different conditions for ending the workflow
if not user_agent_confirmed:
base += """ AND the User Agent has explicitly confirmed with a 'YES' that they are satisfied with the result.
CRITICALLY IMPORTANT: Before ending the workflow, you MUST ask the User Agent whether they are satisfied with the results,
and they must EXPLICITLY respond with 'YES' or a clear confirmation!"""
# Add structured format requirement
base += """
RESPONSE FORMAT: You must respond with a clear agent selection in the following format:
{
"next_agent": "agent_id", // The ID of the selected agent (e.g., "user_agent", "analyzer", "writer")
"task": "description" // Clear description of the task for the agent
}
DO NOT use any other format for agent selection.
"""
# Add specific instruction for user agent interaction
base += """
IMPORTANT USER AGENT HANDLING:
- When selecting the User Agent, formulate a CLEAR QUESTION that requires their input.
- The User Agent MUST be able to provide a response before the workflow continues.
- Never reference a User Agent question without actually selecting the User Agent.
"""
# Add agent listing and instructions
agents_list = "\nAvailable agents:\n"
for agent_id, agent in available_agents.items():
status = "✓ Already used" if agent["used"] else "✗ Not used yet"
result_status = ""
if agent["used"] and agent.get("last_result_status"):
result_status = f" (Last response: {agent.get('last_result_status')})"
description = agent.get("description", "")
capabilities = agent.get("capabilities", "")
agents_list += f"- {agent['name']} (ID: {agent_id}, Type: {agent['type']}): {capabilities}\n {description}\n Status: {status}{result_status}\n"
# Add decision guidelines
instructions = """
Consider the STATUS declarations of agents when making your decision:
- [STATUS: ERGEBNIS] - The agent has delivered a complete result
- [STATUS: TEILWEISE] - The agent has delivered a partial result, more work is needed
- [STATUS: PLAN] - The agent has delivered a plan, no concrete results yet
Possible decisions:
- Select an agent: Provide a structured response with next_agent and task
"""
if not user_agent_confirmed:
instructions += """- For completion (only if [STATUS: ERGEBNIS] exists): You MUST FIRST ask the User Agent explicitly if they are satisfied with the results.
The User Agent MUST respond with "YES" or clear confirmation before the workflow can end!
IMPORTANT: You MUST NOT end the workflow before the User Agent has explicitly confirmed with "YES"!
Ask the User Agent a CLEAR, DIRECT question whether they are satisfied with the result or need more information.
"""
else:
instructions += """- For completion (only if [STATUS: ERGEBNIS] exists and User Agent has confirmed with "YES"): Select "WORKFLOW_COMPLETE" as next_agent to end the workflow.
IMPORTANT: Since the User Agent has already given their approval, you can now end the workflow if an agent has delivered a concrete [STATUS: ERGEBNIS]!
"""
if is_user_input_continuation:
base += "\nIMPORTANT: The User Agent has just responded. Consider this response in your decision."
return base + agents_list + instructions
def _parse_structured_agent_selection(self, moderator_text, available_agents):
"""
Attempts to parse a structured agent selection from the moderator's text.
Falls back to keyword detection if structured format is not found.
"""
# First try to find a JSON structure
import re
import json
# Look for JSON structure in the text
json_pattern = r'\{[\s\S]*?"next_agent"[\s\S]*?\}'
json_match = re.search(json_pattern, moderator_text)
if json_match:
try:
selection = json.loads(json_match.group(0))
next_agent = selection.get("next_agent")
# Handle workflow completion
if next_agent.lower() in ["workflow_complete", "complete", "end"]:
return "WORKFLOW_COMPLETE"
# Check if the selected agent exists
if next_agent in available_agents:
logger.info(f"Successfully parsed structured agent selection: {next_agent}")
return next_agent
# Handle user agent selection explicitly
if next_agent.lower() in ["user", "user_agent", "human"]:
return "user_agent"
except json.JSONDecodeError:
logger.warning("Failed to parse JSON from moderator text")
# Fallback to the traditional logic with improved reliability
text = moderator_text.lower()
# Check for workflow completion phrases
workflow_complete_phrases = [
"workflow beenden - vollständiges ergebnis erreicht",
"workflow beenden - vollständiges ergebnis",
"vollständiges ergebnis erreicht",
"workflow complete",
"end workflow",
"complete workflow"
]
# Check if the workflow should be completed
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
# Check if User Agent has confirmed
user_agent_confirmed = False
if "user_agent" in available_agents and available_agents["user_agent"].get("used", False):
for chat_entry in available_agents["user_agent"].get("chat_entries", []):
if any(confirmation in chat_entry.lower() for confirmation in
["ja", "yes", "bestätige", "confirm", "stimme zu", "agree"]):
user_agent_confirmed = True
break
# Check for explicit workflow completion
if any(phrase in text for phrase in workflow_complete_phrases):
if result_exists and user_agent_confirmed:
return "WORKFLOW_COMPLETE"
elif not result_exists:
logger.warning("Moderator attempted to end workflow without complete results")
elif not user_agent_confirmed:
logger.warning("Moderator attempted to end workflow without User Agent confirmation")
if "user_agent" in available_agents:
return "user_agent" # Force User Agent selection to get confirmation
# Check for explicit agent selection
if "ich wähle" in text or "i choose" in text or "i select" in text:
for agent_id, agent in available_agents.items():
agent_name_lower = agent["name"].lower()
if agent_name_lower in text:
return agent_id
# Check for User Agent queries
user_agent_phrases = [
"user agent",
"benutzer",
"user",
"human",
"ask the user",
"frage den benutzer"
]
# If text contains User Agent phrases and a question, prioritize User Agent
has_question = "?" in text
has_user_phrase = any(phrase in text for phrase in user_agent_phrases)
if has_question and has_user_phrase:
if "user_agent" in available_agents:
return "user_agent"
# Direct name matching as last resort
for agent_id, agent in available_agents.items():
agent_name_lower = agent["name"].lower()
if agent_name_lower in text:
return agent_id
# If a complete result exists but User Agent hasn't confirmed, prioritize User Agent
if result_exists and not user_agent_confirmed and "user_agent" in available_agents:
return "user_agent"
# If no agent explicitly selected, choose first unused agent
for agent_id, agent in available_agents.items():
if not agent["used"]:
return agent_id
# Last resort: reuse first agent
if available_agents:
return list(available_agents.keys())[0]
return None
def _add_log(
self,
workflow_id: str,
@ -602,49 +815,47 @@ class AgentService:
async def process_user_input(self, workflow_id: str, message: str, additional_files: List[Dict[str, Any]] = None) -> bool:
"""
Verarbeitet eine Benutzereingabe für einen laufenden Workflow.
Ergänzt um die Verfolgung von User Agent Bestätigungen.
Enhanced function to process user input for a running workflow.
Ensures user responses are properly tracked and prevents duplicate messages.
Args:
workflow_id: ID des Workflows
message: Nachricht des Benutzers
additional_files: Liste zusätzlicher Dateien (optional)
workflow_id: ID of the workflow
message: User message
additional_files: List of additional files (optional)
Returns:
bool: True, wenn die Eingabe erfolgreich verarbeitet wurde
bool: True if input was successfully processed
"""
if workflow_id not in self.workflows:
logger.warning(f"Workflow {workflow_id} nicht gefunden")
logger.warning(f"Workflow {workflow_id} not found")
return False
# Prüfen, ob der Workflow auf eine Benutzereingabe wartet
# Check if the workflow is waiting for user input
if not self.workflows[workflow_id].get("waiting_for_user", False):
logger.warning(f"Workflow {workflow_id} wartet nicht auf Benutzereingabe")
logger.warning(f"Workflow {workflow_id} is not waiting for user input")
return False
logger.info(f"Verarbeite Benutzereingabe für Workflow {workflow_id}")
logger.info(f"Processing user input for workflow {workflow_id}")
# Benutzerinfos abrufen
# Get user information
user_info = await self._get_user_info(self.user_id)
user_name = user_info.get("full_name") or user_info.get("username") or f"Benutzer {self.user_id}"
user_name = user_info.get("full_name") or user_info.get("username") or f"User {self.user_id}"
# Log-Eintrag für die Benutzereingabe
# Log entry for user input
self._add_log(
workflow_id,
f"Benutzereingabe empfangen: {message[:50]}{'...' if len(message) > 50 else ''}",
f"User input received: {message[:50]}{'...' if len(message) > 50 else ''}",
"info",
"user_agent",
user_name
)
# Verfolge die User Agent Antwort für Bestätigungsprüfung
# Importiere die Hilfsfunktion aus dem agents-Modul
# Track user agent response in the agents module
from modules.agentservice_part_agents import track_user_agent_response
available_agents = self.workflows[workflow_id].get("available_agents", {})
track_user_agent_response(available_agents, message)
# Rest der Funktion bleibt unverändert...
# Wenn zusätzliche Dateien vorhanden sind, diese verarbeiten und in den Workflow integrieren
# Process additional files if provided
additional_context = ""
if additional_files and len(additional_files) > 0:
file_contexts = file_handling.prepare_file_contexts(additional_files, self.upload_dir)
@ -652,51 +863,78 @@ class AgentService:
file_contexts,
self.upload_dir,
workflow_id,
lambda wid, msg, typ, aid=None, aname=None: self._add_log(wid, msg, typ, aid, aname)
lambda wid, msg, typ, aid=None, aname=None: self._add_log(wid, msg, typ, aid, aname),
self.service_aichat # Pass AI service for image analysis
)
# Formatiere den Dateikontext
# Format file context for inclusion
file_context_text = file_handling.format_file_context_text(file_contexts, file_contents)
additional_context = f"\n\n### Zusätzliche Dateien:\n{file_context_text}"
additional_context = f"\n\n### Additional Files:\n{file_context_text}"
# Log-Eintrag für die zusätzlichen Dateien
# Log entry for additional files
self._add_log(
workflow_id,
f"{len(additional_files)} zusätzliche Dateien hinzugefügt",
f"{len(additional_files)} additional files added",
"info",
"user_agent",
user_name
)
# Füge neue Dateien zu den bestehenden Dateikontexten und -inhalten hinzu
# Add new files to existing file contexts and contents
existing_file_contexts = self.workflows[workflow_id].get("file_contexts", [])
existing_file_contents = self.workflows[workflow_id].get("file_contents", {})
# Alte IDs speichern, um Duplikate zu vermeiden
# Store existing IDs to avoid duplicates
existing_ids = {fc["id"] for fc in existing_file_contexts}
# Neue Dateikontexte hinzufügen (nur wenn ID noch nicht existiert)
# Add new file contexts (only if ID doesn't already exist)
for file_context in file_contexts:
if file_context["id"] not in existing_ids:
existing_file_contexts.append(file_context)
existing_ids.add(file_context["id"])
# Neue Dateiinhalte hinzufügen
# Add new file contents
existing_file_contents.update(file_contents)
# Aktualisierte Dateikontexte und -inhalte im Workflow speichern
# Update file contexts and contents in workflow
self.workflows[workflow_id]["file_contexts"] = existing_file_contexts
self.workflows[workflow_id]["file_contents"] = existing_file_contents
# Kombinierte Nachricht erstellen (Benutzernachricht + ggf. zusätzliche Dateien)
# Combined message (user message + additional files)
combined_message = message + additional_context
# Schätzung der gesendeten Bytes
# Check for duplicate user messages in chat history
# This prevents the user's message from appearing twice
chat_history = self.workflows[workflow_id].get("chat_history", [])
last_message = chat_history[-1] if chat_history else None
# Only add user message if the last message wasn't already from the user with the same content
is_duplicate = False
if last_message and last_message.get("role") == "user" and last_message.get("content", "").startswith(f"[User Agent: {user_name}]"):
# Check for content similarity to avoid duplicates
last_content = last_message.get("content", "").replace(f"[User Agent: {user_name}] ", "", 1)
if last_content == message:
is_duplicate = True
logger.warning(f"Detected duplicate user message, skipping addition to chat history")
# Add user message to chat history if not duplicate
if not is_duplicate:
chat_history.append({
"role": "user",
"content": f"[User Agent: {user_name}] {message}"
})
# Update chat history in workflow
self.workflows[workflow_id]["chat_history"] = chat_history
# Estimate sent bytes
message_size = len(combined_message)
self.workflows[workflow_id]["data_stats"]["sent_bytes"] += message_size
# Fortsetzung des Workflows einleiten
# Wir starten eine neue Aufgabe, um den Workflow fortzusetzen
# Save the current state before continuing
results.save_workflow_results(self.workflows, workflow_id, self.results_dir)
# Start a new task to continue the workflow
asyncio.create_task(
self._continue_workflow_after_user_input(
workflow_id,
@ -708,52 +946,44 @@ class AgentService:
return True
async def _continue_workflow_after_user_input(self, workflow_id: str, user_message: str, user_name: str) -> None:
"""
Setzt einen Workflow nach einer Benutzereingabe fort.
Nutzt die gemeinsame _run_moderator_cycle Methode für die Fortsetzung.
Enhanced function to continue a workflow after user input.
Ensures proper state tracking and prevents duplicate processing.
Args:
workflow_id: ID des Workflows
user_message: Nachricht des Benutzers
user_name: Name des Benutzers
workflow_id: ID of the workflow
user_message: User message
user_name: User name
"""
if workflow_id not in self.workflows:
logger.warning(f"Workflow {workflow_id} nicht gefunden")
logger.warning(f"Workflow {workflow_id} not found")
return
# Workflow-Status aktualisieren
# Update workflow status
self.workflows[workflow_id]["status"] = "running"
# Log-Eintrag für die Fortsetzung
# Log entry for continuation
self._add_log(
workflow_id,
"Workflow wird nach Benutzereingabe fortgesetzt",
"Workflow continues after user input",
"info"
)
# Hole die benötigten Daten aus dem Workflow
# Get required data from workflow
chat_history = self.workflows[workflow_id].get("chat_history", [])
available_agents = self.workflows[workflow_id].get("available_agents", {})
file_contexts = self.workflows[workflow_id].get("file_contexts", [])
file_contents = self.workflows[workflow_id].get("file_contents", {})
# Benutzereingabe zum Chatverlauf hinzufügen
chat_history.append({
"role": "user",
"content": f"[User Agent: {user_name}] {user_message}"
})
# Speichere den aktualisierten Chat-Verlauf im Workflow
self.workflows[workflow_id]["chat_history"] = chat_history
# User-Antwort als Ergebnis speichern
# Create user result (store agent result in the workflow)
user_result = results.create_agent_result(
workflow_id,
{"id": "user_agent", "name": "User Agent", "type": "user"},
len(self.workflows[workflow_id].get("results", [])),
"Benutzereingabe",
file_contexts, # Dateikontexte übergeben
"User input",
file_contexts,
user_message,
self.mandate_id,
self.user_id
@ -761,69 +991,148 @@ class AgentService:
self.workflows[workflow_id]["results"].append(user_result)
# Markiere den User-Agent als verwendet
# Mark User Agent as used
if "user_agent" in available_agents:
available_agents["user_agent"]["used"] = True
# Markiere, dass wir nicht mehr auf eine Benutzereingabe warten
# Mark that we're no longer waiting for user input - CRITICAL
self.workflows[workflow_id]["waiting_for_user"] = False
# Hole die aktuelle Rundenzahl und maximale Rundenzahl
# Get current round and max rounds
current_round = self.workflows[workflow_id].get("current_round", 0)
max_rounds = 12 # Gleicher Wert wie in execute_workflow
max_rounds = 12 # Same value as in execute_workflow
# Prüfe, ob wir schon die maximale Anzahl von Runden erreicht haben
# Check if we've reached the maximum number of rounds
if current_round >= max_rounds:
self._add_log(workflow_id, f"Workflow nach {max_rounds} Runden und Benutzereingabe automatisch beendet", "info")
self._add_log(workflow_id, f"Workflow automatically ended after {max_rounds} rounds and user input", "info")
self.workflows[workflow_id]["status"] = "completed"
self.workflows[workflow_id]["progress"] = 1.0
self.workflows[workflow_id]["completed_at"] = datetime.now().isoformat()
# Speichere Ergebnisse
# Save results
results.save_workflow_results(self.workflows, workflow_id, self.results_dir)
return
# Nächste Runde starten
# Start next round
current_round += 1
self.workflows[workflow_id]["current_round"] = current_round
self._add_log(workflow_id, f"Starte Runde {current_round} nach Benutzereingabe", "info")
self._add_log(workflow_id, f"Starting round {current_round} after user input", "info")
# Führe einen Moderator-Zyklus durch
# Run a moderator cycle
workflow_complete, waiting_for_user_input, selected_agent_id = await self._run_moderator_cycle(
workflow_id,
chat_history,
available_agents,
file_contexts,
file_contents,
is_user_input_continuation=True, # Dies ist eine Fortsetzung nach Benutzereingabe
is_user_input_continuation=True,
user_message=user_message
)
# Workflow abschließen, wenn er vollständig ist oder nicht auf Benutzereingabe wartet
if workflow_complete or not waiting_for_user_input:
# Prüfe, ob wir weitere Runden durchführen sollten
if not workflow_complete and not waiting_for_user_input and current_round < max_rounds:
# Starte eine neue Aufgabe für die nächste Moderator-Runde
asyncio.create_task(
self._continue_workflow_after_user_input(
workflow_id,
"", # Leere Nachricht für die nächste Runde
user_name
)
# Update workflow state based on moderator cycle result
if workflow_complete:
self.workflows[workflow_id]["status"] = "completed"
self._add_log(workflow_id, "Workflow successfully completed after user input", "success")
self.workflows[workflow_id]["progress"] = 1.0
self.workflows[workflow_id]["completed_at"] = datetime.now().isoformat()
elif waiting_for_user_input:
# User Agent was selected again, we're waiting for more input
pass
elif current_round < max_rounds:
# Continue with more agent rounds if not waiting for input
asyncio.create_task(
self._continue_workflow_after_moderator_cycle(
workflow_id,
selected_agent_id
)
else:
# Workflow wurde entweder abgeschlossen oder wartet auf Benutzereingabe
if workflow_complete:
self.workflows[workflow_id]["status"] = "completed"
self._add_log(workflow_id, "Workflow nach Benutzereingabe erfolgreich beendet", "success")
self.workflows[workflow_id]["progress"] = 1.0
self.workflows[workflow_id]["completed_at"] = datetime.now().isoformat()
)
# Fortschritt aktualisieren
# Update progress
if not self.workflows[workflow_id]["completed_at"]:
self.workflows[workflow_id]["progress"] = min(0.9, 0.1 + (current_round / max_rounds) * 0.8)
# Speichere Ergebnisse
# Save results
results.save_workflow_results(self.workflows, workflow_id, self.results_dir)
async def _continue_workflow_after_moderator_cycle(self, workflow_id: str, selected_agent_id: str) -> None:
"""
New method to continue workflow after a moderator cycle when not waiting for user input.
This facilitates smoother agent transitions and addresses the issue of the workflow not continuing properly.
Args:
workflow_id: ID of the workflow
selected_agent_id: ID of the selected agent
"""
if workflow_id not in self.workflows:
logger.warning(f"Workflow {workflow_id} not found")
return
# Get required data from workflow
chat_history = self.workflows[workflow_id].get("chat_history", [])
available_agents = self.workflows[workflow_id].get("available_agents", {})
file_contexts = self.workflows[workflow_id].get("file_contexts", [])
file_contents = self.workflows[workflow_id].get("file_contents", {})
# Get current round and max rounds
current_round = self.workflows[workflow_id].get("current_round", 0)
max_rounds = 12 # Same value as in execute_workflow
# Check if we've reached the maximum number of rounds
if current_round >= max_rounds:
self._add_log(workflow_id, f"Workflow automatically ended after {max_rounds} rounds", "info")
self.workflows[workflow_id]["status"] = "completed"
self.workflows[workflow_id]["progress"] = 1.0
self.workflows[workflow_id]["completed_at"] = datetime.now().isoformat()
# Save results
results.save_workflow_results(self.workflows, workflow_id, self.results_dir)
return
# Start next round
current_round += 1
self.workflows[workflow_id]["current_round"] = current_round
self._add_log(workflow_id, f"Starting round {current_round} (continuation)", "info")
# Run another moderator cycle
workflow_complete, waiting_for_user_input, new_selected_agent_id = await self._run_moderator_cycle(
workflow_id,
chat_history,
available_agents,
file_contexts,
file_contents,
is_user_input_continuation=False
)
# Update workflow state based on moderator cycle result
if workflow_complete:
self.workflows[workflow_id]["status"] = "completed"
self._add_log(workflow_id, "Workflow successfully completed", "success")
self.workflows[workflow_id]["progress"] = 1.0
self.workflows[workflow_id]["completed_at"] = datetime.now().isoformat()
elif waiting_for_user_input:
# User Agent was selected, we're waiting for input
pass
elif current_round < max_rounds:
# Continue with more agent rounds if not waiting for input and not at max rounds
asyncio.create_task(
self._continue_workflow_after_moderator_cycle(
workflow_id,
new_selected_agent_id
)
)
else:
# Max rounds reached
self.workflows[workflow_id]["status"] = "completed"
self._add_log(workflow_id, f"Workflow completed after reaching maximum rounds ({max_rounds})", "info")
self.workflows[workflow_id]["progress"] = 1.0
self.workflows[workflow_id]["completed_at"] = datetime.now().isoformat()
# Update progress
if not self.workflows[workflow_id]["completed_at"]:
self.workflows[workflow_id]["progress"] = min(0.9, 0.1 + (current_round / max_rounds) * 0.8)
# Save results
results.save_workflow_results(self.workflows, workflow_id, self.results_dir)

View file

@ -6,111 +6,200 @@ from typing import Dict, Any, List, Optional, Tuple
# Logger konfigurieren
logger = logging.getLogger(__name__)
def read_file_contents(
async def read_file_contents(
file_contexts: List[Dict[str, Any]],
upload_dir: str,
workflow_id: str = None,
add_log_func = None
add_log_func = None,
ai_service = None # Added AI service parameter for image analysis
) -> Dict[str, str]:
"""
Liest die Inhalte aller Dateien und bereitet sie für die Verwendung vor.
Enhanced function to read the contents of all files with proper image and document analysis.
Args:
file_contexts: Liste der Dateikontexte mit Metadaten
upload_dir: Verzeichnis für Uploads
workflow_id: Optional ID des Workflows für Logging
add_log_func: Optionale Funktion zum Hinzufügen von Logs
file_contexts: List of file contexts with metadata
upload_dir: Directory for uploads
workflow_id: Optional ID of the workflow for logging
add_log_func: Optional function for adding logs
ai_service: Optional AI service for image analysis
Returns:
Dictionary mit Dateiinhalten (file_id -> Inhalt)
Dictionary with file contents (file_id -> content)
"""
file_contents = {}
for file in file_contexts:
file_id = file["id"]
file_name = file["name"]
file_type = file["type"]
file_type = file.get("type", "unknown")
file_path = file.get("path", "")
# Wenn Pfad nicht gesetzt, versuche ihn aus dem Upload-Verzeichnis abzuleiten
# If path is not set, try to derive it from the upload directory
if not file_path and file_name:
possible_path = os.path.join(upload_dir, file_name)
if os.path.exists(possible_path):
file_path = possible_path
logger.debug(f"Pfad für Datei {file_name} gefunden: {file_path}")
file["path"] = file_path # Update the path in context
logger.debug(f"Found path for file {file_name}: {file_path}")
# Dateiinhalt lesen, wenn der Pfad verfügbar ist
# Read file content if path is available
if file_path and os.path.exists(file_path):
try:
# Text-basierte Dateien direkt lesen
if file_type == "document":
# Einfache Textdateien
if file_name.endswith(('.txt', '.md', '.json')):
with open(file_path, 'r', encoding='utf-8') as f:
file_contents[file_id] = f.read()
_log(add_log_func, workflow_id, f"Datei {file_name} gelesen", "info")
# Image files - always perform image analysis if AI service is available
if file_type == "image" or file_name.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
if ai_service:
try:
_log(add_log_func, workflow_id, f"Analyzing image {file_name}...", "info")
image_analysis = await ai_service.analyze_image(file_path, "Describe this image in detail")
file_contents[file_id] = f"Image Analysis:\n{image_analysis}"
_log(add_log_func, workflow_id, f"Image {file_name} analyzed successfully", "info")
except Exception as e:
logger.error(f"Error analyzing image {file_name}: {str(e)}")
_log(add_log_func, workflow_id, f"Error analyzing image {file_name}: {str(e)}", "error")
file_contents[file_id] = f"Image file: {file_name} (Analysis failed: {str(e)})"
else:
file_contents[file_id] = f"Image file: {file_name} (AI analysis not available)"
# Document files
elif file_type == "document" or not file_type:
# Simple text files
if file_name.endswith(('.txt', '.md', '.json', '.xml', '.html', '.htm', '.css', '.js')):
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
file_contents[file_id] = content
_log(add_log_func, workflow_id, f"Text file {file_name} read successfully", "info")
# Excel-Dateien
# Excel files
elif file_name.endswith(('.xlsx', '.xls')):
try:
df = pd.read_excel(file_path)
file_contents[file_id] = f"Excel-Datei mit {len(df)} Zeilen und {len(df.columns)} Spalten.\n"
file_contents[file_id] += f"Spalten: {', '.join(df.columns.tolist())}\n"
file_contents[file_id] += df.to_string() # Vollständige Tabelle
_log(add_log_func, workflow_id, f"Excel-Datei {file_name} gelesen", "info")
file_contents[file_id] = f"Excel file with {len(df)} rows and {len(df.columns)} columns.\n"
file_contents[file_id] += f"Columns: {', '.join(df.columns.tolist())}\n\n"
file_contents[file_id] += df.to_string(index=False) # Full table
_log(add_log_func, workflow_id, f"Excel file {file_name} read successfully", "info")
except Exception as e:
_log(add_log_func, workflow_id, f"Fehler beim Lesen der Excel-Datei {file_name}: {str(e)}", "error")
logger.error(f"Error reading Excel file {file_name}: {str(e)}")
_log(add_log_func, workflow_id, f"Error reading Excel file {file_name}: {str(e)}", "error")
file_contents[file_id] = f"Excel file: {file_name} (Reading failed: {str(e)})"
# CSV-Dateien
# CSV files
elif file_name.endswith('.csv'):
try:
df = pd.read_csv(file_path)
file_contents[file_id] = f"CSV-Datei mit {len(df)} Zeilen und {len(df.columns)} Spalten.\n"
file_contents[file_id] += f"Spalten: {', '.join(df.columns.tolist())}\n"
file_contents[file_id] += df.to_string() # Vollständige Tabelle
_log(add_log_func, workflow_id, f"CSV-Datei {file_name} gelesen", "info")
# Try various encodings and delimiters for robust CSV parsing
try:
df = pd.read_csv(file_path, encoding='utf-8')
except UnicodeDecodeError:
try:
df = pd.read_csv(file_path, encoding='latin1')
except:
df = pd.read_csv(file_path, encoding='cp1252')
file_contents[file_id] = f"CSV file with {len(df)} rows and {len(df.columns)} columns.\n"
file_contents[file_id] += f"Columns: {', '.join(df.columns.tolist())}\n\n"
file_contents[file_id] += df.to_string(index=False) # Full table
_log(add_log_func, workflow_id, f"CSV file {file_name} read successfully", "info")
except Exception as e:
_log(add_log_func, workflow_id, f"Fehler beim Lesen der CSV-Datei {file_name}: {str(e)}", "error")
logger.error(f"Error reading CSV file {file_name}: {str(e)}")
_log(add_log_func, workflow_id, f"Error reading CSV file {file_name}: {str(e)}", "error")
file_contents[file_id] = f"CSV file: {file_name} (Reading failed: {str(e)})"
# PDF-Dateien
# PDF files - with enhanced extraction and AI analysis
elif file_name.endswith('.pdf'):
try:
# Falls PyPDF2 installiert ist
# Try PyPDF2 first
try:
from PyPDF2 import PdfReader
reader = PdfReader(file_path)
num_pages = len(reader.pages)
text = ""
for page in reader.pages:
text += page.extract_text() + "\n\n"
file_contents[file_id] = f"PDF mit {len(reader.pages)} Seiten.\nInhalt:\n{text}"
_log(add_log_func, workflow_id, f"PDF-Datei {file_name} gelesen", "info")
# If AI service is available, also analyze images in PDF
if ai_service:
_log(add_log_func, workflow_id, f"Analyzing PDF images in {file_name}...", "info")
try:
image_analysis_results = await ai_service.extract_and_analyze_pdf_images(
file_path,
"Describe this image in the context of the document"
)
if image_analysis_results:
image_analysis_text = "\n\n=== PDF IMAGE ANALYSIS ===\n"
for result in image_analysis_results:
image_analysis_text += f"\nImage on page {result['page']}: {result['response']}\n"
text += image_analysis_text
_log(add_log_func, workflow_id,
f"Successfully analyzed {len(image_analysis_results)} images in PDF",
"info")
except Exception as img_error:
logger.error(f"Error analyzing PDF images: {str(img_error)}")
_log(add_log_func, workflow_id,
f"Error analyzing PDF images: {str(img_error)}",
"warning")
file_contents[file_id] = f"PDF with {num_pages} pages.\nContent:\n{text}"
_log(add_log_func, workflow_id, f"PDF file {file_name} read successfully", "info")
except ImportError:
_log(add_log_func, workflow_id,
"PyPDF2 nicht installiert. PDF-Inhalt kann nicht extrahiert werden.", "warning")
file_contents[file_id] = f"PDF-Datei (Inhalt nicht verfügbar, PyPDF2 fehlt)"
# Try to use a different PDF library if available
try:
import fitz # PyMuPDF
doc = fitz.open(file_path)
text = ""
for page in doc:
text += page.get_text() + "\n\n"
file_contents[file_id] = f"PDF with {len(doc)} pages.\nContent:\n{text}"
_log(add_log_func, workflow_id, f"PDF file {file_name} read with PyMuPDF", "info")
except ImportError:
_log(add_log_func, workflow_id,
"No PDF library installed. Cannot extract PDF content.", "warning")
file_contents[file_id] = f"PDF file (content not available, PDF libraries missing)"
except Exception as e:
_log(add_log_func, workflow_id, f"Fehler beim Lesen der PDF-Datei {file_name}: {str(e)}", "error")
logger.error(f"Error reading PDF file {file_name}: {str(e)}")
_log(add_log_func, workflow_id, f"Error reading PDF file {file_name}: {str(e)}", "error")
file_contents[file_id] = f"PDF file: {file_name} (Reading failed: {str(e)})"
# Andere Dokumenttypen
# Other document types
else:
_log(add_log_func, workflow_id, f"Nicht unterstütztes Dokumentformat: {file_name}", "warning")
file_contents[file_id] = f"Dateiinhalt nicht verfügbar (Nicht unterstütztes Format)"
try:
# Try to read as binary first to check file type
with open(file_path, 'rb') as f:
first_bytes = f.read(8) # Read first few bytes to identify file type
# Try to read as text if it appears to be text-based
try:
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
file_contents[file_id] = content
_log(add_log_func, workflow_id, f"File {file_name} read as text", "info")
except Exception:
file_contents[file_id] = f"File content not available (Binary or unsupported format)"
_log(add_log_func, workflow_id, f"File {file_name} appears to be binary or has unknown format", "warning")
except Exception as e:
logger.error(f"Error processing file {file_name}: {str(e)}")
_log(add_log_func, workflow_id, f"Error processing file {file_name}: {str(e)}", "error")
file_contents[file_id] = f"File content not available (Error: {str(e)})"
# Bilddateien werden nicht direkt gelesen, nur Metadaten gespeichert
elif file_type == "image":
file_contents[file_id] = f"Bilddatei: {file_name} (Inhalt nicht als Text verfügbar)"
# Other file types - just store metadata
else:
file_contents[file_id] = f"File: {file_name} (Type: {file_type}, content not available)"
_log(add_log_func, workflow_id, f"Unsupported file type: {file_type} for {file_name}", "warning")
except Exception as e:
logger.error(f"Fehler beim Lesen der Datei {file_name}: {str(e)}")
_log(add_log_func, workflow_id, f"Fehler beim Lesen der Datei {file_name}: {str(e)}", "error")
logger.error(f"Error reading file {file_name}: {str(e)}")
_log(add_log_func, workflow_id, f"Error reading file {file_name}: {str(e)}", "error")
file_contents[file_id] = f"File content not available (Error: {str(e)})"
else:
if file_path:
_log(add_log_func, workflow_id, f"Datei {file_name} nicht gefunden: {file_path}", "warning")
_log(add_log_func, workflow_id, f"File {file_name} not found: {file_path}", "warning")
else:
_log(add_log_func, workflow_id, f"Kein Pfad für Datei {file_name} verfügbar", "warning")
file_contents[file_id] = f"Dateiinhalt nicht verfügbar"
_log(add_log_func, workflow_id, f"No path available for file {file_name}", "warning")
file_contents[file_id] = f"File content not available (File not found)"
return file_contents
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
@ -179,21 +268,76 @@ async def prepare_message_for_ai(
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.
Enhanced function to prepare a complete message with all file contents for the AI model.
Ensures proper file content integration and handles image analysis results.
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
file_contexts: List of file contexts with metadata
prompt_text: The text prompt
file_contents: Dictionary with file contents
service_aichat: The AI service instance for special analyses
Returns:
Eine vollständig formatierte Nachricht für das AI-Modell
A fully formatted message for the AI model
"""
# Rufe die Methode des AI-Connectors auf, um die Nachricht zu erstellen
return await service_aichat.parse_filedata(file_contexts, prompt_text, file_contents)
# Use the AI connector to create the message
try:
message = await service_aichat.parse_filedata(file_contexts, prompt_text, file_contents)
# Ensure file contents are correctly integrated
if isinstance(message, dict) and message.get("content") and isinstance(message["content"], list):
# For each file context, ensure its content is included
for file_context in file_contexts:
file_id = file_context["id"]
file_name = file_context["name"]
# Check if file content is already included
file_mentioned = False
for content_item in message["content"]:
if isinstance(content_item, dict) and content_item.get("type") == "text":
if file_name in content_item.get("text", ""):
file_mentioned = True
break
# If file is not mentioned but we have its content, add it
if not file_mentioned and file_id in file_contents:
content = file_contents[file_id]
message["content"].append({
"type": "text",
"text": f"--- FILE: {file_name} ---\n\n{content}"
})
logger.info(f"Added missing file content for {file_name} to message")
return message
except Exception as e:
logger.error(f"Error preparing message for AI: {str(e)}")
# Create a basic message structure if the AI connector fails
message = {
"role": "user",
"content": prompt_text + "\n\n"
}
# Manually add file contents
if file_contents:
file_content_text = "\n\n=== FILE CONTENTS ===\n\n"
for file_id, content in file_contents.items():
# Find file name from contexts
file_name = next((f["name"] for f in file_contexts if f["id"] == file_id), f"File {file_id}")
file_content_text += f"--- FILE: {file_name} ---\n\n{content}\n\n"
# Append to message
if isinstance(message["content"], str):
message["content"] += file_content_text
elif isinstance(message["content"], list):
message["content"].append({
"type": "text",
"text": file_content_text
})
return message
def _log(add_log_func, workflow_id, message, log_type, agent_id=None, agent_name=None):
"""Hilfsfunktion zum Loggen mit unterschiedlichen Log-Funktionen"""
# Log über die Logger-Instanz