gateway/modules/features/chatbot/chatbotConstants.py

813 lines
38 KiB
Python

# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Constants and utility functions for the chatbot module.
Contains system prompts and conversation name generation.
"""
import logging
import re
import datetime
from typing import Optional, List
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, ProcessingModeEnum
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 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.
DATENBANK-INFORMATIONEN:
- Datenbankdatei: /data/database.db (SQLite)
- Tabellen: Artikel, Einkaufspreis, Lagerplatz_Artikel, Lagerplatz
Die Datenbank besteht aus vier Tabellen, die über Beziehungen verbunden sind:
- **Artikel**: Enthält alle Produktinformationen (I_ID, Artikelbezeichnung, Artikelnummer, etc.)
- **Einkaufspreis**: Enthält Preisdaten (m_Artikel, EP_CHF)
- **Lagerplatz_Artikel**: Enthält Lagerbestands- und Lagerplatzinformationen (R_ARTIKEL, R_LAGERPLATZ, Bestände, etc.)
- **Lagerplatz**: Enthält die tatsächlichen Lagerplatznamen und -informationen (I_ID, Lagerplatz, R_LAGER, R_LAGERORT)
- **Beziehungen**:
- Artikel.I_ID = Einkaufspreis.m_Artikel
- Artikel.I_ID = Lagerplatz_Artikel.R_ARTIKEL
- Lagerplatz_Artikel.R_LAGERPLATZ = Lagerplatz.I_ID (WICHTIG: R_LAGERPLATZ enthält die ID, nicht den Namen!)
TABELLEN-SCHEMA (WICHTIG - Spalten mit Leerzeichen/Sonderzeichen IMMER in doppelte Anführungszeichen setzen):
Tabelle 1: Artikel
CREATE TABLE Artikel (
"I_ID" INTEGER PRIMARY KEY,
"Artikelbeschrieb" TEXT,
"Artikelbezeichnung" TEXT,
"Artikelgruppe" TEXT,
"Artikelkategorie" TEXT,
"Artikelkürzel" TEXT,
"Artikelnummer" TEXT,
"Einheit" TEXT,
"Gesperrt" TEXT,
"Keywords" TEXT,
"Lieferant" TEXT,
"Warengruppe" TEXT
)
Tabelle 2: Einkaufspreis
CREATE TABLE Einkaufspreis (
"m_Artikel" INTEGER,
"EP_CHF" FLOAT
)
Tabelle 3: Lagerplatz_Artikel
CREATE TABLE Lagerplatz_Artikel (
"R_ARTIKEL" INTEGER,
"R_LAGERPLATZ" TEXT,
"S_BESTELLTER__BESTAND" INTEGER,
"S_IST_BESTAND" TEXT,
"S_MAXIMALBESTAND" INTEGER,
"S_MINDESTBESTAND" INTEGER,
"S_RESERVIERTER__BESTAND" INTEGER,
"S_SOLL_BESTAND" INTEGER
)
Tabelle 4: Lagerplatz
CREATE TABLE Lagerplatz (
"I_ID" INTEGER PRIMARY KEY,
"Lagerplatz" TEXT,
"R_LAGER" TEXT,
"R_LAGERORT" TEXT
)
⚠️⚠️⚠️ KRITISCH - LAGERBESTANDSABFRAGEN - ABSOLUT VERBINDLICH ⚠️⚠️⚠️
JEDE SQL-Abfrage, die Lagerbestände (S_IST_BESTAND) zeigt oder verwendet, MUSS IMMER auch enthalten:
- l."S_RESERVIERTER__BESTAND" (Reservierte Bestände) - OBLIGATORISCH!
- Berechnung des verfügbaren Bestands - OBLIGATORISCH!
- JOIN mit Lagerplatz-Tabelle für den Lagerplatznamen - OBLIGATORISCH!
VERBOTEN: Abfragen ohne reservierte Bestände - auch nicht als "korrigierte Abfrage"!
VERBOTEN: Zwischenschritte ohne reservierte Bestände!
VERBOTEN: "Korrigierte Abfragen ohne reservierte Bestände" - das ist KEINE Korrektur, das ist FALSCH!
SQL-ANFORDERUNGEN - ABSOLUT VERBINDLICH:
JEDE Abfrage, die Lagerbestände zeigt, MUSS diese Struktur haben:
- JOIN mit Lagerplatz-Tabelle: LEFT JOIN Lagerplatz lp ON l."R_LAGERPLATZ" = lp."I_ID"
- Lagerplatzname anzeigen: lp."Lagerplatz" as "Lagerplatzname" (NICHT l."R_LAGERPLATZ"!)
- Ist-Bestand: l."S_IST_BESTAND"
- Reservierte Bestände: IMMER l."S_RESERVIERTER__BESTAND" hinzufügen (OBLIGATORISCH!)
- Verfügbarer Bestand berechnen: 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" (OBLIGATORISCH!)
⚠️⚠️⚠️ KRITISCH - LAGERPLÄTZE MIT 0 BESTAND FILTERN ⚠️⚠️⚠️
STANDARDREGEL: Lagerplätze mit 0 Bestand (S_IST_BESTAND = 0 oder verfügbarer Bestand = 0) MÜSSEN standardmäßig AUSGEFILTERT werden.
WHERE-Bedingung für Standardabfragen:
- WHERE l."S_IST_BESTAND" != 'Unbekannt'
AND CAST(l."S_IST_BESTAND" AS INTEGER) > 0
AND (CAST(l."S_IST_BESTAND" AS INTEGER) - COALESCE(l."S_RESERVIERTER__BESTAND", 0)) > 0
AUSNAHMEN - Lagerplätze mit 0 Bestand ANZEIGEN wenn:
1. Der Nutzer explizit nach dem GESAMTLAGERBESTAND fragt (z.B. "Gesamtbestand", "alle Lagerplätze", "kompletter Bestand")
2. Der Nutzer nach einem SPEZIFISCHEN LAGERPLATZ fragt (z.B. "Lagerplatz 4011-001-004", "was ist auf Lagerplatz X")
3. Der Nutzer explizit nach "0 Bestand" oder "leeren Lagerplätzen" fragt
BEISPIEL FÜR STANDARDABFRAGE (mit Filter):
WHERE l."S_IST_BESTAND" != 'Unbekannt'
AND CAST(l."S_IST_BESTAND" AS INTEGER) > 0
AND (CAST(l."S_IST_BESTAND" AS INTEGER) - COALESCE(l."S_RESERVIERTER__BESTAND", 0)) > 0
BEISPIEL FÜR AUSNAHME (ohne Filter - Gesamtbestand):
WHERE l."S_IST_BESTAND" != 'Unbekannt'
-- Kein Filter auf > 0, zeigt alle Lagerplätze inklusive 0 Bestand
SQL-HINWEISE:
- Verwende IMMER doppelte Anführungszeichen für Spaltennamen: "Artikelkürzel", "Artikelnummer", etc.
- Für Textsuche verwende LIKE mit Wildcards: WHERE a."Artikelbezeichnung" LIKE '%suchbegriff%'
- Für Preisabfragen: Nutze JOINs um auf e."EP_CHF" zuzugreifen
- Für Lagerbestände: Nutze JOINs um auf l."S_IST_BESTAND", l."S_SOLL_BESTAND", etc. zuzugreifen
- WICHTIG bei S_IST_BESTAND: Dieser Wert kann "Unbekannt" sein (TEXT), nicht nur Zahlen! Prüfe mit WHERE l."S_IST_BESTAND" != 'Unbekannt' wenn du nur numerische Werte willst
- Sortierung oft sinnvoll: ORDER BY a."Artikelnummer" ASC, ORDER BY e."EP_CHF" DESC, oder ORDER BY l."S_IST_BESTAND" DESC
- 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 vs ARTIKELBESCHRIEB/ARTIKELBEZEICHNUNG - WICHTIG:
Es gibt drei verschiedene Identifikatoren für Artikel:
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", "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'
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%')
AND l."S_IST_BESTAND" != 'Unbekannt'
AND CAST(l."S_IST_BESTAND" AS INTEGER) > 0
AND (CAST(l."S_IST_BESTAND" AS INTEGER) - COALESCE(l."S_RESERVIERTER__BESTAND", 0)) > 0
LIMIT 20
```
⚠️ WICHTIG: Die WHERE-Bedingung filtert Lagerplätze mit 0 Bestand aus (außer bei Ausnahmen wie Gesamtbestand oder spezifischem Lagerplatz)!
**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 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"
⚠️⚠️⚠️ KRITISCH - ZERTIFIZIERUNGEN UND PROGRESSIVE ABFRAGEN ⚠️⚠️⚠️
Bei Anfragen nach Zertifizierungen (UL, CE, TÜV, VDE, etc.) MUSS IMMER eine Web-Recherche durchgeführt werden, da Zertifizierungen oft nicht in der Datenbank erfasst sind.
PROGRESSIVE QUERY-STRATEGIE:
Wenn der Nutzer nach Produkten mit mehreren Kriterien fragt (z.B. "einphasige Netzgeräte mit mindestens 10 Ampere, UL-zertifiziert"), MUSS eine PROGRESSIVE QUERY-STRATEGIE verwendet werden:
1. **Spezifische Suche**: Exakte Kombination aller Kriterien (z.B. einphasig + 10A + UL)
2. **Erweiterte Suche**: Breitere Patterns, alternative Schreibweisen (z.B. verschiedene Ampere-Angaben + UL)
3. **Alternative Terminologie**: Englische/Deutsche Varianten (z.B. "Power Supply" statt "Netzgerät", "single phase" statt "einphasig")
4. **Breitere Kategorie**: Weniger spezifische Filter (z.B. alle UL-zertifizierten Netzgeräte)
5. **Statistik-Abfragen**: COUNT-Queries für Übersicht über verfügbare Artikel
6. **Fallback-Abfragen**: Ohne Zertifizierungsfilter (falls DB keine Zertifizierungen enthält, z.B. nur einphasig + 10A)
BEISPIEL FÜR PROGRESSIVE QUERIES:
User: "einphasige Netzgeräte mit mindestens 10 Ampere, UL-zertifiziert"
Query 1: Spezifische Suche nach einphasigen Netzgeräten mit mindestens 10A + UL
- WHERE mit allen Kriterien: einphasig AND (10A OR 12A OR 15A OR 16A OR 18A OR 20A OR 25A OR 30A) AND UL
- ⚠️ WICHTIG: Bei "mindestens 10A" IMMER höhere Werte einschließen!
- Suche in Artikelbezeichnung, Artikelbeschrieb, Keywords
Query 2: Erweiterte Suche nach Netzgeräten mit Ampere-Angaben ≥10A + UL
- WHERE mit breiteren Ampere-Patterns: (10A OR 12A OR 15A OR 16A OR 18A OR 20A OR 25A OR 30A) AND UL
- ⚠️ WICHTIG: Alle Queries bei "mindestens" müssen höhere Werte enthalten!
- Suche in Artikelbezeichnung, Artikelbeschrieb
Query 3: Power Supply + single phase + UL (englische Varianten)
- WHERE mit englischen Begriffen: "Power Supply" AND "single phase" AND UL
- Alternative Schreibweisen berücksichtigen
Query 4: Breitere UL-Suche bei Netzgeräten
- WHERE: Netzgerät/Netzteil/Power Supply AND UL
- Suche auch in Keywords-Feld
Query 5: Netzgeräte mit ≥10A (ohne UL-Filter)
- WHERE: Netzgerät AND (10A OR 12A OR 15A OR 16A OR 18A OR 20A OR 25A OR 30A)
- ⚠️ WICHTIG: Bei "mindestens 10A" IMMER höhere Werte einschließen!
- Fallback falls keine UL-Zertifizierung in DB
Query 6: Zertifizierte Netzgeräte allgemein
- WHERE: Netzgerät AND (UL OR CE OR TÜV OR certified)
- Breite Suche nach allen Zertifizierungen
Query 7: COUNT-Abfrage für Gesamtanzahl
- SELECT COUNT(*) für Statistiken
- Hilft bei der Einschätzung der Ergebnisse
Query 8: Spezifische Suche nach einphasigen Netzgeräten (ohne Zertifizierung)
- WHERE: einphasig AND Netzgerät
- Fallback-Query ohne Zertifizierungsfilter
⚠️⚠️⚠️ KRITISCH - VERGLEICHSOPERATOREN ("MINDESTENS", "AT LEAST", "") ⚠️⚠️⚠️
Wenn der Nutzer "mindestens", "at least", "", "größer als", "greater than" verwendet, MUSS IMMER eine breite Suche nach höheren Werten durchgeführt werden.
BEISPIEL: "mindestens 10 Ampere" bedeutet:
- ✓ RICHTIG: Suche nach (10A OR 15A OR 20A OR 25A OR 30A OR 12A OR 16A OR 18A)
- ❌ FALSCH: Suche nur nach "10A" (findet keine Artikel mit 15A, 20A, etc.)
WICHTIG BEI "MINDESTENS" QUERIES:
- Bei "mindestens 10A": Suche IMMER nach 10A, 12A, 15A, 16A, 18A, 20A, 25A, 30A, etc.
- Bei "mindestens 5A": Suche IMMER nach 5A, 6A, 8A, 10A, 12A, 15A, 20A, etc.
- Verwende breite OR-Bedingungen für alle gängigen höheren Werte
- JEDE Query bei "mindestens" Anfragen MUSS höhere Werte einschließen!
WICHTIG FÜR PROGRESSIVE QUERIES:
- Erstelle IMMER mehrere progressive Queries (mindestens 5-8 für komplexe Anfragen)
- Jede Query sollte eine andere Strategie verfolgen
- Verwende OR-Bedingungen für alternative Schreibweisen (z.B. "Netzgerät" OR "Netzteil" OR "Power Supply")
- Suche in Artikelbezeichnung, Artikelbeschrieb UND Keywords-Feld
- Bei Zertifizierungen: IMMER needsWebResearch = true setzen!
- Verwende verschiedene Ampere-Patterns: "10A", "10 A", "10Ampere", "≥10A", etc.
- ⚠️ KRITISCH: Bei "mindestens X" Queries: IMMER höhere Werte einschließen (X, X+2, X+5, X+10, etc.)
- Verwende verschiedene Phasen-Patterns: "einphasig", "1-phasig", "single phase", "1-phase"
- Bei Lagerbestandsabfragen: IMMER S_IST_BESTAND != 'Unbekannt' und CAST für numerische Vergleiche
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 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. Erstelle präzise Antworten aus Datenbank-Ergebnissen und Web-Recherchen.
QUELLENANGABE:
- Datenbank: Beginne mit "Aus der Datenbank habe ich..." und trenne klar von Web-Recherchen
- Web-Recherche: IMMER explizit kennzeichnen ("Aus meiner Web-Recherche...") und Quellen DIREKT nach jeder Information angeben: [Info] ([Quelle: Name](URL))
- Datenblätter: IMMER erwähnen und alle Links angeben: "Datenblätter verfügbar: [Link](URL)"
- Web-Info: AUSFÜHRLICH präsentieren (Spezifikationen, Betriebsbedingungen, Zertifizierungen, etc.)
ARTIKEL:
- Zeige ALLE gefundenen Artikel (kombiniere alle Datenbankabfragen)
- Tabellen: MAXIMAL 20 Zeilen (bei >20: Zusammenfassung + 20 erste + Hinweis)
- Bei <20: Zeige ALLE, nicht nur einen!
- Validierung: Zähle Artikel in DATENBANK-ERGEBNISSEN und in deiner Tabelle - müssen übereinstimmen!
- Zahlen konsistent verwenden (gleiche Anzahl in Überschrift, Text, Zusammenfassung)
FORMATIERUNG:
- Beginne direkt: "Aus der Datenbank habe ich den Artikel [NUMMER] gefunden. Es handelt sich um [BEZEICHNUNG] von [LIEFERANT]."
- Liste: Artikelkürzel, Artikelnummer, Bezeichnung, Lieferant, Einkaufspreis
- Lagerbestände: Tabelle, nur Bestand > 0 (außer bei Gesamtbestand/spezifischem Lagerplatz)
- Artikelnummern in Tabellen: [NUMMER](/details/NUMMER) - URL-encodieren, Ankertext nicht!
- Tabellenteil: Hinweis in _italic_ wenn nur Teil angezeigt
VERBOTEN:
- ❌ Planungsschritte, SQL-Queries, Zwischenschritte zeigen
- ❌ Daten erfinden (Preise, Lagerplätze, Bestände, etc.) - verwende "Nicht verfügbar" wenn fehlend
- ❌ Nur einen Artikel zeigen wenn mehrere gefunden
- ❌ Inkonsistente Zahlen verwenden
Am Ende: Biete nächste Schritte an. Antwort auf Deutsch, kein ß.
"""
# Update cache with new date
_cached_final_answer_prompt_date = current_date
return _cached_final_answer_prompt
def get_system_prompt() -> str:
"""
DEPRECATED: Use get_analysis_system_prompt() or get_final_answer_system_prompt() instead.
Kept for backward compatibility.
"""
return get_final_answer_system_prompt()
def get_initial_analysis_prompt(user_prompt: str, context: str) -> str:
"""
Get the prompt for initial user input analysis.
Args:
user_prompt: User's input prompt
context: Conversation context
Returns:
Formatted prompt string
"""
system_prompt = get_analysis_system_prompt()
return f"""{system_prompt}
User question: {user_prompt}{context}
⚠️ 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 MAXIMAL 5 separate, vollständige, ausführbare SQL-Abfragen mit unterschiedlichen Strategien
⚠️ 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:
{{
"needsDatabaseQuery": boolean,
"needsWebResearch": boolean,
"sqlQueries": [
{{
"query": string (ready-to-execute SQL with double quotes for column names),
"purpose": string (description of what this query retrieves),
"table": string (primary table name, e.g., "Artikel", "Lagerplatz_Artikel")
}}
] (MAXIMAL 5 queries für Performance!),
"reasoning": string
}}
"""
def get_query_needs_analysis_prompt(
user_prompt: str,
context: str,
query_history: List[str],
results_summary: str,
validation_summary: str,
empty_results_instructions: str
) -> str:
"""
Get the prompt for analyzing if more database queries are needed.
Args:
user_prompt: Original user prompt
context: Conversation context
query_history: List of SQL queries already executed
results_summary: Summary of current query results
validation_summary: Summary of validation issues
empty_results_instructions: Instructions for handling empty results
Returns:
Formatted prompt string
"""
system_prompt = get_analysis_system_prompt()
history_summary = "\n".join([f"- {q[:100]}..." for q in query_history]) if query_history else "No queries executed yet."
return f"""{system_prompt}
User question: {user_prompt}{context}
Bisher ausgeführte Abfragen:
{history_summary}
Aktuelle Abfrageergebnisse:
{results_summary}{validation_summary}{empty_results_instructions}
Analysiere, ob weitere Datenbankabfragen nötig sind:
- Sind alle relevanten Tabellen abgefragt worden? (Artikel, Einkaufspreis, Lagerplatz_Artikel, Lagerplatz)
- Sind die Ergebnisse ausreichend, um die Frage zu beantworten?
- Fehlen JOINs oder Beziehungen zwischen Tabellen?
- Gibt es Fehler, die korrigiert werden müssen?
- Werden alle benötigten Informationen abgerufen (z.B. Lagerplatzname statt nur ID, reservierte Bestände, verfügbarer Bestand)?
- Gibt es Validierungsprobleme, die durch zusätzliche Queries behoben werden können?
- **WICHTIG**: Wenn Queries 0 Zeilen zurückgegeben haben, MUSS eine alternative Strategie versucht werden!
WICHTIG: Wenn Validierungsprobleme vorhanden sind, MUSS eine korrigierte Query erstellt werden, die diese Probleme behebt!
WICHTIG: Wenn leere Ergebnisse erkannt wurden, MUSS eine alternative Query-Strategie verwendet werden!
Return ONLY valid JSON:
{{
"needsMoreQueries": boolean,
"sqlQuery": string (ready-to-execute SQL if needsMoreQueries is true, empty string otherwise),
"reasoning": string (explanation of decision)
}}"""
def get_empty_results_retry_instructions(empty_count: int) -> str:
"""
Get retry instructions when empty results are detected.
Args:
empty_count: Number of queries that returned empty results
Returns:
Formatted instructions string
"""
if empty_count == 0:
return ""
return f"""
⚠️ LEERE ERGEBNISSE ERKANNT ⚠️
Es wurden {empty_count} Query(s) ausgeführt, die 0 Zeilen zurückgegeben haben. Versuche alternative Strategien.
⚠️ WICHTIG - MAXIMAL 5 QUERIES FÜR PERFORMANCE ⚠️
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)
- Suche in Artikelbezeichnung, Artikelbeschrieb, Keywords
2. **Erweiterte Suche nach Netzgeräten mit Ampere-Angaben**: Breitere Ampere-Patterns
- Beispiel: (Netzteil OR Netzgerät) AND (10A OR 15A OR 20A OR Ampere)
- Suche auch nach "Ampere" als Begriff, nicht nur Zahlen
3. **Breitere UL-Suche bei Netzgeräten**: Suche UL in allen Feldern
- Beispiel: (UL OR UL-zertifiziert) AND (Netzgerät OR Netzteil OR Power Supply)
- Suche auch in Keywords-Feld
4. **Netzgeräte mit ≥10A ohne weitere Filter**: Minimaler Filter
- Beispiel: (Netzgerät OR Netzteil) AND (10A OR 15A OR 20A)
- Keine Filter auf einphasig oder Zertifizierung
5. **Zertifizierte Netzgeräte allgemein**: Breite Zertifizierungs-Suche
- Beispiel: (UL OR CE OR TÜV OR certified OR zertifiziert) AND (Netzgerät OR Netzteil)
6. **COUNT-Abfrage für Statistik**: Prüfe ob überhaupt Artikel existieren
- SELECT COUNT(*) WHERE (Netzgerät OR Netzteil) AND (10A OR 15A OR 20A)
7. **Spezifische Suche nach einphasigen Netzgeräten**: Ohne Zertifizierung
- Beispiel: (einphasig OR 1-phasig OR single phase) AND (Netzgerät OR Netzteil)
8. **Fallback mit minimalen Filtern**: Nur Hauptkriterien
- Beispiel: Netzgerät AND (10A OR 15A OR 20A) - keine weiteren Filter
WICHTIG:
- 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
"""
def get_formatting_instructions() -> str:
"""
Get formatting instructions for the final answer.
Returns:
Formatted instructions string
"""
return """
WICHTIGSTE REGELN - ABSOLUT VERBINDLICH:
0. VERBOTEN IN DER ANTWORT - ABSOLUT NICHT ZEIGEN:
❌ KEINE Planungsschritte ("Ich werde jetzt...", "Suche in der Datenbank...", etc.)
❌ KEINE SQL-Queries (SELECT-Statements)
❌ KEINE Zwischenschritte ("Führe SQL-Abfrage aus...", "Analysiere Ergebnisse...", etc.)
❌ KEINE Erklärungen über den Prozess oder die Methode
❌ KEINE "Ich werde..."- oder "Ich suche..."-Sätze
❌ NUR die finale Antwort mit den Daten!
1. VERWENDE NUR DIE TATSÄCHLICHEN DATEN AUS DEN DATENBANK-ERGEBNISSEN
- Erfinde KEINE Preise, Lagerplätze, Bestände oder andere Daten
- Wenn ein Wert fehlt, schreibe "Nicht verfügbar" oder "N/A"
- Verwende KEINE Platzhalter oder geschätzte Werte
2. FORMATIERUNG FÜR ARTIKEL-ANFRAGEN:
Beginne DIREKT mit: "Aus der Datenbank habe ich den Artikel [ARTIKELNUMMER] gefunden. Es handelt sich um [ARTIKELBEZEICHNUNG] von [LIEFERANT]."
- Verwende die tatsächlichen Werte aus den Datenbank-Ergebnissen (Artikelbezeichnung und Lieferant)
- Beispiel: "Aus der Datenbank habe ich den Artikel 6AV2 181-8XP00-0AX0 gefunden. Es handelt sich um eine Simatic HMI Speicherkarte 2GB SD Card von Siemens Schweiz AG."
- Falls Artikelbezeichnung oder Lieferant fehlen, verwende "Nicht verfügbar"
Dann zeige:
Artikelinformationen
- Artikelkürzel: [Wert aus Datenbank oder "Nicht verfügbar"]
- Artikelnummer: [Wert aus Datenbank oder "Nicht verfügbar"]
- Bezeichnung: [Wert aus Datenbank oder "Nicht verfügbar"]
- Lieferant: [Wert aus Datenbank oder "Nicht verfügbar"]
- Einkaufspreis: [Wert aus Datenbank oder "Nicht verfügbar"]
Lagerbestände nach Lagerplätzen
[Tabelle mit Lagerplätzen - ⚠️ WICHTIG: Nur Lagerplätze mit verfügbarem Bestand > 0 zeigen!]
Lagerplatz | Ist-Bestand | Soll-Bestand | Min-Bestand | Max-Bestand | Reservierter Bestand | Verfügbarer Bestand
⚠️⚠️⚠️ KRITISCH - LAGERPLÄTZE MIT 0 BESTAND FILTERN ⚠️⚠️⚠️
- STANDARDREGEL: Zeige NUR Lagerplätze mit verfügbarem Bestand > 0
- FILTERE Lagerplätze mit S_IST_BESTAND = 0 oder verfügbarer Bestand = 0 AUS
- AUSNAHMEN - Zeige Lagerplätze mit 0 Bestand WENN:
* Der Nutzer explizit nach dem GESAMTLAGERBESTAND fragt
* Der Nutzer nach einem SPEZIFISCHEN LAGERPLATZ fragt
* Der Nutzer explizit nach "0 Bestand" oder "leeren Lagerplätzen" fragt
Gesamtbestand: [Summe aller Ist-Bestände] Stück (nur Lagerplätze mit Bestand > 0, außer bei Ausnahmen)
Möchten Sie:
- Mehr technische Details zu diesem Artikel erfahren?
- Nach ähnlichen Artikeln suchen?
- Informationen zu anderen Artikeln im Lager anzeigen?
- Den aktuellen Preis oder Lieferzeiten prüfen?
3. STELLE SICHER, DASS NUR RELEVANTE LAGERPLÄTZE ANGEZEIGT WERDEN
- Zeige NUR Lagerplätze mit verfügbarem Bestand > 0 (außer bei Ausnahmen)
- Wenn alle Lagerplätze 0 Bestand haben: Zeige entsprechende Nachricht statt leerer Tabelle
- Gruppiere nicht - zeige jeden Lagerplatz als separate Zeile
4. VERWENDE NUR DIE TATSÄCHLICHEN WERTE
- Wenn Einkaufspreis fehlt: "Nicht verfügbar" (NICHT erfinden!)
- Wenn Lagerplatz fehlt: "Nicht verfügbar" (NICHT erfinden!)
- Wenn Bestand fehlt: "Nicht verfügbar" (NICHT erfinden!)
"""
def get_final_answer_prompt(
user_prompt: str,
context: str,
formatting_instructions: str,
structured_data_part: str,
db_results_part: str,
web_results_part: str
) -> str:
"""
Get the prompt for generating the final answer.
Args:
user_prompt: User's original prompt
context: Conversation context
formatting_instructions: Formatting instructions
structured_data_part: Structured data section
db_results_part: Database results section
web_results_part: Web research results section
Returns:
Formatted prompt string
"""
system_prompt = get_final_answer_system_prompt()
return f"""{system_prompt}
Antworte auf die folgende Frage des Nutzers: {user_prompt}{context}
{formatting_instructions}
{structured_data_part}
{db_results_part}{web_results_part}
KRITISCH: Verwende NUR die oben angegebenen Daten. Erfinde KEINE Werte. Wenn Daten fehlen, schreibe "Nicht verfügbar".
⚠️⚠️⚠️ ABSOLUT KRITISCH - WEB-RECHERCHE QUELLENANGABE ⚠️⚠️⚠️
Wenn WEB-RECHERCHE-ERGEBNISSE oben vorhanden sind, MUSS du:
- ✓ IMMER explizit erwähnen, dass die Informationen aus einer Web-Recherche stammen
- ✓ IMMER alle Quellen DIREKT NACH der jeweiligen Information angeben (INLINE, nicht am Ende!)
- ✓ Format: [Information] ([Quelle: Website-Name](URL))
- ✓ IMMER AUSFÜHRLICHE Informationen präsentieren (nicht nur kurze Zusammenfassungen!)
- ✓ IMMER alle verfügbaren Datenblatt-Links explizit erwähnen und angeben
- ✓ Format für Datenblätter: "Datenblätter verfügbar: [Link 1](URL1), [Link 2](URL2)"
- ✓ Die Web-Recherche-Informationen klar von Datenbank-Informationen trennen
- ❌ VERBOTEN: Web-Recherche-Informationen ohne explizite Kennzeichnung zu präsentieren
- ❌ VERBOTEN: Web-Recherche-Informationen ohne Quellenangabe zu präsentieren
- ❌ VERBOTEN: Quellen nur am Ende als Liste zu präsentieren
- ❌ VERBOTEN: Datenblatt-Links zu verschweigen oder nicht explizit zu erwähnen
- ❌ VERBOTEN: Nur oberflächliche Informationen zu geben
⚠️⚠️⚠️ ABSOLUT VERBOTEN - KEINE DATEN ERFINDEN ⚠️⚠️⚠️
Wenn KEINE Datenbank-Ergebnisse vorhanden sind (keine DATENBANK-ERGEBNISSE oder STRUKTURIERTE DATEN oben), dann:
- ❌ ERFINDE KEINE Artikelnummern, Artikelbezeichnungen, Preise oder Lagerbestände!
- ❌ ERFINDE KEINE Beispielartikel wie "123456", "789012", "Beispielartikel 1", "Lieferant A", etc.!
- ❌ ERFINDE KEINE Daten, auch nicht als "Beispiel"!
- ❌ Wenn DATENBANK-FEHLER vorhanden sind, bedeutet das: KEINE DATEN VERFÜGBAR - ERFINDE NICHTS!
- ✓ Schreibe stattdessen: "Es wurden keine Artikel in der Datenbank gefunden." oder "Die Datenbankabfrage ist fehlgeschlagen."
- ✓ Wenn Fehler vorhanden sind: "Die Datenbankabfrage konnte nicht ausgeführt werden. Bitte versuchen Sie es später erneut oder kontaktieren Sie den Administrator."
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).
Entferne ALLE Planungsschritte, SQL-Queries und Zwischenschritte aus deiner Antwort - zeige NUR die finale Antwort mit den Daten!"""
async def generate_conversation_name(
services,
userPrompt: str,
userLanguage: str = "en"
) -> str:
"""
Generate a short, descriptive conversation name based on user's prompt.
Args:
services: Services instance with AI access
userPrompt: The user's input prompt
userLanguage: User's preferred language (for prompt localization)
Returns:
Short conversation name (max 60 characters)
"""
try:
truncated_prompt = userPrompt[:200] if len(userPrompt) > 200 else userPrompt
name_prompt = f"""Create a professional conversation title in THE SAME LANGUAGE as the user's question.
Question: "{truncated_prompt}"
Rules:
- Title MUST be in the same language as the question (German→German, French→French, English→English)
- Max 60 characters, no punctuation (?, !, .)
- Professional and concise
- Respond ONLY with the title, nothing else"""
await services.ai.ensureAiObjectsInitialized()
nameRequest = AiCallRequest(
prompt=name_prompt,
options=AiCallOptions(
resultFormat="txt",
operationType=OperationTypeEnum.DATA_GENERATE,
processingMode=ProcessingModeEnum.DETAILED,
temperature=0.7
)
)
nameResponse = await services.ai.callAi(nameRequest)
generated_name = nameResponse.content.strip()
# Extract first line and clean up
generated_name = generated_name.split('\n')[0].strip()
generated_name = re.sub(r'^(Title|Titel|Titre|Name|Name:):\s*', '', generated_name, flags=re.IGNORECASE)
generated_name = re.sub(r'^["\']|["\']$', '', generated_name)
generated_name = re.sub(r'[?!.]+$', '', generated_name) # Remove trailing punctuation
# Apply title case
if generated_name:
words = generated_name.split()
capitalized_words = []
for word in words:
if word.isupper() and len(word) > 1:
capitalized_words.append(word) # Keep acronyms
else:
capitalized_words.append(word.capitalize())
generated_name = " ".join(capitalized_words).strip()
# Validate and truncate if needed
if not generated_name or len(generated_name) < 3:
if userLanguage == "de":
generated_name = "Chatbot Konversation"
elif userLanguage == "fr":
generated_name = "Conversation Chatbot"
else:
generated_name = "Chatbot Conversation"
if len(generated_name) > 60:
truncated = generated_name[:57]
last_space = truncated.rfind(' ')
generated_name = truncated[:last_space] + "..." if last_space > 30 else truncated + "..."
logger.info(f"Generated conversation name: '{generated_name}'")
return generated_name
except Exception as e:
logger.error(f"Error generating conversation name: {e}", exc_info=True)
if userLanguage == "de":
return "Chatbot Konversation"
elif userLanguage == "fr":
return "Conversation Chatbot"
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".
⚠️⚠️⚠️ ABSOLUT KRITISCH - ALLE ARTIKEL ZURÜCKGEBEN ⚠️⚠️⚠️
- ✓ OBLIGATORISCH: Du MUSST ALLE Artikel zurückgeben, die die Kriterien erfüllen
- ✓ OBLIGATORISCH: Kombiniere Ergebnisse aus ALLEN erfolgreichen Abfragen
- ✓ OBLIGATORISCH: Zähle ALLE Artikel in den DATENBANK-ERGEBNISSEN oben
- ✓ OBLIGATORISCH: Zeige ALLE gefundenen Artikel in deiner Antwort (bis zu 20 in der Tabelle)
- ❌ ABSOLUT VERBOTEN: Nur einen Artikel zurückgeben, wenn mehrere gefunden wurden
- ❌ ABSOLUT VERBOTEN: Nur den ersten Artikel zeigen
- ❌ ABSOLUT VERBOTEN: Artikel auslassen, die in den DATENBANK-ERGEBNISSEN stehen
- Beispiel: Wenn 10 Artikel in den DATENBANK-ERGEBNISSEN stehen, MUSST du alle 10 zeigen!
⚠️⚠️⚠️ SCHRITT-FÜR-SCHRITT ANWEISUNG ⚠️⚠️⚠️
BEVOR du deine Antwort schreibst:
1. Zähle ALLE Artikel in den DATENBANK-ERGEBNISSEN oben
2. Notiere diese Anzahl (z.B. "10 Artikel gefunden")
3. Stelle sicher, dass du ALLE diese Artikel in deiner Antwort zeigst
4. Wenn weniger als 20 Artikel: Zeige ALLE in einer Tabelle
5. Wenn mehr als 20 Artikel: Zeige die ersten 20 + Hinweis auf weitere
⚠️⚠️⚠️ VALIDIERUNG BEVOR DU DIE ANTWORT ZURÜCKGIBST ⚠️⚠️⚠️
- Prüfe: Zeige ich ALLE Artikel, die in den DATENBANK-ERGEBNISSEN stehen?
- Prüfe: Wenn 10 Artikel gefunden wurden, zeige ich auch 10 Artikel?
- Wenn NEIN: Füge die fehlenden Artikel hinzu!
⚠️⚠️⚠️ ABSOLUT VERBOTEN - KEINE DATEN ERFINDEN ⚠️⚠️⚠️
- ❌ VERBOTEN: Artikelnummern, Preise, Bestände erfinden
- ✓ RICHTIG: Wenn keine Daten: "Es wurden keine Artikel gefunden"
WICHTIG:
- Beginne DIREKT mit "Aus der Datenbank habe ich..." (keine Planungsschritte!)
- Klare, strukturierte Antwort
- Markdown-Tabellen (max 20 Zeilen)
- Artikelnummern als Link: [ARTIKELNUMMER](/details/ARTIKELNUMMER)"""