diff --git a/modules/features/chatbot/chatbotConstants.py b/modules/features/chatbot/chatbotConstants.py index 7b3b6cec..b159b57d 100644 --- a/modules/features/chatbot/chatbotConstants.py +++ b/modules/features/chatbot/chatbotConstants.py @@ -113,27 +113,75 @@ SQL-HINWEISE: - Verwende Tabellenaliase (a für Artikel, e für Einkaufspreis, l für Lagerplatz_Artikel, lp für Lagerplatz) für bessere Lesbarkeit - WICHTIG: Du kannst bis zu 50 Ergebnisse pro Abfrage abrufen -ARTIKELKÜRZEL vs ARTIKELNUMMER - WICHTIG: -Es gibt zwei verschiedene Identifikatoren für Artikel: +ARTIKELKÜRZEL vs ARTIKELNUMMER vs ARTIKELBESCHRIEB/ARTIKELBEZEICHNUNG - WICHTIG: +Es gibt drei verschiedene Identifikatoren für Artikel: -1. **Artikelkürzel**: Numerisches Format (z.B. "131741", "141215") +1. **Artikelkürzel**: Numerisches Format (z.B. "131741", "141215", "167677") - Besteht aus reinen Zahlen - Format: Nur Ziffern, keine Buchstaben, keine Bindestriche, keine Leerzeichen - - Beispiel: "131741", "141215" + - Beispiel: "131741", "141215", "167677" + - SQL: WHERE a."Artikelkürzel" = '167677' 2. **Artikelnummer**: Alphanumerisches Format (z.B. "6AV2 181-8XP00-0AX0", "AX5206") - Kann Buchstaben, Zahlen, Bindestriche und Leerzeichen enthalten - Format: Alphanumerisch, kann Bindestriche und Leerzeichen enthalten - Beispiel: "6AV2 181-8XP00-0AX0", "AX5206", "SIE.6ES7500" + - SQL: WHERE a."Artikelnummer" = '6AV2 181-8XP00-0AX0' -WICHTIG - RICHTIGE SPALTE VERWENDEN: -- Wenn der Nutzer eine rein numerische Zahl angibt (z.B. "131741", "141215") → Suche in a."Artikelkürzel" -- Wenn der Nutzer eine alphanumerische Bezeichnung angibt mit Buchstaben, Bindestrichen oder Leerzeichen (z.B. "6AV2 181-8XP00-0AX0", "AX5206") → Suche in a."Artikelnummer" +3. **Artikelbeschrieb/Artikelbezeichnung**: Textformat mit Wörtern (z.B. "1517H Bundle", "LED Lampe 12V") + - Enthält Wörter wie "Bundle", "Lampe", etc. + - Kann Zahlen, Buchstaben, Leerzeichen und Wörter enthalten + - Format: Text mit beschreibenden Wörtern + - Beispiel: "1517H Bundle", "LED Lampe 12V", "Kabel 5m" + - SQL: WHERE a."Artikelbeschrieb" LIKE '%1517H Bundle%' OR a."Artikelbezeichnung" LIKE '%1517H Bundle%' + +⚠️⚠️⚠️ KRITISCH - BREITE SUCHE BEI LAGERBESTANDSABFRAGEN ⚠️⚠️⚠️ +Bei Fragen nach Lagerbeständen ("wie viel haben wir auf lager", "wie viele auf lager", etc.) MUSS IMMER eine BREITE SUCHE über ALLE Identifikationsfelder durchgeführt werden: + +**OBLIGATORISCH**: Verwende IMMER OR-Bedingungen mit LIKE-Patterns über mehrere Felder: +```sql +WHERE a."Artikelkürzel" LIKE '%[Suchbegriff]%' + OR a."Artikelnummer" LIKE '%[Suchbegriff]%' + OR a."Artikelbezeichnung" LIKE '%[Suchbegriff]%' +``` + +**WARUM**: Der Suchbegriff könnte in verschiedenen Feldern vorkommen: +- "1517H" könnte in Artikelkürzel, Artikelnummer ODER Artikelbezeichnung stehen +- "1517H Bundle" könnte in Artikelbezeichnung stehen, aber auch "1517H" allein in Artikelkürzel oder Artikelnummer +- Nutzer geben oft nur Teilinformationen an + +**BEISPIEL FÜR KORREKTE ABFRAGE**: +User: "wie viel vom 1517H bundle haben wir auf lager?" +```sql +SELECT a."Artikelkürzel", a."Artikelnummer", a."Artikelbezeichnung", a."Lieferant", + e."EP_CHF", lp."Lagerplatz" as "Lagerplatzname", l."S_IST_BESTAND", l."S_SOLL_BESTAND", l."S_RESERVIERTER__BESTAND", + CASE WHEN l."S_IST_BESTAND" != 'Unbekannt' THEN CAST(l."S_IST_BESTAND" AS INTEGER) - COALESCE(l."S_RESERVIERTER__BESTAND", 0) ELSE NULL END as "Verfügbarer Bestand" +FROM Artikel a +LEFT JOIN Einkaufspreis e ON a."I_ID" = e."m_Artikel" +LEFT JOIN Lagerplatz_Artikel l ON a."I_ID" = l."R_ARTIKEL" +LEFT JOIN Lagerplatz lp ON l."R_LAGERPLATZ" = lp."I_ID" +WHERE a."Artikelkürzel" LIKE '%1517H%' + OR a."Artikelnummer" LIKE '%1517H%' + OR a."Artikelbezeichnung" LIKE '%1517H%' +LIMIT 20 +``` + +**WICHTIG**: +- Verwende IMMER LIKE mit Wildcards (%Suchbegriff%) für breite Suche +- Suche IMMER in allen drei Feldern (Artikelkürzel, Artikelnummer, Artikelbezeichnung) mit OR +- Bei eindeutigen numerischen Werten (z.B. "167677") kannst du auch = verwenden, aber OR mit LIKE ist sicherer +- Bei alphanumerischen Werten mit beschreibenden Wörtern (z.B. "1517H Bundle") IMMER LIKE mit OR über mehrere Felder + +WICHTIG - RICHTIGE SPALTE VERWENDEN (für einfache, eindeutige Fälle): +- Wenn der Nutzer eine rein numerische Zahl angibt (z.B. "167677", "131741") → Suche primär in a."Artikelkürzel", aber bei Lagerbestandsabfragen auch in anderen Feldern +- Wenn der Nutzer eine alphanumerische Bezeichnung angibt mit Buchstaben, Bindestrichen oder Leerzeichen, aber KEINE beschreibenden Wörter (z.B. "6AV2 181-8XP00-0AX0", "AX5206") → Suche primär in a."Artikelnummer", aber bei Lagerbestandsabfragen auch in anderen Feldern +- Wenn der Nutzer eine Bezeichnung mit beschreibenden Wörtern angibt (z.B. "1517H Bundle", "LED Lampe", "Kabel 5m") → Suche IMMER mit OR über mehrere Felder: a."Artikelbeschrieb" LIKE '%Suchbegriff%' OR a."Artikelbezeichnung" LIKE '%Suchbegriff%' OR a."Artikelnummer" LIKE '%Suchbegriff%' OR a."Artikelkürzel" LIKE '%Suchbegriff%' Beispiele: -- "Wie viele von 141215 haben wir auf Lager?" → Artikelkürzel "141215" → WHERE a."Artikelkürzel" = '141215' -- "Wie viel von 6AV2 181-8XP00-0AX0 haben wir auf Lager?" → Artikelnummer "6AV2 181-8XP00-0AX0" → WHERE a."Artikelnummer" = '6AV2 181-8XP00-0AX0' -- "Zeig mir Informationen zu AX5206" → Artikelnummer "AX5206" → WHERE a."Artikelnummer" = 'AX5206' +- "Wie viele von 167677 haben wir auf Lager?" → Breite Suche: WHERE a."Artikelkürzel" LIKE '%167677%' OR a."Artikelnummer" LIKE '%167677%' OR a."Artikelbezeichnung" LIKE '%167677%' +- "Wie viel vom 1517H Bundle haben wir auf Lager?" → Breite Suche: WHERE a."Artikelkürzel" LIKE '%1517H%' OR a."Artikelnummer" LIKE '%1517H%' OR a."Artikelbezeichnung" LIKE '%1517H%' +- "Wie viel von 6AV2 181-8XP00-0AX0 haben wir auf Lager?" → Breite Suche: WHERE a."Artikelkürzel" LIKE '%6AV2 181-8XP00-0AX0%' OR a."Artikelnummer" LIKE '%6AV2 181-8XP00-0AX0%' OR a."Artikelbezeichnung" LIKE '%6AV2 181-8XP00-0AX0%' +- "Zeig mir Informationen zu AX5206" → Breite Suche: WHERE a."Artikelkürzel" LIKE '%AX5206%' OR a."Artikelnummer" LIKE '%AX5206%' OR a."Artikelbezeichnung" LIKE '%AX5206%' Bei Fragen nach Lagerbestand: Kombiniere mit der Lagerplatz_Artikel Tabelle über JOIN und beachte die Anforderungen aus dem Abschnitt "LAGERBESTANDSABFRAGEN" @@ -385,14 +433,18 @@ WICHTIG für SQL-Abfragen: - Bei Lagerplatzabfragen: IMMER JOIN mit Lagerplatz-Tabelle für den Namen - Abfragen müssen direkt ausführbar sein (keine Platzhalter) - Erstelle SEPARATE Abfragen für verschiedene Tabellen/Datenquellen, nicht eine große JOIN-Abfrage +- ⚠️ KRITISCH: Bei Lagerbestandsabfragen IMMER breite Suche mit OR über Artikelkürzel, Artikelnummer UND Artikelbezeichnung verwenden STRATEGIE FÜR MEHRERE ABFRAGEN: - Analysiere welche Informationen benötigt werden - Identifiziere welche Tabellen diese Informationen enthalten - Erstelle für jede Tabelle/Datenquelle eine separate, fokussierte Abfrage -- Beispiel für "wie viel von 6AV2 181-8XP00-0AX0 haben wir auf lager": - * Abfrage 1: Artikel-Informationen (Artikelbezeichnung, Lieferant, etc.) aus Artikel-Tabelle - * Abfrage 2: Lagerbestände und Lagerplätze aus Lagerplatz_Artikel + Lagerplatz-Tabellen +- ⚠️ WICHTIG: Du kannst komplexe JOINs vermeiden, indem du mehrere einfache Abfragen parallel ausführst +- Beispiel für "wie viel vom 1517H bundle haben wir auf lager": + * Abfrage 1: Artikel-Informationen mit breiter Suche (Artikelkürzel, Artikelnummer, Artikelbezeichnung) aus Artikel-Tabelle + * Abfrage 2: Lagerbestände und Lagerplätze aus Lagerplatz_Artikel + Lagerplatz-Tabellen (mit JOIN für Lagerplatznamen) + * Diese Abfragen können parallel ausgeführt werden und die Ergebnisse werden dann kombiniert +- ⚠️ BEI LAGERBESTANDSABFRAGEN: Eine Abfrage mit JOINs ist OK, aber stelle sicher, dass die WHERE-Bedingung eine breite Suche über alle Identifikationsfelder enthält Return ONLY valid JSON: {{ @@ -729,3 +781,55 @@ Rules: else: return "Chatbot Conversation" + +def get_final_answer_prompt_with_results( + user_prompt: str, + context: str, + db_results_part: str, + web_results_part: str +) -> str: + """ + Get the complete prompt for generating the final answer with database and web results. + + Args: + user_prompt: User's original prompt + context: Conversation context + db_results_part: Formatted database results section + web_results_part: Formatted web research results section + + Returns: + Complete formatted prompt string + """ + system_prompt = get_final_answer_system_prompt() + + return f"""{system_prompt} + +Antworte auf die folgende Frage des Nutzers: {user_prompt}{context} + +{db_results_part}{web_results_part} + +KRITISCH: Verwende NUR die oben angegebenen Daten. Erfinde KEINE Werte. Wenn Daten fehlen, schreibe "Nicht verfügbar". + +WICHTIG - MEHRERE ABFRAGEN: +Die oben angegebenen DATENBANK-ERGEBNISSE können aus mehreren separaten Abfragen stammen. Jede Abfrage ist mit "=== Abfrage X ===" markiert und enthält Informationen zu einem spezifischen Aspekt (z.B. Artikel-Informationen, Lagerbestände, etc.). +- Kombiniere die Informationen aus ALLEN erfolgreichen Abfragen zu einer umfassenden Antwort +- Beispiel: Wenn Abfrage 1 Artikel-Informationen liefert und Abfrage 2 Lagerbestände liefert, kombiniere beide in deiner Antwort +- Verwende ALLE verfügbaren Informationen aus den verschiedenen Abfragen + +⚠️⚠️⚠️ ABSOLUT VERBOTEN - KEINE DATEN ERFINDEN ⚠️⚠️⚠️ +Wenn KEINE Datenbank-Ergebnisse vorhanden sind, dann: +- ❌ ERFINDE KEINE Artikelnummern, Artikelbezeichnungen, Preise oder Lagerbestände! +- ❌ ERFINDE KEINE Beispielartikel! +- ✓ Schreibe stattdessen: "Es wurden keine Artikel in der Datenbank gefunden." oder "Die Datenbankabfrage ist fehlgeschlagen." + +WICHTIG: Deine Antwort soll NUR die finale Antwort enthalten - KEINE Planungsschritte, KEINE SQL-Queries, KEINE Zwischenschritte! +Beginne DIREKT mit "Aus der Datenbank habe ich..." (wenn Daten vorhanden) oder "Es wurden keine Artikel gefunden" (wenn keine Daten vorhanden). + +WICHTIG: +- Klare, strukturierte Antwort +- Zahlen mit Tausender-Trennzeichen (7'411) +- Markdown-Tabellen für Daten (max 20 Zeilen) +- Artikelnummern als Link: [ARTIKELNUMMER](/details/ARTIKELNUMMER) +- Wenn Excel-Datei erstellt wurde, bestätige dies explizit am Ende +- Wenn Excel-Verarbeitung fehlgeschlagen ist, erkläre dem Nutzer den Fehler klar""" + diff --git a/modules/features/chatbot/mainChatbot.py b/modules/features/chatbot/mainChatbot.py index 079d970b..bc469a95 100644 --- a/modules/features/chatbot/mainChatbot.py +++ b/modules/features/chatbot/mainChatbot.py @@ -13,9 +13,10 @@ import asyncio import re from typing import Optional, Dict, Any, List -from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum, ChatLog +from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum, ChatLog, ChatDocument from modules.datamodels.datamodelUam import User from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, ProcessingModeEnum +from modules.datamodels.datamodelDocref import DocumentReferenceList, DocumentItemReference from modules.shared.timeUtils import getUtcTimestamp, parseTimestamp from modules.services import getInterface as getServices from modules.features.chatbot.eventManager import get_event_manager @@ -24,8 +25,10 @@ from modules.connectors.connectorPreprocessor import PreprocessorConnector from modules.features.chatbot.chatbotConstants import ( get_initial_analysis_prompt, generate_conversation_name, - get_final_answer_system_prompt + get_final_answer_system_prompt, + get_final_answer_prompt_with_results ) +import base64 logger = logging.getLogger(__name__) @@ -293,13 +296,12 @@ async def _emit_log_and_event( round_number: Optional[int] = None ) -> None: """ - Store log in database. The route's periodic chat data fetch will handle emitting it. - This avoids duplicate log emissions. + Store log in database and emit event for streaming. Args: interfaceDbChat: Database interface workflowId: Workflow ID - event_manager: Event manager (unused, kept for compatibility) + event_manager: Event manager for streaming message: Log message log_type: Log type (info, warning, error) status: Status string @@ -322,10 +324,37 @@ async def _emit_log_and_event( "status": status, "roundNumber": round_number } - # Only store in database - route's periodic fetch will emit it - interfaceDbChat.createLog(log_data) + # Store log in database + created_log = interfaceDbChat.createLog(log_data) + + # Emit event directly for streaming (using correct signature) + if created_log and event_manager: + try: + from modules.datamodels.datamodelChat import ChatLog + # Convert to dict if it's a Pydantic model + if hasattr(created_log, "model_dump"): + log_dict = created_log.model_dump() + elif hasattr(created_log, "dict"): + log_dict = created_log.dict() + else: + log_dict = log_data + + await event_manager.emit_event( + context_id=workflowId, + event_type="chatdata", + data={ + "type": "log", + "createdAt": log_timestamp, + "item": log_dict + }, + event_category="chat", + message="New log", + step="log" + ) + except Exception as emit_error: + logger.warning(f"Error emitting log event: {emit_error}") except Exception as e: - logger.error(f"Error storing log: {e}") + logger.error(f"Error storing log: {e}", exc_info=True) async def _check_workflow_stopped(interfaceDbChat, workflowId: str) -> bool: @@ -596,7 +625,6 @@ async def _convert_file_ids_to_document_references( # Search database if not found in messages if not document_id: try: - from modules.datamodels.datamodelChat import ChatDocument from modules.shared.databaseUtils import getRecordsetWithRBAC documents = getRecordsetWithRBAC( services.interfaceDbChat.db, @@ -1060,44 +1088,30 @@ async def _processChatbotMessage( error_query_keys = [k for k in queryResults.keys() if k.endswith("_error")] has_only_errors = bool(error_query_keys and not successful_query_keys) + # Add warning messages if needed if not has_query_results and needsDatabaseQuery: - db_results_part = "\n\nWICHTIG: Es wurden KEINE Datenbank-Ergebnisse gefunden. Die Datenbankabfrage wurde nicht ausgeführt oder hat keine Ergebnisse zurückgegeben." + 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." if has_only_errors: - 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'." + 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'." - answer_prompt = f"""{system_prompt} - -Antworte auf die folgende Frage des Nutzers: {userInput.prompt}{context} - -{db_results_part}{web_results_part} - -KRITISCH: Verwende NUR die oben angegebenen Daten. Erfinde KEINE Werte. Wenn Daten fehlen, schreibe "Nicht verfügbar". - -WICHTIG - MEHRERE ABFRAGEN: -Die oben angegebenen DATENBANK-ERGEBNISSE können aus mehreren separaten Abfragen stammen. Jede Abfrage ist mit "=== Abfrage X ===" markiert und enthält Informationen zu einem spezifischen Aspekt (z.B. Artikel-Informationen, Lagerbestände, etc.). -- Kombiniere die Informationen aus ALLEN erfolgreichen Abfragen zu einer umfassenden Antwort -- Beispiel: Wenn Abfrage 1 Artikel-Informationen liefert und Abfrage 2 Lagerbestände liefert, kombiniere beide in deiner Antwort -- Verwende ALLE verfügbaren Informationen aus den verschiedenen Abfragen - -⚠️⚠️⚠️ ABSOLUT VERBOTEN - KEINE DATEN ERFINDEN ⚠️⚠️⚠️ -Wenn KEINE Datenbank-Ergebnisse vorhanden sind, dann: -- ❌ ERFINDE KEINE Artikelnummern, Artikelbezeichnungen, Preise oder Lagerbestände! -- ❌ ERFINDE KEINE Beispielartikel! -- ✓ Schreibe stattdessen: "Es wurden keine Artikel in der Datenbank gefunden." oder "Die Datenbankabfrage ist fehlgeschlagen." - -WICHTIG: Deine Antwort soll NUR die finale Antwort enthalten - KEINE Planungsschritte, KEINE SQL-Queries, KEINE Zwischenschritte! -Beginne DIREKT mit "Aus der Datenbank habe ich..." (wenn Daten vorhanden) oder "Es wurden keine Artikel gefunden" (wenn keine Daten vorhanden).""" - -WICHTIG: -- Klare, strukturierte Antwort -- Zahlen mit Tausender-Trennzeichen (7'411) -- Markdown-Tabellen für Daten (max 20 Zeilen) -- Artikelnummern als Link: [ARTIKELNUMMER](/details/ARTIKELNUMMER) -- Wenn Excel-Datei erstellt wurde, bestätige dies explizit am Ende -- Wenn Excel-Verarbeitung fehlgeschlagen ist, erkläre dem Nutzer den Fehler klar""" + # Use the function from constants file to build the prompt + answer_prompt = get_final_answer_prompt_with_results( + userInput.prompt, + context, + db_results_part, + web_results_part + ) answerRequest = AiCallRequest( prompt=answer_prompt, @@ -1128,6 +1142,7 @@ WICHTIG: return # Create assistant message with final answer + message_id = f"msg_{uuid.uuid4()}" assistantMessageData = { "id": message_id, "workflowId": workflowId,