From 378128c3cefe0fe904b6cc15b9b1977ef0bc3329 Mon Sep 17 00:00:00 2001 From: Ida Dittrich Date: Mon, 12 Jan 2026 16:08:32 +0100 Subject: [PATCH] fix:performance optimization --- modules/features/chatbot/chatbotConstants.py | 206 +++---------- modules/features/chatbot/mainChatbot.py | 307 ++++++++----------- 2 files changed, 174 insertions(+), 339 deletions(-) diff --git a/modules/features/chatbot/chatbotConstants.py b/modules/features/chatbot/chatbotConstants.py index b1c9bbb8..c923e39a 100644 --- a/modules/features/chatbot/chatbotConstants.py +++ b/modules/features/chatbot/chatbotConstants.py @@ -14,15 +14,28 @@ from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, Operati logger = logging.getLogger(__name__) +# Cache for system prompts to avoid regenerating on every request +_cached_analysis_prompt = None +_cached_analysis_prompt_date = None +_cached_final_answer_prompt = None +_cached_final_answer_prompt_date = None + def get_analysis_system_prompt() -> str: """ Get the system prompt for analyzing user input and creating queries. Focuses on understanding the question and determining what queries are needed. + Uses caching to avoid regenerating the prompt on every request. """ + global _cached_analysis_prompt, _cached_analysis_prompt_date current_date = datetime.datetime.now().strftime("%d.%m.%Y") - return f"""Heute ist der {current_date}. + # Return cached prompt if date hasn't changed + if _cached_analysis_prompt is not None and _cached_analysis_prompt_date == current_date: + return _cached_analysis_prompt + + # Regenerate prompt (only when date changes) + _cached_analysis_prompt = f"""Heute ist der {current_date}. Du bist ein Chatbot der Althaus AG. Deine Aufgabe ist es, Benutzeranfragen zu analysieren und zu bestimmen, welche Datenbankabfragen oder Web-Recherchen benötigt werden, um die Frage zu beantworten. @@ -288,16 +301,27 @@ WICHTIG FÜR PROGRESSIVE QUERIES: Du antwortest ausschliesslich auf Deutsch. Nutze kein sz(ß) sondern immer ss. """ + + # Update cache with new date + _cached_analysis_prompt_date = current_date + return _cached_analysis_prompt def get_final_answer_system_prompt() -> str: """ Get the system prompt for generating the final answer. Focuses on formatting, presenting results, and user engagement. + Uses caching to avoid regenerating the prompt on every request. """ + global _cached_final_answer_prompt, _cached_final_answer_prompt_date current_date = datetime.datetime.now().strftime("%d.%m.%Y") - return f"""Heute ist der {current_date}. + # Return cached prompt if date hasn't changed + if _cached_final_answer_prompt is not None and _cached_final_answer_prompt_date == current_date: + return _cached_final_answer_prompt + + # Regenerate prompt (only when date changes) + _cached_final_answer_prompt = f"""Heute ist der {current_date}. Du bist ein Chatbot der Althaus AG. Deine Aufgabe ist es, auf Basis von Datenbank-Ergebnissen und Web-Recherchen hilfreiche, präzise Antworten zu geben. @@ -454,6 +478,10 @@ Am Ende jeder Antwort biete hilfreiche Optionen für nächste Schritte an (Detai Du antwortest ausschliesslich auf Deutsch. Nutze kein sz(ß) sondern immer ss. """ + + # Update cache with new date + _cached_final_answer_prompt_date = current_date + return _cached_final_answer_prompt def get_system_prompt() -> str: @@ -480,152 +508,21 @@ def get_initial_analysis_prompt(user_prompt: str, context: str) -> str: User question: {user_prompt}{context} -⚠️⚠️⚠️ ABSOLUT KRITISCH - DU MUSST MINDESTENS 5-8 ABFRAGEN ERSTELLEN ⚠️⚠️⚠️ -❌ ABSOLUT VERBOTEN: Nur 1 Abfrage zu erstellen! -❌ ABSOLUT VERBOTEN: Nur 2-3 Abfragen zu erstellen! -✓ OBLIGATORISCH: MINDESTENS 5-8 Abfragen bei normalen Anfragen -✓ OBLIGATORISCH: MINDESTENS 8 Abfragen bei Zertifizierungen (UL, CE, TÜV, etc.) +⚠️ WICHTIG - QUERY-ANZAHL FÜR PERFORMANCE ⚠️ +✓ Erstelle MAXIMAL 5 SQL-Queries (für bessere Performance) +✓ Jede Query muss eine andere Strategie verfolgen +✓ Alle Queries werden parallel ausgeführt Analysiere die Benutzeranfrage und bestimme: 1. Ob eine Datenbankabfrage benötigt wird (needsDatabaseQuery) 2. Ob eine Web-Recherche benötigt wird (needsWebResearch) -3. Falls eine Datenbankabfrage benötigt wird: Erstelle MINDESTENS 5-8 separate, vollständige, ausführbare SQL-Abfragen +3. Falls eine Datenbankabfrage benötigt wird: Erstelle MAXIMAL 5 separate, vollständige, ausführbare SQL-Abfragen mit unterschiedlichen Strategien -⚠️⚠️⚠️ KRITISCH - MINDESTENS 5-8 ABFRAGEN PARALLEL ⚠️⚠️⚠️ -- ✓ OBLIGATORISCH: Erstelle IMMER MINDESTENS 5-8 SQL-Queries -- ✓ OBLIGATORISCH: Bei Zertifizierungen (UL, CE, TÜV, etc.) MINDESTENS 8 Queries! -- ✓ OBLIGATORISCH: Alle Queries werden parallel ausgeführt und Ergebnisse kombiniert -- ✓ OBLIGATORISCH: Jede Query MUSS eine andere Strategie verfolgen -- ❌ VERBOTEN: Nur 1 Query zu erstellen - DAS IST ABSOLUT FALSCH UND WIRD ABGELEHNT! -- ❌ VERBOTEN: Nur 2-3 Queries zu erstellen - DAS IST ZU WENIG! -- ❌ VERBOTEN: Alle Queries mit derselben Strategie - verwende unterschiedliche Ansätze! - -⚠️⚠️⚠️ WICHTIG - SO ERSTELLST DU DIE ABFRAGEN ⚠️⚠️⚠️ -1. SCHRITT 1: Analysiere die Anfrage und identifiziere ALLE Suchkriterien -2. SCHRITT 2: Erstelle für JEDES Kriterium mindestens eine Query mit unterschiedlichen Strategien -3. SCHRITT 3: Erstelle zusätzliche Queries mit kombinierten Strategien -4. SCHRITT 4: Erstelle Fallback-Queries ohne spezifische Filter -5. SCHRITT 5: ZÄHLE die Queries im sqlQueries-Array -6. SCHRITT 6: Wenn weniger als 5 (oder 8 bei Zertifizierungen): ERSTELLE WEITERE QUERIES! - -WENN DU NUR 1 QUERY ERSTELLST, IST DIE ANTWORT FALSCH UND WIRD ABGELEHNT! -WENN DU NUR 2-3 QUERIES ERSTELLST, IST DAS ZU WENIG UND WIRD ABGELEHNT! - -⚠️⚠️⚠️ KRITISCH - VERGLEICHSOPERATOREN ("MINDESTENS", "AT LEAST", "≥") ⚠️⚠️⚠️ -- Bei "mindestens X" MUSS JEDE Query höhere Werte einschließen -- Beispiel: "mindestens 10A" → IMMER: (10A OR 12A OR 15A OR 16A OR 18A OR 20A OR 25A OR 30A) -- ❌ VERBOTEN: Nur nach dem exakten Wert suchen -- ⚠️⚠️⚠️ ABSOLUT KRITISCH - MEHRERE ABFRAGEN BEI "MINDESTENS" ⚠️⚠️⚠️ - * Wenn der Nutzer "mindestens X" sagt, MÜSSEN IMMER mehrere Datenbankabfragen erstellt werden - * ✓ OBLIGATORISCH: Erstelle mehrere Queries mit unterschiedlichen Strategien, um ALLE passenden Artikel zu finden - * ✓ OBLIGATORISCH: Du MUSST ALLE Artikel zurückgeben, die die Kriterien erfüllen (z.B. 10A UND 20A bei "mindestens 10A") - * ❌ VERBOTEN: Nur eine Query zu erstellen - das würde Artikel mit höheren Werten übersehen - * Beispiel: Bei "mindestens 10A" müssen Artikel mit 10A, 15A, 20A, 25A, etc. ALLE gefunden werden - -⚠️⚠️⚠️ KRITISCH - ZERTIFIZIERUNGEN ERFORDERN WEB-RECHERCHE ⚠️⚠️⚠️ -- Bei Zertifizierungen (UL, CE, TÜV, VDE, etc.) IMMER needsWebResearch = true setzen -- ❌ VERBOTEN: needsWebResearch = false bei Zertifizierungen - DAS IST FALSCH! -- ✓ OBLIGATORISCH: Bei "UL", "UL-zertifiziert", "UL certified" IMMER needsWebResearch = true! -- Erstelle MINDESTENS 8 progressive SQL-Queries mit unterschiedlichen Strategien - -⚠️⚠️⚠️ OBLIGATORISCH - ERSTELLE DIESE 8 QUERIES BEI ZERTIFIZIERUNGEN ⚠️⚠️⚠️ -Bei "einphasige Netzgeräte mit mindestens 10A, UL-zertifiziert" MUSS du MINDESTENS diese 8 Queries erstellen: - -⚠️ WICHTIG: Jede Query MUSS eine ANDERE Strategie haben! ⚠️ - -Query 1: Spezifische Suche - alle Kriterien kombiniert -Zweck: Exakte Suche nach allen Kriterien gleichzeitig -WHERE: (Netzgerät OR Netzteil OR Power Supply) AND (einphasig OR 1-phasig OR single phase) AND (10A OR 12A OR 15A OR 16A OR 18A OR 20A OR 25A OR 30A) AND (UL OR UL-zertifiziert) -Suche in: Artikelbezeichnung, Artikelbeschrieb, Keywords -Strategie: Alle Filter kombiniert - -Query 2: Erweiterte Suche - breitere Ampere-Patterns + UL -Zweck: Breitere Suche nach Ampere-Angaben mit UL -WHERE: (Netzteil OR Netzgerät) AND (Ampere OR 10A OR 15A OR 20A OR 12A OR 16A OR 18A OR 25A OR 30A) AND (UL OR UL-zertifiziert) -Suche auch nach "Ampere" als Begriff (nicht nur Zahlen) -Strategie: Breitere Ampere-Patterns, weniger spezifisch - -Query 3: Power Supply + single phase + UL (englische Varianten) -Zweck: Suche mit englischen Begriffen -WHERE: (Power Supply OR Stromversorgung) AND (single phase OR einphasig OR 1-phase) AND (10A OR 12A OR 15A OR 16A OR 18A OR 20A OR 25A OR 30A) AND (UL OR UL certified) -Strategie: Alternative Terminologie (englisch/deutsch) - -Query 4: Breitere UL-Suche bei Netzgeräten -Zweck: UL-Suche ohne spezifische Ampere/Phasen-Filter -WHERE: (UL OR UL-zertifiziert OR UL certified) AND (Netzgerät OR Netzteil OR Power Supply OR Stromversorgung) -Suche auch in Keywords-Feld -Strategie: Nur UL + Netzgerät, keine weiteren Filter - -Query 5: Netzgeräte mit ≥10A ohne UL-Filter -Zweck: Fallback falls UL nicht in DB erfasst -WHERE: (Netzgerät OR Netzteil) AND (einphasig OR 1-phasig OR single phase) AND (10A OR 12A OR 15A OR 16A OR 18A OR 20A OR 25A OR 30A) -Strategie: Entferne Zertifizierungsfilter komplett - -Query 6: Zertifizierte Netzgeräte allgemein -Zweck: Breite Suche nach allen Zertifizierungen -WHERE: (UL OR CE OR TÜV OR certified OR zertifiziert) AND (Netzgerät OR Netzteil OR Power Supply) -Strategie: Alle Zertifizierungen, keine spezifischen Filter - -Query 7: COUNT-Abfrage für Statistik -Zweck: Prüfe ob überhaupt Artikel existieren -SELECT COUNT(*) WHERE (Netzgerät OR Netzteil) AND (10A OR 12A OR 15A OR 16A OR 18A OR 20A OR 25A OR 30A) -Strategie: Statistik-Abfrage ohne spezifische Filter - -Query 8: Spezifische Suche nach einphasigen Netzgeräten ohne Zertifizierung -Zweck: Fallback ohne Zertifizierungsfilter -WHERE: (1-Phasig OR einphasig OR single phase) AND (Netzgerät OR Netzteil OR Power Supply) -Strategie: Nur Phasen-Filter, keine Zertifizierung, keine Ampere - -⚠️⚠️⚠️ WICHTIG - DIESE 8 QUERIES SIND BEISPIEL ⚠️⚠️⚠️ -- Passe die Queries an die tatsächliche Anfrage an -- Verwende die gleichen Strategien, aber mit den richtigen Begriffen -- Jede Query muss eine ANDERE Strategie haben -- Erstelle MINDESTENS 8 Queries, auch wenn du mehr brauchst - -❌ VERBOTEN: Nur 1 Query zu erstellen - DAS IST ABSOLUT FALSCH! -❌ VERBOTEN: Nur 2-3 Queries zu erstellen - DAS IST ZU WENIG! -✓ OBLIGATORISCH: MINDESTENS 8 Queries bei Zertifizierungen! - -⚠️⚠️⚠️ KRITISCH - ALLE ERGEBNISSE ZURÜCKGEBEN ⚠️⚠️⚠️ -- ✓ OBLIGATORISCH: Du MUSST ALLE Artikel zurückgeben, die die Kriterien erfüllen -- ❌ VERBOTEN: Nur einen Artikel zurückgeben, wenn mehrere gefunden wurden - -WICHTIG für SQL-Abfragen: -- Verwende IMMER doppelte Anführungszeichen für Spaltennamen -- Bei Lagerbestandsabfragen: IMMER S_RESERVIERTER__BESTAND, verfügbarer Bestand, JOIN mit Lagerplatz-Tabelle -- Bei Lagerbestandsabfragen: IMMER breite Suche mit OR über Artikelkürzel, Artikelnummer UND Artikelbezeichnung -- Filtere Lagerplätze mit 0 Bestand aus (außer bei Gesamtbestand oder spezifischem Lagerplatz) -- Abfragen müssen direkt ausführbar sein (keine Platzhalter) - -⚠️⚠️⚠️ ABSOLUT KRITISCH - JSON-VALIDIERUNG - MUSS BEVOR DU DAS JSON ZURÜCKGIBST ⚠️⚠️⚠️ -DU MUSST DIESE SCHRITTE BEVOR DU DAS JSON ZURÜCKGIBST AUSFÜHREN: - -SCHRITT 1: Zähle die Anzahl der Queries im sqlQueries-Array -SCHRITT 2: Prüfe die Anzahl: - - Wenn needsDatabaseQuery = true UND weniger als 5 Queries: ERSTELLE SOFORT WEITERE QUERIES! - - Bei Zertifizierungen UND weniger als 8 Queries: ERSTELLE SOFORT WEITERE QUERIES! - - Wenn nur 1 Query im Array: DAS IST ABSOLUT FALSCH - ERSTELLE MINDESTENS 5-8 QUERIES! - - Wenn nur 2-3 Queries im Array: DAS IST ZU WENIG - ERSTELLE MINDESTENS 5-8 QUERIES! -SCHRITT 3: Prüfe, ob jede Query eine andere Strategie hat -SCHRITT 4: Wenn nicht genug Queries: ERSTELLE WEITERE QUERIES MIT ANDEREN STRATEGIEN! -SCHRITT 5: Wiederhole SCHRITT 1-4 bis du mindestens 5-8 (oder 8 bei Zertifizierungen) Queries hast -SCHRITT 6: ERST DANN gib das JSON zurück! - -⚠️⚠️⚠️ BEISPIEL FÜR KORREKTE ANZAHL VON QUERIES ⚠️⚠️⚠️ -Bei einer normalen Anfrage (z.B. "Netzgeräte mit 10A"): -- Query 1: Spezifische Suche mit allen Kriterien -- Query 2: Breitere Suche mit alternativen Begriffen -- Query 3: Suche nur nach Hauptkriterien -- Query 4: COUNT-Query für Statistik -- Query 5: Fallback-Query mit minimalen Filtern -→ MINDESTENS 5 Queries! - -Bei Zertifizierungen (z.B. "UL-zertifizierte Netzgeräte"): -- Query 1-5: Wie oben -- Query 6: UL-Suche in Keywords -- Query 7: Zertifizierte Netzgeräte allgemein -- Query 8: Fallback ohne Zertifizierungsfilter -→ MINDESTENS 8 Queries! +⚠️ WICHTIGE REGELN: +- Bei "mindestens X": Höhere Werte einschließen (z.B. "mindestens 10A" → 10A OR 12A OR 15A OR 20A) +- Bei Zertifizierungen (UL, CE, TÜV, etc.): IMMER needsWebResearch = true setzen +- SQL: Doppelte Anführungszeichen für Spaltennamen, JOIN mit Lagerplatz bei Beständen +- Bei Lagerbeständen: Breite Suche über Artikelkürzel, Artikelnummer UND Artikelbezeichnung Return ONLY valid JSON: {{ @@ -637,18 +534,9 @@ Return ONLY valid JSON: "purpose": string (description of what this query retrieves), "table": string (primary table name, e.g., "Artikel", "Lagerplatz_Artikel") }} - ] (array of query objects - MINDESTENS 5-8 bei komplexen Anfragen, MINDESTENS 8 bei Zertifizierungen!), + ] (MAXIMAL 5 queries für Performance!), "reasoning": string }} - -⚠️⚠️⚠️ FINALE PRÜFUNG BEVOR DU DAS JSON ZURÜCKGIBST ⚠️⚠️⚠️ -1. Zähle die Anzahl der Queries im sqlQueries-Array: [ANZAHL] -2. Prüfe: Ist [ANZAHL] >= 5? (Bei Zertifizierungen: >= 8?) -3. Wenn NEIN: ERSTELLE SOFORT WEITERE QUERIES! -4. Prüfe: Hat jede Query eine andere Strategie? -5. Wenn NEIN: ERSTELLE QUERIES MIT ANDEREN STRATEGIEN! -6. Wiederhole bis [ANZAHL] >= 5 (oder >= 8 bei Zertifizierungen) -7. ERST DANN gib das JSON zurück! """ @@ -721,15 +609,13 @@ def get_empty_results_retry_instructions(empty_count: int) -> str: return "" return f""" -⚠️⚠️⚠️ KRITISCH - LEERE ERGEBNISSE ERKANNT ⚠️⚠️⚠️ +⚠️ LEERE ERGEBNISSE ERKANNT ⚠️ -Es wurden {empty_count} Query(s) ausgeführt, die 0 Zeilen zurückgegeben haben. Die bisherige Query-Strategie war nicht erfolgreich. +Es wurden {empty_count} Query(s) ausgeführt, die 0 Zeilen zurückgegeben haben. Versuche alternative Strategien. -DU MUSST JETZT MEHRERE ALTERNATIVE QUERY-STRATEGIEN VERSUCHEN! +⚠️ WICHTIG - MAXIMAL 5 QUERIES FÜR PERFORMANCE ⚠️ -⚠️⚠️⚠️ OBLIGATORISCH - ERSTELLE MINDESTENS 5-8 ALTERNATIVE QUERIES ⚠️⚠️⚠️ - -Erstelle mehrere alternative SQL-Queries mit komplett anderen Strategien: +Erstelle MAXIMAL 5 alternative SQL-Queries mit komplett anderen Strategien: 1. **Breitere Suche ohne Zertifizierung**: Entferne Zertifizierungsfilter komplett - Beispiel: Suche nur nach Netzgerät + einphasig + 10A (ohne UL) @@ -760,7 +646,7 @@ Erstelle mehrere alternative SQL-Queries mit komplett anderen Strategien: - Beispiel: Netzgerät AND (10A OR 15A OR 20A) - keine weiteren Filter WICHTIG: -- Erstelle IMMER mehrere Queries (mindestens 5-8) mit unterschiedlichen Strategien +- Erstelle MAXIMAL 5 Queries mit unterschiedlichen Strategien (für Performance) - Verwende breitere OR-Bedingungen für alternative Begriffe - Entferne zu spezifische Filter, die möglicherweise keine Treffer finden - Suche in Artikelbezeichnung, Artikelbeschrieb UND Keywords-Feld diff --git a/modules/features/chatbot/mainChatbot.py b/modules/features/chatbot/mainChatbot.py index a00da09b..4c93e6a9 100644 --- a/modules/features/chatbot/mainChatbot.py +++ b/modules/features/chatbot/mainChatbot.py @@ -229,7 +229,7 @@ async def chatProcess( async def _execute_queries_parallel(queries: List[Dict[str, Any]]) -> Dict[str, Any]: """ - Execute multiple SQL queries in parallel. + Execute multiple SQL queries in parallel with shared connector. Args: queries: List of query dictionaries, each containing: @@ -243,21 +243,24 @@ async def _execute_queries_parallel(queries: List[Dict[str, Any]]) -> Dict[str, - "query_1_data", "query_2_data", etc.: Raw data arrays - "query_1_error", "query_2_error", etc.: Error messages if query failed """ - async def execute_single_query(idx: int, query_info: Dict[str, Any]): - """Execute a single query and return result.""" - connector = PreprocessorConnector() - try: - query_text = query_info.get("query", "") - result = await connector.executeQuery(query_text, return_json=True) - await connector.close() - return idx, result, None - except Exception as e: - await connector.close() - return idx, None, str(e) - - # Execute all queries in parallel - tasks = [execute_single_query(i, q) for i, q in enumerate(queries)] - results = await asyncio.gather(*tasks, return_exceptions=True) + # Create single connector instance to reuse across all queries + connector = PreprocessorConnector() + try: + async def execute_single_query(idx: int, query_info: Dict[str, Any]): + """Execute a single query using shared connector.""" + try: + query_text = query_info.get("query", "") + result = await connector.executeQuery(query_text, return_json=True) + return idx, result, None + except Exception as e: + return idx, None, str(e) + + # Execute all queries in parallel with shared connector + tasks = [execute_single_query(i, q) for i, q in enumerate(queries)] + results = await asyncio.gather(*tasks, return_exceptions=True) + finally: + # Close connector once after all queries complete + await connector.close() # Process results into dictionary query_results = {} @@ -915,108 +918,11 @@ async def _processChatbotMessage( logger.warning("Certification detected but needsWebResearch is false - forcing to true") needsWebResearch = True - # Validate query count - retry if too few queries (iterative retry up to 3 attempts) - min_queries_required = 8 if has_certification else 5 - max_retry_attempts = 3 - retry_attempt = 0 - - while needsDatabaseQuery and len(sql_queries) < min_queries_required and retry_attempt < max_retry_attempts: - retry_attempt += 1 - logger.warning(f"Only {len(sql_queries)} queries created, but {min_queries_required} required. Retry attempt {retry_attempt}/{max_retry_attempts}...") - await _emit_log_and_event( - interfaceDbChat, - workflowId, - event_manager, - f"Zu wenige Abfragen erstellt ({len(sql_queries)} statt {min_queries_required}). Versuch {retry_attempt}/{max_retry_attempts}: Erstelle alternative Strategien...", - log_type="warning" - ) - - # Build progressively stronger retry prompt - retry_context = f"{context}\n\n" - if retry_attempt == 1: - retry_context += "⚠️⚠️⚠️ KRITISCH - ZU WENIGE ABFRAGEN ERSTELLT ⚠️⚠️⚠️\n" - elif retry_attempt == 2: - retry_context += "⚠️⚠️⚠️ ABSOLUT KRITISCH - IMMER NOCH ZU WENIGE ABFRAGEN ⚠️⚠️⚠️\n" - else: - retry_context += "⚠️⚠️⚠️ LETZTER VERSUCH - DU MUSST JETZT MINDESTENS 5-8 ABFRAGEN ERSTELLEN ⚠️⚠️⚠️\n" - - retry_context += f"Du hast nur {len(sql_queries)} Abfrage(n) erstellt, aber es werden MINDESTENS {min_queries_required} benötigt!\n" - retry_context += f"Dies ist bereits Versuch {retry_attempt} von {max_retry_attempts}!\n" - retry_context += "ERSTELLE JETZT MINDESTENS 5-8 ABFRAGEN MIT VERSCHIEDENEN STRATEGIEN!\n" - retry_context += "JEDE Query muss eine ANDERE Strategie verfolgen:\n" - retry_context += "- Query 1: Spezifische Suche mit allen Kriterien\n" - retry_context += "- Query 2: Breitere Suche mit alternativen Begriffen\n" - retry_context += "- Query 3: Suche ohne Zertifizierungsfilter\n" - retry_context += "- Query 4: Suche nur nach Hauptkriterien\n" - retry_context += "- Query 5: COUNT-Query für Statistik\n" - if has_certification: - retry_context += "- Query 6: UL-Suche in Keywords\n" - retry_context += "- Query 7: Zertifizierte Netzgeräte allgemein\n" - retry_context += "- Query 8: Fallback mit minimalen Filtern\n" - retry_context += "\n⚠️ ABSOLUT VERBOTEN: Nur 1 Query zu erstellen! ⚠️\n" - retry_context += "⚠️ ABSOLUT VERBOTEN: Alle Queries mit derselben Strategie! ⚠️\n" - retry_context += "⚠️ BEVOR DU DAS JSON ZURÜCKGIBST: Zähle die Queries im sqlQueries-Array! ⚠️\n" - retry_context += "⚠️ WENN WENIGER ALS 8: ERSTELLE WEITERE QUERIES! ⚠️\n" - - retry_analysis_prompt = get_initial_analysis_prompt(userInput.prompt, retry_context) - retry_analysis_result = await method_ai.process({ - "aiPrompt": retry_analysis_prompt, - "documentList": None, - "resultType": "json", - "simpleMode": True - }) - - retry_analysis_content = None - if retry_analysis_result.success and retry_analysis_result.documents: - retry_analysis_content = retry_analysis_result.documents[0].documentData - if isinstance(retry_analysis_content, bytes): - retry_analysis_content = retry_analysis_content.decode('utf-8') - - if retry_analysis_content: - retry_analysis = _extractJsonFromResponse(retry_analysis_content) - if retry_analysis: - retry_needsDatabaseQuery = retry_analysis.get("needsDatabaseQuery", False) - retry_needsWebResearch = retry_analysis.get("needsWebResearch", False) - retry_sql_queries = retry_analysis.get("sqlQueries", []) - - logger.info(f"Retry attempt {retry_attempt}: Got {len(retry_sql_queries)} queries (required: {min_queries_required})") - - if retry_needsDatabaseQuery and len(retry_sql_queries) >= min_queries_required: - logger.info(f"Retry successful: {len(retry_sql_queries)} queries created") - needsDatabaseQuery = retry_needsDatabaseQuery - needsWebResearch = retry_needsWebResearch or needsWebResearch # Keep web research if already set - sql_queries = retry_sql_queries - reasoning = retry_analysis.get("reasoning", reasoning) - await _emit_log_and_event( - interfaceDbChat, - workflowId, - event_manager, - f"Alternative Strategie erfolgreich: {len(sql_queries)} Abfrage(n) erstellt", - log_type="info" - ) - break # Success, exit retry loop - else: - logger.warning(f"Retry attempt {retry_attempt} still insufficient: {len(retry_sql_queries)} queries (required: {min_queries_required})") - # Update sql_queries even if insufficient, so next iteration has context - if retry_needsDatabaseQuery and len(retry_sql_queries) > len(sql_queries): - sql_queries = retry_sql_queries - needsDatabaseQuery = retry_needsDatabaseQuery - needsWebResearch = retry_needsWebResearch or needsWebResearch - else: - logger.warning(f"Retry attempt {retry_attempt}: Failed to parse JSON response") - else: - logger.warning(f"Retry attempt {retry_attempt}: No content in response") - - # Final check: if still insufficient after all retries, log warning but continue - if needsDatabaseQuery and len(sql_queries) < min_queries_required: - logger.error(f"CRITICAL: After {max_retry_attempts} retry attempts, only {len(sql_queries)} queries created (required: {min_queries_required}). Continuing with insufficient queries.") - await _emit_log_and_event( - interfaceDbChat, - workflowId, - event_manager, - f"⚠️ WARNUNG: Nach {max_retry_attempts} Versuchen nur {len(sql_queries)} Abfrage(n) erstellt (benötigt: {min_queries_required}). Setze mit reduzierten Abfragen fort.", - log_type="warning" - ) + # Limit query count to maximum 5 for performance + max_queries_allowed = 5 + if needsDatabaseQuery and len(sql_queries) > max_queries_allowed: + logger.info(f"Limiting queries from {len(sql_queries)} to {max_queries_allowed} for performance") + sql_queries = sql_queries[:max_queries_allowed] logger.info(f"Analysis: DB={needsDatabaseQuery}, Web={needsWebResearch}, SQL queries={len(sql_queries)}") @@ -1078,6 +984,32 @@ async def _processChatbotMessage( queryResults = {} webResearchResults = "" + # Start web research early in parallel with DB queries if needed + web_research_task = None + if needsWebResearch: + # Start with basic query (will enrich later with DB results if available) + basic_web_query = _buildWebResearchQuery(userInput.prompt, workflow.messages, None) + logger.info(f"Starting web research in parallel with DB queries using basic query: '{basic_web_query}'") + await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, "Suche im Internet nach Informationen...") + + async def perform_web_research(): + """Perform web research and return results.""" + try: + researchResult = await services.web.performWebResearch( + prompt=basic_web_query, + urls=[], + country=None, + language=userInput.userLanguage or "de", + researchDepth="general", + operationId=None + ) + return json.dumps(researchResult, ensure_ascii=False, indent=2) if isinstance(researchResult, dict) else str(researchResult) + except Exception as e: + logger.error(f"Web research failed: {e}", exc_info=True) + return f"Web research error: {str(e)}" + + web_research_task = asyncio.create_task(perform_web_research()) + # Execute database queries in parallel if needsDatabaseQuery and sql_queries: logger.info(f"Executing {len(sql_queries)} database queries in parallel...") @@ -1151,10 +1083,11 @@ async def _processChatbotMessage( (len(successful_queries) > 0 or len(failed_queries) == 0) # Either we have successful queries or no failures (queries executed but empty) ) - # Iterative retry loop: try up to 3 times with different strategies - max_empty_retry_attempts = 3 + # Iterative retry loop: try up to 2 times with different strategies + max_empty_retry_attempts = 2 empty_retry_attempt = 0 original_sql_queries_count = len(sql_queries) + previous_retry_rows = 0 while should_retry and empty_retry_attempt < max_empty_retry_attempts: empty_retry_attempt += 1 @@ -1188,7 +1121,7 @@ async def _processChatbotMessage( retry_context += f"Die bisherigen {len(sql_queries)} Abfragen haben 0 Zeilen zurückgegeben.\n" retry_context += f"{empty_results_instructions}\n" retry_context += f"Dies ist bereits Versuch {empty_retry_attempt} von {max_empty_retry_attempts}!\n" - retry_context += "Erstelle JETZT mehrere alternative SQL-Queries (mindestens 5-8) mit komplett anderen Strategien:\n" + retry_context += "Erstelle JETZT MAXIMAL 5 alternative SQL-Queries mit komplett anderen Strategien (für Performance):\n" if empty_retry_attempt == 1: retry_context += "- Breitere Suche ohne zu spezifische Filter\n" @@ -1230,6 +1163,10 @@ async def _processChatbotMessage( retry_analysis = _extractJsonFromResponse(retry_analysis_content) if retry_analysis and retry_analysis.get("needsDatabaseQuery", False): retry_sql_queries = retry_analysis.get("sqlQueries", []) + # Limit to maximum 5 queries for performance + if len(retry_sql_queries) > 5: + logger.info(f"Limiting retry queries from {len(retry_sql_queries)} to 5 for performance") + retry_sql_queries = retry_sql_queries[:5] if retry_sql_queries: logger.info(f"Executing {len(retry_sql_queries)} retry queries (attempt {empty_retry_attempt}) with alternative strategies...") await _emit_log_and_event( @@ -1287,20 +1224,32 @@ async def _processChatbotMessage( ) should_retry = False # Stop retry loop, we found results break - else: - # Still no results, continue to next attempt + elif retry_rows > previous_retry_rows: + # Made some progress (found more rows than before) - continue + previous_retry_rows = retry_rows await _emit_log_and_event( interfaceDbChat, workflowId, event_manager, - f"Versuch {empty_retry_attempt}: Immer noch keine Ergebnisse. Versuche nächste Strategie...", + f"Versuch {empty_retry_attempt}: Fortschritt erzielt ({retry_rows} Zeilen gefunden). Versuche weitere Strategie...", + log_type="info" + ) + else: + # No progress made - stop retrying + await _emit_log_and_event( + interfaceDbChat, + workflowId, + event_manager, + f"Versuch {empty_retry_attempt}: Keine Ergebnisse gefunden. Beende Retry-Versuche.", log_type="warning" ) + should_retry = False # Stop retry loop, no progress + break except Exception as retry_error: logger.error(f"Error executing retry queries (attempt {empty_retry_attempt}): {retry_error}", exc_info=True) # Continue to next attempt even on error - # Check if we should continue retrying + # Check if we should continue retrying (already handled in break conditions above) if empty_retry_attempt >= max_empty_retry_attempts: logger.warning(f"Reached maximum empty retry attempts ({max_empty_retry_attempts}), stopping retry loop") await _emit_log_and_event( @@ -1322,33 +1271,18 @@ async def _processChatbotMessage( log_type="error" ) - # Execute web research - if needsWebResearch: - logger.info("Performing web research...") - await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, "Suche im Internet nach Informationen...") - + # Wait for web research to complete (if it was started in parallel) + if web_research_task: try: - # Rebuild enriched query with database results if available (better product context) - web_research_query = _buildWebResearchQuery( - userInput.prompt, - workflow.messages, - queryResults if queryResults else None - ) - - logger.info(f"Using enriched web research query: '{web_research_query}'") - - researchResult = await services.web.performWebResearch( - prompt=web_research_query, - urls=[], - country=None, - language=userInput.userLanguage or "de", - researchDepth="general", - operationId=None - ) - webResearchResults = json.dumps(researchResult, ensure_ascii=False, indent=2) if isinstance(researchResult, dict) else str(researchResult) - await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, "Internet-Recherche abgeschlossen") + webResearchResults = await web_research_task + if webResearchResults and not webResearchResults.startswith("Web research error"): + logger.info("Web research completed successfully") + await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, "Internet-Recherche abgeschlossen") + else: + logger.warning("Web research completed with errors") + await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, "Internet-Recherche fehlgeschlagen", log_type="warning") except Exception as e: - logger.error(f"Web research failed: {e}", exc_info=True) + logger.error(f"Error waiting for web research: {e}", exc_info=True) webResearchResults = f"Web research error: {str(e)}" await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, "Internet-Recherche fehlgeschlagen", log_type="warning") @@ -1364,11 +1298,11 @@ async def _processChatbotMessage( # Build prompt for final answer system_prompt = get_final_answer_system_prompt() - # Build answer context with query results - answerContext = f"User question: {userInput.prompt}{context}\n\n" + # Build answer context with query results using efficient list-based building + answer_context_parts = [f"User question: {userInput.prompt}{context}\n"] # Add database results - organize by query with metadata - db_results_part = "" + db_results_parts = [] if queryResults: successful_results = [] error_results = [] @@ -1415,19 +1349,31 @@ async def _processChatbotMessage( if "error" in queryResults: error_results.append(f"Allgemeiner Fehler: {queryResults['error']}") + # Build db_results_part efficiently if successful_results: - db_results_part = "\n\nDATENBANK-ERGEBNISSE:\n" + "\n\n".join(successful_results) - answerContext += "DATENBANK-ERGEBNISSE:\n" + "\n\n".join(successful_results) + "\n\n" + db_results_parts.append("\n\nDATENBANK-ERGEBNISSE:\n") + db_results_parts.append("\n\n".join(successful_results)) + answer_context_parts.append("DATENBANK-ERGEBNISSE:\n") + answer_context_parts.append("\n\n".join(successful_results)) + answer_context_parts.append("\n") if error_results: - db_results_part += "\n\nDATENBANK-FEHLER:\n" + "\n".join(error_results) - answerContext += "DATENBANK-FEHLER:\n" + "\n".join(error_results) + "\n\n" + db_results_parts.append("\n\nDATENBANK-FEHLER:\n") + db_results_parts.append("\n".join(error_results)) + answer_context_parts.append("DATENBANK-FEHLER:\n") + answer_context_parts.append("\n".join(error_results)) + answer_context_parts.append("\n") + + db_results_part = "".join(db_results_parts) # Add web research results web_results_part = "" if webResearchResults: web_results_part = f"\n\nINTERNET-RECHERCHE:\n{webResearchResults}" - answerContext += f"INTERNET-RECHERCHE:\n{webResearchResults}\n\n" + answer_context_parts.append(f"INTERNET-RECHERCHE:\n{webResearchResults}\n") + + # Join answer context efficiently + answerContext = "".join(answer_context_parts) # Check if we have any actual data successful_query_keys = [k for k in queryResults.keys() if k.startswith("query_") and not k.endswith("_error") and not k.endswith("_data")] @@ -1447,39 +1393,42 @@ async def _processChatbotMessage( logger.info(f"Total articles found across all queries: {total_articles_found}") - # Add explicit article count information to prompt + # Add explicit article count information to prompt (using efficient list building) if total_articles_found > 0: - article_count_info = f"\n\n⚠️⚠️⚠️ WICHTIG - ARTIKELANZAHL ⚠️⚠️⚠️\n" - article_count_info += f"In den DATENBANK-ERGEBNISSEN oben wurden INSGESAMT {total_articles_found} Artikel gefunden.\n" - article_count_info += f"DU MUSST ALLE {total_articles_found} Artikel in deiner Antwort zeigen!\n" + article_count_parts = [ + "\n\n⚠️⚠️⚠️ WICHTIG - ARTIKELANZAHL ⚠️⚠️⚠️\n", + f"In den DATENBANK-ERGEBNISSEN oben wurden INSGESAMT {total_articles_found} Artikel gefunden.\n", + f"DU MUSST ALLE {total_articles_found} Artikel in deiner Antwort zeigen!\n" + ] if total_articles_found <= 20: - article_count_info += f"Zeige ALLE {total_articles_found} Artikel in einer Tabelle.\n" + article_count_parts.append(f"Zeige ALLE {total_articles_found} Artikel in einer Tabelle.\n") else: - article_count_info += f"Zeige die ersten 20 Artikel in einer Tabelle + Hinweis auf weitere {total_articles_found - 20} Artikel.\n" - article_count_info += f"❌ VERBOTEN: Nur einen Artikel zu zeigen, wenn {total_articles_found} gefunden wurden!\n" - article_count_info += f"✓ OBLIGATORISCH: Zeige ALLE {total_articles_found} Artikel!\n" + article_count_parts.append(f"Zeige die ersten 20 Artikel in einer Tabelle + Hinweis auf weitere {total_articles_found - 20} Artikel.\n") + article_count_parts.extend([ + f"❌ VERBOTEN: Nur einen Artikel zu zeigen, wenn {total_articles_found} gefunden wurden!\n", + f"✓ OBLIGATORISCH: Zeige ALLE {total_articles_found} Artikel!\n" + ]) + article_count_info = "".join(article_count_parts) if db_results_part: db_results_part = article_count_info + db_results_part else: db_results_part = article_count_info - # Add warning messages if needed + # Add warning messages if needed (using efficient list building) + warning_parts = [] if not has_query_results and needsDatabaseQuery: - if db_results_part: - db_results_part += "\n\nWICHTIG: Es wurden KEINE Datenbank-Ergebnisse gefunden. Die Datenbankabfrage wurde nicht ausgeführt oder hat keine Ergebnisse zurückgegeben." - else: - db_results_part = "\n\nWICHTIG: Es wurden KEINE Datenbank-Ergebnisse gefunden. Die Datenbankabfrage wurde nicht ausgeführt oder hat keine Ergebnisse zurückgegeben." + warning_parts.append("\n\nWICHTIG: Es wurden KEINE Datenbank-Ergebnisse gefunden. Die Datenbankabfrage wurde nicht ausgeführt oder hat keine Ergebnisse zurückgegeben.") if has_only_errors: - if db_results_part: - db_results_part += "\n\n⚠️⚠️⚠️ KRITISCH - ALLE QUERIES FEHLGESCHLAGEN ⚠️⚠️⚠️\n" + \ - "ALLE Datenbankabfragen sind fehlgeschlagen. Es gibt KEINE gültigen Daten aus der Datenbank.\n" + \ - "DU DARFST KEINE DATEN ERFINDEN! Schreibe stattdessen: 'Es wurden keine Artikel gefunden' oder 'Die Datenbankabfrage ist fehlgeschlagen'." - else: - db_results_part = "\n\n⚠️⚠️⚠️ KRITISCH - ALLE QUERIES FEHLGESCHLAGEN ⚠️⚠️⚠️\n" + \ - "ALLE Datenbankabfragen sind fehlgeschlagen. Es gibt KEINE gültigen Daten aus der Datenbank.\n" + \ - "DU DARFST KEINE DATEN ERFINDEN! Schreibe stattdessen: 'Es wurden keine Artikel gefunden' oder 'Die Datenbankabfrage ist fehlgeschlagen'." + warning_parts.extend([ + "\n\n⚠️⚠️⚠️ KRITISCH - ALLE QUERIES FEHLGESCHLAGEN ⚠️⚠️⚠️\n", + "ALLE Datenbankabfragen sind fehlgeschlagen. Es gibt KEINE gültigen Daten aus der Datenbank.\n", + "DU DARFST KEINE DATEN ERFINDEN! Schreibe stattdessen: 'Es wurden keine Artikel gefunden' oder 'Die Datenbankabfrage ist fehlgeschlagen'." + ]) + + if warning_parts: + db_results_part = db_results_part + "".join(warning_parts) if db_results_part else "".join(warning_parts) # Use the function from constants file to build the prompt answer_prompt = get_final_answer_prompt_with_results(