diff --git a/modules/datamodels/datamodelFeatures.py b/modules/datamodels/datamodelFeatures.py index 60153f28..0a5dc441 100644 --- a/modules/datamodels/datamodelFeatures.py +++ b/modules/datamodels/datamodelFeatures.py @@ -3,7 +3,7 @@ """Feature models: Feature, FeatureInstance.""" import uuid -from typing import Optional +from typing import Optional, Dict, Any from pydantic import BaseModel, Field from modules.shared.attributeUtils import registerModelLabels from modules.datamodels.datamodelUtils import TextMultilingual @@ -68,6 +68,11 @@ class FeatureInstance(BaseModel): description="Whether this feature instance is enabled", json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False} ) + config: Optional[Dict[str, Any]] = Field( + default=None, + description="Instance-specific configuration (JSONB). Structure depends on featureCode.", + json_schema_extra={"frontend_type": "json", "frontend_readonly": False, "frontend_required": False} + ) registerModelLabels( @@ -79,5 +84,6 @@ registerModelLabels( "mandateId": {"en": "Mandate", "de": "Mandant", "fr": "Mandat"}, "label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"}, "enabled": {"en": "Enabled", "de": "Aktiviert", "fr": "Activé"}, + "config": {"en": "Configuration", "de": "Konfiguration", "fr": "Configuration"}, }, ) diff --git a/modules/features/chatbot/aiCenterAdapter.py b/modules/features/chatbot/aiCenterAdapter.py new file mode 100644 index 00000000..2e638edd --- /dev/null +++ b/modules/features/chatbot/aiCenterAdapter.py @@ -0,0 +1,170 @@ +# Copyright (c) 2025 Patrick Motsch +# All rights reserved. +""" +Adapter to use AI Center as a LangChain-compatible chat model. +Maps LangChain message format to AI Center requests and responses. +""" + +import logging +from typing import Any, AsyncIterator, Iterator, List, Optional + +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_core.messages import ( + AIMessage, + BaseMessage, + HumanMessage, + SystemMessage, +) +from langchain_core.outputs import ChatGeneration, ChatResult +from langchain_core.callbacks import AsyncCallbackHandlerForLLMRun, CallbackManagerForLLMRun + +from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, ProcessingModeEnum + +logger = logging.getLogger(__name__) + + +class AICenterChatModel(BaseChatModel): + """ + Adapter to use AI center as LangChain chat model. + Converts LangChain messages to AI center format and back. + """ + + def __init__( + self, + services, + system_prompt: str = "", + temperature: float = 0.2, + **kwargs + ): + """ + Initialize AI Center chat model adapter. + + Args: + services: Services instance with AI access + system_prompt: System prompt to use + temperature: Temperature for AI calls + """ + super().__init__(**kwargs) + self.services = services + self.system_prompt = system_prompt + self.temperature = temperature + + @property + def _llm_type(self) -> str: + """Return identifier of LLM type.""" + return "ai_center" + + def _generate( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[CallbackManagerForLLMRun] = None, + **kwargs: Any, + ) -> ChatResult: + """ + Synchronous generation - not supported, use async version. + """ + raise NotImplementedError("Use async version: _agenerate") + + async def _agenerate( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[AsyncCallbackHandlerForLLMRun] = None, + **kwargs: Any, + ) -> ChatResult: + """ + Generate chat response using AI center. + + Args: + messages: List of LangChain messages + stop: Optional list of stop sequences + run_manager: Optional callback manager + **kwargs: Additional arguments + + Returns: + ChatResult with generated message + """ + # Convert LangChain messages to AI center prompt format + prompt_parts = [] + + # Add system prompt if present + if self.system_prompt: + prompt_parts.append(self.system_prompt) + + # Convert messages to text format + for msg in messages: + if isinstance(msg, SystemMessage): + # System messages are already in system_prompt or can be added here + if not self.system_prompt: + prompt_parts.append(f"System: {msg.content}") + elif isinstance(msg, HumanMessage): + prompt_parts.append(f"User: {msg.content}") + elif isinstance(msg, AIMessage): + prompt_parts.append(f"Assistant: {msg.content}") + else: + # Generic message + prompt_parts.append(str(msg.content)) + + # Combine into single prompt + full_prompt = "\n\n".join(prompt_parts) + + # Create AI center request + ai_request = AiCallRequest( + prompt=full_prompt, + options=AiCallOptions( + resultFormat="txt", + operationType=OperationTypeEnum.DATA_ANALYSE, + processingMode=ProcessingModeEnum.DETAILED, + temperature=self.temperature + ) + ) + + # Call AI center + try: + await self.services.ai.ensureAiObjectsInitialized() + ai_response = await self.services.ai.callAi(ai_request) + + # Extract content + content = ai_response.content if hasattr(ai_response, 'content') else str(ai_response) + + # Create AIMessage from response + ai_message = AIMessage(content=content) + + # Create ChatGeneration + generation = ChatGeneration(message=ai_message) + + # Return ChatResult + return ChatResult(generations=[generation]) + + except Exception as e: + logger.error(f"Error calling AI center: {e}", exc_info=True) + # Return error message + error_message = AIMessage(content=f"Error: {str(e)}") + generation = ChatGeneration(message=error_message) + return ChatResult(generations=[generation]) + + async def astream( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + run_manager: Optional[AsyncCallbackHandlerForLLMRun] = None, + **kwargs: Any, + ) -> AsyncIterator[BaseMessage]: + """ + Stream chat response (not fully supported by AI center, returns single chunk). + + Args: + messages: List of LangChain messages + stop: Optional list of stop sequences + run_manager: Optional callback manager + **kwargs: Additional arguments + + Yields: + BaseMessage chunks + """ + # For now, just return the full response as a single chunk + # TODO: Implement proper streaming if AI center supports it + result = await self._agenerate(messages, stop, run_manager, **kwargs) + if result.generations: + yield result.generations[0].message diff --git a/modules/features/chatbot/chatbotConfig.py b/modules/features/chatbot/chatbotConfig.py new file mode 100644 index 00000000..89345712 --- /dev/null +++ b/modules/features/chatbot/chatbotConfig.py @@ -0,0 +1,231 @@ +# Copyright (c) 2025 Patrick Motsch +# All rights reserved. +""" +Chatbot instance configuration management. +Handles loading and applying instance-specific configurations. +""" + +import logging +from typing import Optional, Dict, Any, List +from modules.interfaces.interfaceFeatures import getFeatureInterface +from modules.interfaces.interfaceDbApp import getRootInterface + +logger = logging.getLogger(__name__) + + +class ChatbotConfig: + """ + Chatbot instance configuration structure. + Provides defaults and validation for chatbot instance configs. + """ + + # Default configuration + DEFAULT_CONFIG = { + "connector": { + "types": ["preprocessor"], # Array of database connector types: "preprocessor", "custom" + "type": "preprocessor", # Legacy: single connector type (for backward compatibility) + "customConnectorClass": None # For custom connectors + }, + "prompts": { + "useCustomPrompts": False, + "customAnalysisPrompt": None, + "customFinalAnswerPrompt": None, + "customSystemPrompt": None # For LangGraph workflow (single system prompt) + }, + "behavior": { + "maxQueries": 5, + "enableWebResearch": True, + "enableRetryOnEmpty": True, + "maxRetryAttempts": 2 + }, + "database": { + "schema": None, # Custom schema info if needed + "tablePrefix": None # Custom table prefix if needed + } + } + + def __init__(self, config: Optional[Dict[str, Any]] = None): + """ + Initialize chatbot config with defaults and overrides. + + Args: + config: Instance-specific config dict (from FeatureInstance.config) + """ + self.config = self._merge_config(config or {}) + + def _merge_config(self, instance_config: Dict[str, Any]) -> Dict[str, Any]: + """ + Merge instance config with defaults, handling nested dicts. + + Args: + instance_config: Instance-specific config + + Returns: + Merged configuration dict + """ + merged = self.DEFAULT_CONFIG.copy() + + # Deep merge nested dicts + for key, value in instance_config.items(): + if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): + merged[key] = {**merged[key], **value} + else: + merged[key] = value + + return merged + + @property + def connector_types(self) -> List[str]: + """Get connector types as list (supports multiple connectors).""" + connector_config = self.config.get("connector", {}) + # Support new array format + types = [] + if "types" in connector_config and isinstance(connector_config["types"], list): + types = connector_config["types"] + # Fallback to legacy single type format + elif "type" in connector_config: + types = [connector_config["type"]] + else: + types = ["preprocessor"] + + # Filter out 'websearch' (not a database connector, handled separately via enableWebResearch) + types = [t for t in types if t != "websearch"] + + # Ensure at least one connector + if not types: + types = ["preprocessor"] + + return types + + @property + def connector_type(self) -> str: + """Get primary connector type (preprocessor, custom).""" + # For backward compatibility, return first connector type + types = self.connector_types + return types[0] if types else "preprocessor" + + @property + def custom_connector_class(self) -> Optional[str]: + """Get custom connector class name if using custom connector.""" + return self.config.get("connector", {}).get("customConnectorClass") + + @property + def use_custom_prompts(self) -> bool: + """Check if custom prompts should be used. Always true since prompts are required.""" + # Prompts are now required, so this is always true if prompts are configured + return bool(self.config.get("prompts", {}).get("customAnalysisPrompt") or + self.config.get("prompts", {}).get("customFinalAnswerPrompt")) + + @property + def custom_analysis_prompt(self) -> Optional[str]: + """Get custom analysis prompt (required for chatbot instances).""" + prompt = self.config.get("prompts", {}).get("customAnalysisPrompt") + if not prompt: + logger.warning("custom_analysis_prompt is not configured - this is required for chatbot instances") + return prompt + + @property + def custom_final_answer_prompt(self) -> Optional[str]: + """Get custom final answer prompt (required for chatbot instances).""" + prompt = self.config.get("prompts", {}).get("customFinalAnswerPrompt") + if not prompt: + logger.warning("custom_final_answer_prompt is not configured - this is required for chatbot instances") + return prompt + + @property + def custom_system_prompt(self) -> Optional[str]: + """Get custom system prompt for LangGraph workflow.""" + # Prefer customSystemPrompt, fallback to customAnalysisPrompt + prompt = self.config.get("prompts", {}).get("customSystemPrompt") + if not prompt: + prompt = self.config.get("prompts", {}).get("customAnalysisPrompt") + return prompt + + @property + def max_queries(self) -> int: + """Get maximum number of queries allowed.""" + return self.config.get("behavior", {}).get("maxQueries", 5) + + @property + def enable_web_research(self) -> bool: + """Check if web research is enabled.""" + return self.config.get("behavior", {}).get("enableWebResearch", True) + + @property + def enable_retry_on_empty(self) -> bool: + """Check if retry on empty results is enabled.""" + return self.config.get("behavior", {}).get("enableRetryOnEmpty", True) + + @property + def max_retry_attempts(self) -> int: + """Get maximum retry attempts.""" + return self.config.get("behavior", {}).get("maxRetryAttempts", 2) + + def get_connector_instance(self): + """ + Get connector instance based on configuration. + Uses the primary (first) connector type from the configured connectors. + + Returns: + Connector instance (PreprocessorConnector, or custom connector if configured) + """ + # Use primary connector type (first in the list) + connector_type = self.connector_type.lower() + + if connector_type == "preprocessor": + from modules.connectors.connectorPreprocessor import PreprocessorConnector + return PreprocessorConnector() + elif connector_type == "custom" and self.custom_connector_class: + # Dynamic import for custom connectors + try: + module_path, class_name = self.custom_connector_class.rsplit(".", 1) + module = __import__(module_path, fromlist=[class_name]) + connector_class = getattr(module, class_name) + return connector_class() + except Exception as e: + logger.error(f"Failed to load custom connector {self.custom_connector_class}: {e}") + raise ValueError(f"Invalid custom connector: {self.custom_connector_class}") + else: + # Default to PreprocessorConnector + logger.warning(f"Unknown connector type '{connector_type}', using PreprocessorConnector") + from modules.connectors.connectorPreprocessor import PreprocessorConnector + return PreprocessorConnector() + + +def get_chatbot_config(instance_id: Optional[str]) -> ChatbotConfig: + """ + Load chatbot configuration for a feature instance. + + Args: + instance_id: FeatureInstance ID (None for default config) + + Returns: + ChatbotConfig instance with merged defaults and instance config + """ + if not instance_id: + # Return default config if no instance ID provided + return ChatbotConfig() + + try: + rootInterface = getRootInterface() + featureInterface = getFeatureInterface(rootInterface.db) + + instance = featureInterface.getFeatureInstance(instance_id) + if not instance: + logger.warning(f"Feature instance {instance_id} not found, using default config") + return ChatbotConfig() + + # Verify it's a chatbot instance + if instance.featureCode != "chatbot": + logger.warning(f"Instance {instance_id} is not a chatbot instance, using default config") + return ChatbotConfig() + + # Load config from instance + instance_config = instance.config if hasattr(instance, 'config') and instance.config else {} + + return ChatbotConfig(instance_config) + + except Exception as e: + logger.error(f"Error loading chatbot config for instance {instance_id}: {e}") + # Return default config on error + return ChatbotConfig() diff --git a/modules/features/chatbot/chatbotConstants.py b/modules/features/chatbot/chatbotConstants.py deleted file mode 100644 index ac5405cc..00000000 --- a/modules/features/chatbot/chatbotConstants.py +++ /dev/null @@ -1,1008 +0,0 @@ -# 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, is_resumed: bool = False) -> str: - """ - Get the prompt for initial user input analysis. - - Args: - user_prompt: User's input prompt - context: Conversation context - is_resumed: If True, exclude system prompt (already in context from previous messages) - - Returns: - Formatted prompt string - """ - system_prompt = get_analysis_system_prompt() - - if is_resumed: - # System prompt already in context, don't repeat it - # Emphasize that the current question is primary - if context: - context_section = f""" -⚠️⚠️⚠️ KONTEXT (NUR FÜR REFERENZ - IGNORIEREN WENN NICHT BENÖTIGT) ⚠️⚠️⚠️ -{context} -⚠️⚠️⚠️ ENDE KONTEXT ⚠️⚠️⚠️ - -""" - else: - context_section = "" - - return f"""⚠️⚠️⚠️ AKTUELLE FRAGE (PRIMÄR - DIESE MUSS BEANTWORTET WERDEN) ⚠️⚠️⚠️ -User question: {user_prompt} -{context_section} -⚠️⚠️⚠️ WICHTIG ⚠️⚠️⚠️ -- Die AKTUELLE FRAGE OBEN ist die einzige Frage, die beantwortet werden muss -- Ignoriere den Kontext komplett, es sei denn die aktuelle Frage bezieht sich explizit darauf (z.B. "Was war das nochmal?", "Erkläre das genauer") -- Analysiere NUR die aktuelle Frage, nicht die Kontext-Fragen - -⚠️ 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 AKTUELLE Benutzeranfrage OBEN 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 -}} -""" - else: - # New chat: include 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, - is_resumed: bool = False, - has_db_results: bool = False, - has_web_results: bool = False -) -> 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 - is_resumed: If True, exclude system prompt (already in context from previous messages) - - Returns: - Complete formatted prompt string - """ - system_prompt = get_final_answer_system_prompt() - - if is_resumed: - # System prompt already in context, don't repeat it - # Emphasize that the current question is primary - if context: - context_section = f""" -⚠️⚠️⚠️ KONTEXT (NUR FÜR REFERENZ - IGNORIEREN WENN NICHT BENÖTIGT) ⚠️⚠️⚠️ -{context} -⚠️⚠️⚠️ ENDE KONTEXT ⚠️⚠️⚠️ - -""" - else: - context_section = "" - - # Build instructions based on what data sources are available - if has_web_results and not has_db_results: - # Only web research - emphasize web research - instructions = f"""⚠️⚠️⚠️ WICHTIG - NUR INTERNET-RECHERCHE VERFÜGBAR ⚠️⚠️⚠️ -- Die AKTUELLE FRAGE OBEN ist die einzige Frage, die beantwortet werden muss -- Ignoriere den Kontext komplett, es sei denn die aktuelle Frage bezieht sich explizit darauf -- Antworte NUR auf die aktuelle Frage, nicht auf Fragen aus dem Kontext - -{db_results_part}{web_results_part} - -KRITISCH: Verwende NUR die oben angegebenen Daten aus der INTERNET-RECHERCHE. Erfinde KEINE Werte. - -⚠️⚠️⚠️ WICHTIG - INTERNET-RECHERCHE VERWENDEN ⚠️⚠️⚠️ -- ✓ OBLIGATORISCH: Verwende die Informationen aus der INTERNET-RECHERCHE oben -- ✓ OBLIGATORISCH: Beginne mit "Aus meiner Web-Recherche..." oder "Aus meiner Internet-Recherche..." -- ✓ OBLIGATORISCH: Gib Quellen an: [Info] ([Quelle: Name](URL)) -- ✓ OBLIGATORISCH: Präsentiere die Informationen ausführlich und strukturiert -- ❌ ABSOLUT VERBOTEN: Erwähne Datenbank-Ergebnisse, wenn keine vorhanden sind -- ❌ ABSOLUT VERBOTEN: Daten erfinden - -WICHTIG: -- Beginne DIREKT mit "Aus meiner Web-Recherche..." oder "Aus meiner Internet-Recherche..." -- Klare, strukturierte Antwort mit Quellenangaben -- Präsentiere die gefundenen Informationen ausführlich""" - elif has_db_results and not has_web_results: - # Only database - use existing database-focused instructions - instructions = f"""⚠️⚠️⚠️ WICHTIG ⚠️⚠️⚠️ -- Die AKTUELLE FRAGE OBEN ist die einzige Frage, die beantwortet werden muss -- Ignoriere den Kontext komplett, es sei denn die aktuelle Frage bezieht sich explizit darauf -- Antworte NUR auf die aktuelle Frage, nicht auf Fragen aus dem Kontext - -{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 - -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)""" - else: - # Both database and web research - instructions = f"""⚠️⚠️⚠️ WICHTIG ⚠️⚠️⚠️ -- Die AKTUELLE FRAGE OBEN ist die einzige Frage, die beantwortet werden muss -- Ignoriere den Kontext komplett, es sei denn die aktuelle Frage bezieht sich explizit darauf -- Antworte NUR auf die aktuelle Frage, nicht auf Fragen aus dem Kontext - -{db_results_part}{web_results_part} - -KRITISCH: Verwende NUR die oben angegebenen Daten. Erfinde KEINE Werte. Wenn Daten fehlen, schreibe "Nicht verfügbar". - -⚠️⚠️⚠️ WICHTIG - BEIDE QUELLEN VERWENDEN ⚠️⚠️⚠️ -- ✓ OBLIGATORISCH: Verwende sowohl DATENBANK-ERGEBNISSE als auch INTERNET-RECHERCHE -- ✓ OBLIGATORISCH: Beginne mit "Aus der Datenbank habe ich..." für Datenbank-Ergebnisse -- ✓ OBLIGATORISCH: Verwende "Aus meiner Web-Recherche..." für Internet-Informationen -- ✓ OBLIGATORISCH: Gib Quellen für Web-Informationen an: [Info] ([Quelle: Name](URL)) -- ✓ OBLIGATORISCH: Zeige ALLE Artikel aus den DATENBANK-ERGEBNISSEN (bis zu 20 in Tabelle) - -WICHTIG: -- Beginne DIREKT mit "Aus der Datenbank habe ich..." für Datenbank-Ergebnisse -- Dann "Aus meiner Web-Recherche..." für Internet-Informationen -- Klare, strukturierte Antwort mit Quellenangaben""" - - return f"""⚠️⚠️⚠️ AKTUELLE FRAGE (PRIMÄR - DIESE MUSS BEANTWORTET WERDEN) ⚠️⚠️⚠️ -Antworte auf die folgende Frage des Nutzers: {user_prompt} -{context_section}{instructions}""" - else: - # New chat: include system prompt - # Build instructions based on what data sources are available - if has_web_results and not has_db_results: - # Only web research - emphasize web research - 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 aus der INTERNET-RECHERCHE. Erfinde KEINE Werte. - -⚠️⚠️⚠️ WICHTIG - INTERNET-RECHERCHE VERWENDEN ⚠️⚠️⚠️ -- ✓ OBLIGATORISCH: Verwende die Informationen aus der INTERNET-RECHERCHE oben -- ✓ OBLIGATORISCH: Beginne mit "Aus meiner Web-Recherche..." oder "Aus meiner Internet-Recherche..." -- ✓ OBLIGATORISCH: Gib Quellen an: [Info] ([Quelle: Name](URL)) -- ✓ OBLIGATORISCH: Präsentiere die Informationen ausführlich und strukturiert -- ❌ ABSOLUT VERBOTEN: Erwähne Datenbank-Ergebnisse, wenn keine vorhanden sind -- ❌ ABSOLUT VERBOTEN: Daten erfinden - -WICHTIG: -- Beginne DIREKT mit "Aus meiner Web-Recherche..." oder "Aus meiner Internet-Recherche..." -- Klare, strukturierte Antwort mit Quellenangaben -- Präsentiere die gefundenen Informationen ausführlich""" - elif has_db_results and not has_web_results: - # Only database - use existing database-focused instructions - 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)""" - else: - # Both database and web research - 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 - BEIDE QUELLEN VERWENDEN ⚠️⚠️⚠️ -- ✓ OBLIGATORISCH: Verwende sowohl DATENBANK-ERGEBNISSE als auch INTERNET-RECHERCHE -- ✓ OBLIGATORISCH: Beginne mit "Aus der Datenbank habe ich..." für Datenbank-Ergebnisse -- ✓ OBLIGATORISCH: Verwende "Aus meiner Web-Recherche..." für Internet-Informationen -- ✓ OBLIGATORISCH: Gib Quellen für Web-Informationen an: [Info] ([Quelle: Name](URL)) -- ✓ OBLIGATORISCH: Zeige ALLE Artikel aus den DATENBANK-ERGEBNISSEN (bis zu 20 in Tabelle) - -WICHTIG: -- Beginne DIREKT mit "Aus der Datenbank habe ich..." für Datenbank-Ergebnisse -- Dann "Aus meiner Web-Recherche..." für Internet-Informationen -- Klare, strukturierte Antwort mit Quellenangaben""" - diff --git a/modules/features/chatbot/chatbotUtils.py b/modules/features/chatbot/chatbotUtils.py new file mode 100644 index 00000000..dba0498c --- /dev/null +++ b/modules/features/chatbot/chatbotUtils.py @@ -0,0 +1,160 @@ +# Copyright (c) 2025 Patrick Motsch +# All rights reserved. +""" +Utility functions for the chatbot module. +Contains conversation name generation and other utilities. +""" + +import logging +import re +from typing import Optional + +from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, ProcessingModeEnum + +logger = logging.getLogger(__name__) + + +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_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 +""" diff --git a/modules/features/chatbot/langgraphChatbot.py b/modules/features/chatbot/langgraphChatbot.py new file mode 100644 index 00000000..09154a1b --- /dev/null +++ b/modules/features/chatbot/langgraphChatbot.py @@ -0,0 +1,345 @@ +# Copyright (c) 2025 Patrick Motsch +# All rights reserved. +""" +LangGraph-based chatbot implementation. +Uses LangGraph workflow with AI Center integration and connector tools. +""" + +import logging +from dataclasses import dataclass +from typing import Annotated, AsyncIterator, Any, Optional, List +from pydantic import BaseModel + +from langchain_core.messages import ( + BaseMessage, + HumanMessage, + SystemMessage, + trim_messages, +) +from langgraph.graph.message import add_messages +from langgraph.graph import StateGraph, START, END +from langgraph.graph.state import CompiledStateGraph +from langgraph.prebuilt import ToolNode +from langgraph.checkpoint.memory import MemorySaver + +from modules.features.chatbot.aiCenterAdapter import AICenterChatModel +from modules.features.chatbot.langgraphTools import ( + send_streaming_message, + create_sql_tool, + create_tavily_tools, +) +from modules.shared.configuration import APP_CONFIG + +logger = logging.getLogger(__name__) + + +class ChatState(BaseModel): + """Represents the state of a chat session.""" + + messages: Annotated[List[BaseMessage], add_messages] + + +@dataclass +class LangGraphChatbot: + """LangGraph-based chatbot with AI Center integration.""" + + model: AICenterChatModel + memory: Any + app: Optional[CompiledStateGraph] = None + system_prompt: str = "You are a helpful assistant." + + @classmethod + async def create( + cls, + services, + system_prompt: str, + connector_instance, + enable_web_research: bool = True, + tavily_api_key: Optional[str] = None, + context_window_size: int = 8000, + ) -> "LangGraphChatbot": + """ + Factory method to create and configure a LangGraphChatbot instance. + + Args: + services: Services instance with AI access + system_prompt: The system prompt to initialize the chatbot + connector_instance: Database connector instance (PreprocessorConnector) + enable_web_research: Whether to enable web research tools + tavily_api_key: Tavily API key for web research (if None, uses APP_CONFIG) + context_window_size: Maximum context window size in tokens + + Returns: + A configured LangGraphChatbot instance + """ + # Get Tavily API key from config if not provided + if tavily_api_key is None: + tavily_api_key = APP_CONFIG.get("Connector_AiTavily_API_SECRET") + + # Create AI Center chat model adapter + model = AICenterChatModel( + services=services, + system_prompt=system_prompt, + temperature=0.2 + ) + + # Create memory/checkpointer + memory = MemorySaver() + + instance = LangGraphChatbot( + model=model, + memory=memory, + system_prompt=system_prompt, + ) + + # Configure tools + configured_tools = await instance._configure_tools( + connector_instance, + enable_web_research, + tavily_api_key + ) + + # Build LangGraph app + instance.app = instance._build_app(memory, configured_tools, context_window_size) + + return instance + + async def _configure_tools( + self, + connector_instance, + enable_web_research: bool, + tavily_api_key: Optional[str] + ) -> List: + """ + Configure tools for the chatbot. + + Args: + connector_instance: Database connector instance + enable_web_research: Whether web research is enabled + tavily_api_key: Tavily API key + + Returns: + List of configured tools + """ + tools = [] + + # SQL tool using connector + sql_tool = create_sql_tool(connector_instance) + tools.append(sql_tool) + + # Streaming message tool + tools.append(send_streaming_message) + + # Tavily tools (if enabled) + if enable_web_research: + tavily_tools = create_tavily_tools(tavily_api_key, enable_web_research) + tools.extend(tavily_tools) + + logger.info(f"Configured {len(tools)} tools for LangGraph chatbot") + return tools + + def _build_app( + self, + memory: Any, + tools: List, + context_window_size: int + ) -> CompiledStateGraph[ChatState, None, ChatState, ChatState]: + """ + Builds the chatbot application workflow using LangGraph. + + Args: + memory: The chat memory/checkpointer to use + tools: The list of tools the chatbot can use + context_window_size: Maximum context window size + + Returns: + A compiled state graph representing the chatbot application + """ + # Bind tools to model + llm_with_tools = self.model.bind_tools(tools=tools) + + def select_window(msgs: List[BaseMessage]) -> List[BaseMessage]: + """Selects a window of messages that fit within the context window size. + + Args: + msgs: The list of messages to select from. + + Returns: + A list of messages that fit within the context window size. + """ + def approx_counter(items: List[BaseMessage]) -> int: + """Approximate token counter for messages. + + Args: + items: List of messages to count tokens for. + + Returns: + Approximate number of tokens in the messages. + """ + return sum(len(getattr(m, "content", "") or "") for m in items) + + return trim_messages( + msgs, + strategy="last", + token_counter=approx_counter, + max_tokens=context_window_size, + start_on="human", + end_on=("human", "tool"), + include_system=True, + ) + + def agent_node(state: ChatState) -> dict: + """Agent node for the chatbot workflow. + + Args: + state: The current chat state. + + Returns: + The updated chat state after processing. + """ + # Select the message window to fit in context (trim if needed) + window = select_window(state.messages) + + # Ensure the system prompt is present at the start + if not window or not isinstance(window[0], SystemMessage): + window = [SystemMessage(content=self.system_prompt)] + window + + # Call the LLM with tools + response = llm_with_tools.invoke(window) + + # Return the new state + return {"messages": [response]} + + def should_continue(state: ChatState) -> str: + """Determines whether to continue the workflow or end it. + + This conditional edge is called after the agent node to decide + whether to continue to the tools node (if the last message contains + tool calls) or to end the workflow (if no tool calls are present). + + Args: + state: The current chat state. + + Returns: + The next node to transition to ("tools" or END). + """ + # Get the last message + last_message = state.messages[-1] + + # Check if the last message contains tool calls + # If so, continue to the tools node; otherwise, end the workflow + return "tools" if getattr(last_message, "tool_calls", None) else END + + # Compose the workflow + workflow = StateGraph(ChatState) + workflow.add_node("agent", agent_node) + workflow.add_node("tools", ToolNode(tools=tools)) + workflow.add_edge(START, "agent") + workflow.add_conditional_edges("agent", should_continue) + workflow.add_edge("tools", "agent") + + return workflow.compile(checkpointer=memory) + + async def chat(self, message: str, chat_id: str = "default") -> List[BaseMessage]: + """ + Process a chat message by calling the LLM and tools and returns the chat history. + + Args: + message: The user message to process + chat_id: The chat thread ID + + Returns: + The list of messages in the chat history + """ + if not self.app: + raise RuntimeError("Chatbot app not initialized. Call create() first.") + + # Set the right thread ID for memory + config = {"configurable": {"thread_id": chat_id}} + + # Single-turn chat (non-streaming) + result = await self.app.ainvoke( + {"messages": [HumanMessage(content=message)]}, config=config + ) + + # Extract and return the messages from the result + return result["messages"] + + async def stream_events( + self, *, message: str, chat_id: str = "default" + ) -> AsyncIterator[dict]: + """ + Stream UI-focused events using astream_events v2. + + Args: + message: The user message to process + chat_id: Logical thread identifier; forwarded in the runnable config so + memory and tools are scoped per thread + + Yields: + dict: One of: + - ``{"type": "status", "label": str}`` for short progress updates. + - ``{"type": "final", "response": {"thread": str, "chat_history": list[dict]}}`` + where ``chat_history`` only includes ``user``/``assistant`` roles. + - ``{"type": "error", "message": str}`` if an exception occurs. + """ + if not self.app: + raise RuntimeError("Chatbot app not initialized. Call create() first.") + + # Thread-aware config for LangGraph/LangChain + config = {"configurable": {"thread_id": chat_id}} + + def _is_root(ev: dict) -> bool: + """Return True if the event is from the root run (v2: empty parent_ids).""" + return not ev.get("parent_ids") + + try: + async for event in self.app.astream_events( + {"messages": [HumanMessage(content=message)]}, + config=config, + version="v2", + ): + etype = event.get("event") + ename = event.get("name") or "" + edata = event.get("data") or {} + + # Stream human-readable progress via the special send_streaming_message tool + if etype == "on_tool_start" and ename == "send_streaming_message": + tool_in = edata.get("input") or {} + msg = tool_in.get("message") + if isinstance(msg, str) and msg.strip(): + yield {"type": "status", "label": msg.strip()} + continue + + # Emit the final payload when the root run finishes + if etype == "on_chain_end" and _is_root(event): + output_obj = edata.get("output") + + # Extract message list from the graph's final output + final_msgs = output_obj.get("messages", []) if isinstance(output_obj, dict) else [] + + # Normalize for the frontend (only user/assistant with text content) + chat_history_payload: List[dict] = [] + for m in final_msgs: + if isinstance(m, BaseMessage): + role = "user" if isinstance(m, HumanMessage) else "assistant" if isinstance(m, BaseMessage) else None + content = getattr(m, "content", "") + if role and content: + chat_history_payload.append({ + "role": role, + "content": content + }) + + yield { + "type": "final", + "response": { + "thread": chat_id, + "chat_history": chat_history_payload, + }, + } + return + + except Exception as exc: + # Emit a single error envelope and end the stream + logger.error(f"Exception in stream_events: {exc}", exc_info=True) + yield {"type": "error", "message": f"Fehler beim Verarbeiten: {exc}"} diff --git a/modules/features/chatbot/langgraphTools.py b/modules/features/chatbot/langgraphTools.py new file mode 100644 index 00000000..29773981 --- /dev/null +++ b/modules/features/chatbot/langgraphTools.py @@ -0,0 +1,166 @@ +# Copyright (c) 2025 Patrick Motsch +# All rights reserved. +""" +LangGraph-compatible tools for chatbot. +Wraps connectors and external services as LangGraph tools. +""" + +import logging +import json +from typing import Optional + +from langchain_core.tools import tool + +logger = logging.getLogger(__name__) + + +@tool +def send_streaming_message(message: str) -> str: + """Send a streaming message to the user to provide updates during processing. + + Use this tool to send short status updates to the user while you are working + on their request. This helps keep the user informed about what you are doing. + + Args: + message: A short German message describing what you are currently doing. + Examples: "Durchsuche Datenbank nach Lampen, LED, Leuchten, und Ähnlichem." + "Suche im Internet nach Produktinformationen." + "Analysiere Suchergebnisse." + + Returns: + A confirmation that the message was sent. + """ + # This tool doesn't actually do anything - it's just for the AI to signal + # what it's doing to the frontend via the tool call mechanism + return f"Status-Update gesendet: {message}" + + +def create_sql_tool(connector_instance): + """ + Create a LangGraph-compatible SQL tool using a connector instance. + + Args: + connector_instance: PreprocessorConnector or similar connector instance + + Returns: + LangChain tool for SQL queries + """ + # Store connector in closure + connector = connector_instance + + @tool + async def execute_sql_query(query: str) -> str: + """Execute a SQL SELECT query on the database. + + This tool allows you to query the database to find articles, prices, + inventory levels, and other information. + + Args: + query: A valid SQL SELECT query. Only SELECT queries are allowed. + Use double quotes for column names with spaces or special characters. + Example: SELECT "Artikelnummer", "Artikelbezeichnung" FROM Artikel + WHERE "Artikelbezeichnung" LIKE '%Lampe%' LIMIT 20 + + Returns: + Query results as formatted string with data rows + """ + try: + logger.info(f"Executing SQL query via connector: {query[:100]}...") + + # Ensure connector is initialized + if connector is None: + return "Error: Database connector not initialized" + + # Execute query + result = await connector.executeQuery(query, return_json=True) + + if isinstance(result, dict): + # Return formatted text result + text_result = result.get("text", "Query executed successfully but returned no results.") + # Also include data count if available + data = result.get("data", []) + if data: + text_result += f"\n\nFound {len(data)} row(s)." + return text_result + else: + # Return string result directly + return str(result) + + except Exception as e: + error_msg = f"Error executing SQL query: {str(e)}" + logger.error(error_msg, exc_info=True) + return error_msg + + # Set tool metadata for better AI understanding + execute_sql_query.name = "execute_sql_query" + execute_sql_query.description = """Execute a SQL SELECT query on the database. + + Use this tool to search for articles, check prices, inventory levels, suppliers, etc. + Only SELECT queries are allowed. Use double quotes for column names with spaces. + + Database tables: Artikel, Einkaufspreis_neu, Lagerplatz_Artikel, Lagerplatz + + Example queries: + - SELECT "Artikelnummer", "Artikelbezeichnung" FROM Artikel WHERE "Artikelbezeichnung" LIKE '%Lampe%' + - SELECT a."Artikelnummer", e."EP_CHF" FROM Artikel a LEFT JOIN Einkaufspreis_neu e ON a."I_ID" = e."ARTIKEL" + - SELECT a."Artikelnummer", l."S_IST_BESTAND" FROM Artikel a LEFT JOIN Lagerplatz_Artikel l ON a."I_ID" = l."R_ARTIKEL" + """ + + return execute_sql_query + + +def create_tavily_tools(tavily_api_key: Optional[str] = None, enable_web_research: bool = True): + """ + Create Tavily search tools for web research. + + Args: + tavily_api_key: Tavily API key (if None, tools will return error messages) + enable_web_research: Whether web research is enabled + + Returns: + List of Tavily tools (search and extract) + """ + tools = [] + + if not enable_web_research or not tavily_api_key: + # Return dummy tools that explain web research is disabled + @tool + def tavily_search_disabled(query: str) -> str: + """Web research is disabled for this chatbot instance.""" + return "Web research is not enabled for this chatbot instance." + + @tool + def tavily_extract_disabled(urls: str) -> str: + """Web research is disabled for this chatbot instance.""" + return "Web research is not enabled for this chatbot instance." + + return [tavily_search_disabled, tavily_extract_disabled] + + try: + from langchain_tavily import TavilySearchResults, TavilyExtract + + # Create Tavily search tool + tavily_search = TavilySearchResults( + tavily_api_key=tavily_api_key, + max_results=5 + ) + + # Create Tavily extract tool + tavily_extract = TavilyExtract(tavily_api_key=tavily_api_key) + + return [tavily_search, tavily_extract] + + except ImportError: + logger.warning("langchain_tavily not available, creating dummy tools") + + @tool + def tavily_search_fallback(query: str) -> str: + """Tavily search tool (not available - langchain_tavily not installed).""" + return "Tavily search is not available. Please install langchain_tavily package." + + @tool + def tavily_extract_fallback(urls: str) -> str: + """Tavily extract tool (not available - langchain_tavily not installed).""" + return "Tavily extract is not available. Please install langchain_tavily package." + + return [tavily_search_fallback, tavily_extract_fallback] diff --git a/modules/features/chatbot/mainChatbot.py b/modules/features/chatbot/mainChatbot.py index 6a172adf..b539ea9e 100644 --- a/modules/features/chatbot/mainChatbot.py +++ b/modules/features/chatbot/mainChatbot.py @@ -392,15 +392,12 @@ from modules.shared.timeUtils import getUtcTimestamp, parseTimestamp from modules.services import getInterface as getServices from modules.features.chatbot import interfaceFeatureChatbot from modules.features.chatbot.eventManager import get_event_manager -from modules.workflows.methods.methodAi.methodAi import MethodAi -from modules.connectors.connectorPreprocessor import PreprocessorConnector -from modules.features.chatbot.chatbotConstants import ( - get_initial_analysis_prompt, +from modules.features.chatbot.chatbotUtils import ( generate_conversation_name, - get_final_answer_system_prompt, - get_final_answer_prompt_with_results, - get_empty_results_retry_instructions ) +from modules.features.chatbot.chatbotConfig import get_chatbot_config, ChatbotConfig +from modules.features.chatbot.langgraphChatbot import LangGraphChatbot +from langchain_core.messages import HumanMessage import base64 logger = logging.getLogger(__name__) @@ -460,6 +457,16 @@ async def chatProcess( ChatWorkflow instance """ try: + # Load chatbot configuration for this instance + chatbot_config = get_chatbot_config(featureInstanceId) + logger.info(f"Loaded chatbot config for instance {featureInstanceId}: connector={chatbot_config.connector_type}, maxQueries={chatbot_config.max_queries}") + + # Validate that required system prompt is configured + if not chatbot_config.custom_system_prompt: + error_msg = f"Chatbot instance {featureInstanceId} is missing required customSystemPrompt configuration" + logger.error(error_msg) + raise ValueError(error_msg) + # Get services normally (for other services like chat, ai, etc.) services = getServices(currentUser, None, mandateId=mandateId) @@ -612,7 +619,8 @@ async def chatProcess( services, workflow.id, userInput, - userMessage.id + userMessage.id, + chatbot_config )) # Reload workflow to include new message @@ -624,7 +632,7 @@ async def chatProcess( raise -async def _execute_queries_parallel(queries: List[Dict[str, Any]]) -> Dict[str, Any]: +async def _execute_queries_parallel(queries: List[Dict[str, Any]], chatbot_config: ChatbotConfig) -> Dict[str, Any]: """ Execute multiple SQL queries in parallel with shared connector. @@ -633,6 +641,7 @@ async def _execute_queries_parallel(queries: List[Dict[str, Any]]) -> Dict[str, - "query": SQL query string - "purpose": Description of what the query retrieves - "table": Primary table name + chatbot_config: ChatbotConfig instance for connector selection Returns: Dictionary mapping query indices to results: @@ -640,8 +649,8 @@ 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 """ - # Create single connector instance to reuse across all queries - connector = PreprocessorConnector() + # Create connector instance based on configuration + connector = chatbot_config.get_connector_instance() try: async def execute_single_query(idx: int, query_info: Dict[str, Any]): """Execute a single query using shared connector.""" @@ -775,6 +784,248 @@ async def _check_workflow_stopped(interfaceDbChat, workflowId: str) -> bool: return False +def _build_final_answer_prompt_with_results( + system_prompt: str, + user_prompt: str, + context: str, + db_results_part: str, + web_results_part: str, + is_resumed: bool = False, + has_db_results: bool = False, + has_web_results: bool = False +) -> str: + """ + Build the complete prompt for generating the final answer with database and web results. + Uses the provided system_prompt from configuration instead of hardcoded prompts. + + Args: + system_prompt: System prompt from chatbot configuration + user_prompt: User's original prompt + context: Conversation context + db_results_part: Formatted database results section + web_results_part: Formatted web research results section + is_resumed: If True, exclude system prompt (already in context from previous messages) + has_db_results: Whether database results are available + has_web_results: Whether web research results are available + + Returns: + Complete formatted prompt string + """ + if is_resumed: + # System prompt already in context, don't repeat it + # Emphasize that the current question is primary + if context: + context_section = f""" +⚠️⚠️⚠️ KONTEXT (NUR FÜR REFERENZ - IGNORIEREN WENN NICHT BENÖTIGT) ⚠️⚠️⚠️ +{context} +⚠️⚠️⚠️ ENDE KONTEXT ⚠️⚠️⚠️ + +""" + else: + context_section = "" + + # Build instructions based on what data sources are available + if has_web_results and not has_db_results: + # Only web research - emphasize web research + instructions = f"""⚠️⚠️⚠️ WICHTIG - NUR INTERNET-RECHERCHE VERFÜGBAR ⚠️⚠️⚠️ +- Die AKTUELLE FRAGE OBEN ist die einzige Frage, die beantwortet werden muss +- Ignoriere den Kontext komplett, es sei denn die aktuelle Frage bezieht sich explizit darauf +- Antworte NUR auf die aktuelle Frage, nicht auf Fragen aus dem Kontext + +{db_results_part}{web_results_part} + +KRITISCH: Verwende NUR die oben angegebenen Daten aus der INTERNET-RECHERCHE. Erfinde KEINE Werte. + +⚠️⚠️⚠️ WICHTIG - INTERNET-RECHERCHE VERWENDEN ⚠️⚠️⚠️ +- ✓ OBLIGATORISCH: Verwende die Informationen aus der INTERNET-RECHERCHE oben +- ✓ OBLIGATORISCH: Beginne mit "Aus meiner Web-Recherche..." oder "Aus meiner Internet-Recherche..." +- ✓ OBLIGATORISCH: Gib Quellen an: [Info] ([Quelle: Name](URL)) +- ✓ OBLIGATORISCH: Präsentiere die Informationen ausführlich und strukturiert +- ❌ ABSOLUT VERBOTEN: Erwähne Datenbank-Ergebnisse, wenn keine vorhanden sind +- ❌ ABSOLUT VERBOTEN: Daten erfinden + +WICHTIG: +- Beginne DIREKT mit "Aus meiner Web-Recherche..." oder "Aus meiner Internet-Recherche..." +- Klare, strukturierte Antwort mit Quellenangaben +- Präsentiere die gefundenen Informationen ausführlich""" + elif has_db_results and not has_web_results: + # Only database - use existing database-focused instructions + instructions = f"""⚠️⚠️⚠️ WICHTIG ⚠️⚠️⚠️ +- Die AKTUELLE FRAGE OBEN ist die einzige Frage, die beantwortet werden muss +- Ignoriere den Kontext komplett, es sei denn die aktuelle Frage bezieht sich explizit darauf +- Antworte NUR auf die aktuelle Frage, nicht auf Fragen aus dem Kontext + +{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 + +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)""" + elif not has_db_results and not has_web_results: + # No results from either source - but database query was executed + instructions = f"""⚠️⚠️⚠️ KRITISCH - DATENBANKABFRAGE WURDE AUSGEFÜHRT ⚠️⚠️⚠️ +- Die AKTUELLE FRAGE OBEN ist die einzige Frage, die beantwortet werden muss +- Ignoriere den Kontext komplett, es sei denn die aktuelle Frage bezieht sich explizit darauf +- Antworte NUR auf die aktuelle Frage, nicht auf Fragen aus dem Kontext + +{db_results_part}{web_results_part} + +⚠️⚠️⚠️ ABSOLUT KRITISCH - DATENBANKABFRAGE WURDE AUSGEFÜHRT ⚠️⚠️⚠️ +Die Datenbankabfrage wurde AUSGEFÜHRT, hat aber keine Ergebnisse zurückgegeben. +DU HAST ZUGRIFF AUF DIE DATENBANK - die Abfrage wurde durchgeführt! + +VERBOTEN - NIEMALS SAGEN: +- "Ich habe keinen Zugriff auf die Datenbank" +- "Ich kann nicht auf die Datenbank zugreifen" +- "Es tut mir leid, aber ich habe keinen Zugriff" +- "Ich habe keinen Zugriff auf Echtzeit-Datenbanken" +- Jede andere Formulierung, die suggeriert, dass du keinen Zugriff hast! + +RICHTIG - SAGE STATTDESSEN: +- "Es wurden keine Artikel gefunden" +- "Keine passenden Artikel in der Datenbank gefunden" +- "Die Datenbanksuche ergab keine Treffer" +- "Ich habe in der Datenbank gesucht, aber keine passenden Artikel gefunden" + +WICHTIG: Die Datenbank wurde durchsucht - es wurden nur keine passenden Artikel gefunden! +Beginne deine Antwort mit: "Ich habe in der Datenbank gesucht, aber..." oder "Es wurden keine Artikel gefunden..." oder ähnlich.""" + else: + # Both database and web research + instructions = f"""⚠️⚠️⚠️ WICHTIG ⚠️⚠️⚠️ +- Die AKTUELLE FRAGE OBEN ist die einzige Frage, die beantwortet werden muss +- Ignoriere den Kontext komplett, es sei denn die aktuelle Frage bezieht sich explizit darauf +- Antworte NUR auf die aktuelle Frage, nicht auf Fragen aus dem Kontext + +{db_results_part}{web_results_part} + +KRITISCH: Verwende NUR die oben angegebenen Daten. Erfinde KEINE Werte. Wenn Daten fehlen, schreibe "Nicht verfügbar". + +⚠️⚠️⚠️ WICHTIG - BEIDE QUELLEN VERWENDEN ⚠️⚠️⚠️ +- ✓ OBLIGATORISCH: Verwende sowohl DATENBANK-ERGEBNISSE als auch INTERNET-RECHERCHE +- ✓ OBLIGATORISCH: Beginne mit "Aus der Datenbank habe ich..." für Datenbank-Ergebnisse +- ✓ OBLIGATORISCH: Verwende "Aus meiner Web-Recherche..." für Internet-Informationen +- ✓ OBLIGATORISCH: Gib Quellen für Web-Informationen an: [Info] ([Quelle: Name](URL)) +- ✓ OBLIGATORISCH: Zeige ALLE Artikel aus den DATENBANK-ERGEBNISSEN (bis zu 20 in Tabelle) + +WICHTIG: +- Beginne DIREKT mit "Aus der Datenbank habe ich..." für Datenbank-Ergebnisse +- Dann "Aus meiner Web-Recherche..." für Internet-Informationen +- Klare, strukturierte Antwort mit Quellenangaben""" + + return f"""⚠️⚠️⚠️ AKTUELLE FRAGE (PRIMÄR - DIESE MUSS BEANTWORTET WERDEN) ⚠️⚠️⚠️ +Antworte auf die folgende Frage des Nutzers: {user_prompt} +{context_section}{instructions}""" + else: + # New chat: include system prompt + # Build instructions based on what data sources are available + if has_web_results and not has_db_results: + # Only web research - emphasize web research + 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 aus der INTERNET-RECHERCHE. Erfinde KEINE Werte. + +⚠️⚠️⚠️ WICHTIG - INTERNET-RECHERCHE VERWENDEN ⚠️⚠️⚠️ +- ✓ OBLIGATORISCH: Verwende die Informationen aus der INTERNET-RECHERCHE oben +- ✓ OBLIGATORISCH: Beginne mit "Aus meiner Web-Recherche..." oder "Aus meiner Internet-Recherche..." +- ✓ OBLIGATORISCH: Gib Quellen an: [Info] ([Quelle: Name](URL)) +- ✓ OBLIGATORISCH: Präsentiere die Informationen ausführlich und strukturiert +- ❌ ABSOLUT VERBOTEN: Erwähne Datenbank-Ergebnisse, wenn keine vorhanden sind +- ❌ ABSOLUT VERBOTEN: Daten erfinden + +WICHTIG: +- Beginne DIREKT mit "Aus meiner Web-Recherche..." oder "Aus meiner Internet-Recherche..." +- Klare, strukturierte Antwort mit Quellenangaben +- Präsentiere die gefundenen Informationen ausführlich""" + elif has_db_results and not has_web_results: + # Only database - use existing database-focused instructions + 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 + +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)""" + elif not has_db_results and not has_web_results: + # No results from either source - but database query was executed + return f"""{system_prompt} + +Antworte auf die folgende Frage des Nutzers: {user_prompt}{context} + +{db_results_part}{web_results_part} + +⚠️⚠️⚠️ KRITISCH - DATENBANKABFRAGE WURDE AUSGEFÜHRT ⚠️⚠️⚠️ +Die Datenbankabfrage wurde AUSGEFÜHRT, hat aber keine Ergebnisse zurückgegeben. +DU HAST ZUGRIFF AUF DIE DATENBANK - die Abfrage wurde durchgeführt! + +VERBOTEN - NIEMALS SAGEN: +- "Ich habe keinen Zugriff auf die Datenbank" +- "Ich kann nicht auf die Datenbank zugreifen" +- "Es tut mir leid, aber ich habe keinen Zugriff" +- "Ich habe keinen Zugriff auf Echtzeit-Datenbanken" +- Jede andere Formulierung, die suggeriert, dass du keinen Zugriff hast! + +RICHTIG - SAGE STATTDESSEN: +- "Es wurden keine Artikel gefunden" +- "Keine passenden Artikel in der Datenbank gefunden" +- "Die Datenbanksuche ergab keine Treffer" +- "Ich habe in der Datenbank gesucht, aber keine passenden Artikel gefunden" + +WICHTIG: Die Datenbank wurde durchsucht - es wurden nur keine passenden Artikel gefunden! +Beginne deine Antwort mit: "Ich habe in der Datenbank gesucht, aber..." oder "Es wurden keine Artikel gefunden..." oder ähnlich.""" + else: + # Both database and web research + 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 - BEIDE QUELLEN VERWENDEN ⚠️⚠️⚠️ +- ✓ OBLIGATORISCH: Verwende sowohl DATENBANK-ERGEBNISSE als auch INTERNET-RECHERCHE +- ✓ OBLIGATORISCH: Beginne mit "Aus der Datenbank habe ich..." für Datenbank-Ergebnisse +- ✓ OBLIGATORISCH: Verwende "Aus meiner Web-Recherche..." für Internet-Informationen +- ✓ OBLIGATORISCH: Gib Quellen für Web-Informationen an: [Info] ([Quelle: Name](URL)) +- ✓ OBLIGATORISCH: Zeige ALLE Artikel aus den DATENBANK-ERGEBNISSEN (bis zu 20 in Tabelle) + +WICHTIG: +- Beginne DIREKT mit "Aus der Datenbank habe ich..." für Datenbank-Ergebnisse +- Dann "Aus meiner Web-Recherche..." für Internet-Informationen +- Klare, strukturierte Antwort mit Quellenangaben""" + + def _buildWebResearchQuery(userPrompt: str, workflowMessages: List, queryResults: Optional[Dict[str, Any]] = None) -> str: """ Build enriched web research query by extracting product context from conversation history and current prompt. @@ -1248,11 +1499,12 @@ async def _processChatbotMessage( services, workflowId: str, userInput: UserInputRequest, - userMessageId: str + userMessageId: str, + chatbot_config: ChatbotConfig ): """ - Process chatbot message in background. - Analyzes user input and generates list of queries, then streams them back. + Process chatbot message using LangGraph workflow. + Uses LangGraph to handle the conversation flow with tools (SQL, Tavily, streaming). """ event_manager = get_event_manager() @@ -1278,39 +1530,177 @@ async def _processChatbotMessage( logger.info(f"Workflow {workflowId} was stopped, aborting processing") return - # Build conversation context from history - # Only include context if the new question might need it (e.g., references to previous messages) - context = "" - is_resumed = len(workflow.messages) > 0 if workflow.messages else False - - # Check if the current question might need context (references like "it", "that", "previous", "earlier", etc.) - needs_context = False - if is_resumed: - current_prompt_lower = userInput.prompt.lower() - context_keywords = ["es", "das", "dieses", "jenes", "vorherige", "frühere", "vorhin", "oben", - "it", "that", "this", "previous", "earlier", "above", "mentioned", "before", - "davor", "dazu", "darauf", "damit", "davon"] - needs_context = any(keyword in current_prompt_lower for keyword in context_keywords) - - if is_resumed and needs_context: - recent_messages = workflow.messages[-3:] # Reduced from 5 to 3 for less distraction - context = "\n\n⚠️ WICHTIG - KONTEXT NUR FÜR REFERENZ ⚠️\n" - context += "Die folgende Konversation ist nur als Referenz, falls die aktuelle Frage darauf Bezug nimmt.\n" - context += "FOKUSSIERE AUF DIE AKTUELLE FRAGE OBEN!\n\n" - context += "Vorherige Konversation:\n" - for msg in recent_messages: - if msg.role == "user": - context += f"User: {msg.message}\n" - elif msg.role == "assistant": - context += f"Assistant: {msg.message}\n" - await services.ai.ensureAiObjectsInitialized() - # Step 1: Analyze user input to generate queries + # Get connector instance + connector = chatbot_config.get_connector_instance() + + # Get system prompt + system_prompt = chatbot_config.custom_system_prompt + if not system_prompt: + raise ValueError(f"System prompt not configured for chatbot instance") + + # Create LangGraph chatbot instance + logger.info(f"Creating LangGraph chatbot for workflow {workflowId}") + chatbot = await LangGraphChatbot.create( + services=services, + system_prompt=system_prompt, + connector_instance=connector, + enable_web_research=chatbot_config.enable_web_research, + context_window_size=8000 + ) + + # Process message using LangGraph streaming + logger.info(f"Processing message with LangGraph for workflow {workflowId}") + final_answer = None + chat_history = [] + + async for event in chatbot.stream_events(message=userInput.prompt, chat_id=workflowId): + # Check if workflow was stopped + if await _check_workflow_stopped(interfaceDbChat, workflowId): + logger.info(f"Workflow {workflowId} was stopped during processing") + return + + event_type = event.get("type") + + if event_type == "status": + # Emit status update + label = event.get("label", "") + await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, label, log_type="info") + + elif event_type == "final": + # Final response received + response_data = event.get("response", {}) + chat_history = response_data.get("chat_history", []) + # Extract final answer from chat history (last assistant message) + for msg in reversed(chat_history): + if msg.get("role") == "assistant": + final_answer = msg.get("content", "") + break + + elif event_type == "error": + # Error occurred + error_msg = event.get("message", "Unknown error") + logger.error(f"LangGraph error: {error_msg}") + await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, f"Fehler: {error_msg}", log_type="error") + final_answer = f"Entschuldigung, ein Fehler ist aufgetreten: {error_msg}" + + # Close connector + try: + await connector.close() + except Exception as e: + logger.warning(f"Error closing connector: {e}") + + # Check if workflow was stopped before storing answer + if await _check_workflow_stopped(interfaceDbChat, workflowId): + logger.info(f"Workflow {workflowId} was stopped, not storing final message") + return + + # Store final answer if we have one + if final_answer: + workflow = interfaceDbChat.getWorkflow(workflowId) + message_id = f"msg_{uuid.uuid4()}" + assistantMessageData = { + "id": message_id, + "workflowId": workflowId, + "parentMessageId": userMessageId, + "message": final_answer, + "role": "assistant", + "status": "last", + "sequenceNr": len(workflow.messages) + 1, + "publishedAt": getUtcTimestamp(), + "success": True, + "roundNumber": workflow.currentRound, + "taskNumber": 0, + "actionNumber": 0 + } + + assistantMessage = interfaceDbChat.createMessage(assistantMessageData) + logger.info(f"Stored assistant message: {assistantMessage.id}") + + # Emit message event for streaming + message_timestamp = parseTimestamp(assistantMessage.publishedAt, default=getUtcTimestamp()) + await event_manager.emit_event( + context_id=workflowId, + event_type="chatdata", + data={ + "type": "message", + "createdAt": message_timestamp, + "item": assistantMessage.dict() + }, + event_category="chat" + ) + + # Update workflow status to completed + if not await _check_workflow_stopped(interfaceDbChat, workflowId): + interfaceDbChat.updateWorkflow(workflowId, { + "status": "completed", + "lastActivity": getUtcTimestamp() + }) + + await event_manager.emit_event( + context_id=workflowId, + event_type="complete", + data={"workflowId": workflowId}, + event_category="workflow", + message="Chatbot-Verarbeitung abgeschlossen", + step="complete" + ) + + # Schedule cleanup + await event_manager.cleanup(workflowId, delay=300.0) + + logger.info(f"LangGraph processing completed for workflow {workflowId}") + + except Exception as e: logger.info("Analyzing user input to generate queries...") await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, "Analysiere Benutzeranfrage...") - analysisPrompt = get_initial_analysis_prompt(userInput.prompt, context, is_resumed) + # Use custom prompt from configuration (already validated at start of chatProcess) + analysisPrompt = chatbot_config.custom_analysis_prompt.replace("{userPrompt}", userInput.prompt).replace("{context}", context or "") + + # CRITICAL: Add explicit JSON format requirement to ensure AI returns JSON + json_format_instruction = """ + +⚠️⚠️⚠️ ABSOLUT KRITISCH - JSON-FORMAT ERFORDERLICH ⚠️⚠️⚠️ +DU MUSST DEINE ANTWORT AUSSCHLIESSLICH IM JSON-FORMAT GEBEN! +ANTWORTE NICHT MIT NORMALEM TEXT ODER EINER CHAT-ANTWORT! +DEINE ANTWORT MUSS EIN GÜLTIGES JSON-OBJEKT SEIN! + +Erforderliches JSON-Format: +{ + "needsDatabaseQuery": true/false, + "needsWebResearch": true/false, + "sqlQueries": [ + { + "query": "SQL-Abfrage hier", + "purpose": "Zweck der Abfrage", + "table": "Haupttabelle" + } + ], + "reasoning": "Begründung für die Abfragen" +} + +⚠️⚠️⚠️ KRITISCH - WANN DATENBANKABFRAGE ERFORDERLICH ⚠️⚠️⚠️ +SETZE "needsDatabaseQuery": true, WENN: +- Der Nutzer nach Artikeln, Produkten, Preisen, Lagerbeständen, Lieferanten fragt +- Der Nutzer nach Informationen aus der Datenbank fragt (auch allgemeine Fragen!) +- Der Nutzer eine Frage stellt, die mit Datenbank-Daten beantwortet werden kann +- Du dir nicht sicher bist - dann setze "needsDatabaseQuery": true und führe eine allgemeine Abfrage durch! + +VERBOTEN: +- "needsDatabaseQuery": false setzen, nur weil die Frage allgemein klingt +- "needsDatabaseQuery": false setzen, ohne zu prüfen, ob Datenbank-Daten helfen könnten +- Chat-Antworten geben statt Datenbankabfragen durchzuführen + +WICHTIG: +- Antworte NUR mit dem JSON-Objekt, KEIN zusätzlicher Text davor oder danach! +- KEINE Erklärungen, KEINE Begrüßungen, KEINE Chat-Antworten! +- NUR das JSON-Objekt! +- Bei Unsicherheit: IMMER "needsDatabaseQuery": true setzen! +""" + analysisPrompt = analysisPrompt + json_format_instruction + logger.info("Using custom analysis prompt from instance config with JSON format requirement") # AI call for analysis method_ai = MethodAi(services) @@ -1326,42 +1716,178 @@ async def _processChatbotMessage( logger.info(f"Workflow {workflowId} was stopped during analysis, aborting processing") return - # Extract content from ActionResult + # Retry logic for failed analysis (max 3 attempts) + max_analysis_retries = 3 + analysis_retry_count = 0 + analysis = None analysis_content = None - if analysis_result.success and analysis_result.documents: - analysis_content = analysis_result.documents[0].documentData - if isinstance(analysis_content, bytes): - analysis_content = analysis_content.decode('utf-8') - if not analysis_content: - logger.warning("Analysis failed, using fallback") - analysis = {} - else: + while analysis_retry_count < max_analysis_retries: + # Extract content from ActionResult + analysis_content = None + if analysis_result.success and analysis_result.documents: + analysis_content = analysis_result.documents[0].documentData + if isinstance(analysis_content, bytes): + analysis_content = analysis_content.decode('utf-8') + + # Validate analysis was successful + if not analysis_content: + analysis_retry_count += 1 + if analysis_retry_count < max_analysis_retries: + logger.warning(f"Analysis failed (attempt {analysis_retry_count}/{max_analysis_retries}): No content returned from AI, retrying...") + await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, f"Analyse fehlgeschlagen, Versuch {analysis_retry_count}/{max_analysis_retries}...", log_type="warning") + # Retry analysis + analysis_result = await method_ai.process({ + "aiPrompt": analysisPrompt, + "documentList": None, + "resultType": "json", + "simpleMode": True + }) + continue + else: + error_msg = "Die Analyse Ihrer Anfrage ist nach mehreren Versuchen fehlgeschlagen. Bitte versuchen Sie es später erneut oder formulieren Sie Ihre Frage anders." + logger.error(f"Analysis failed after {max_analysis_retries} attempts: No content returned from AI") + await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, f"Fehler: Analyse nach {max_analysis_retries} Versuchen fehlgeschlagen", log_type="error") + # Store error message as assistant response + workflow = interfaceDbChat.getWorkflow(workflowId) + message_id = f"msg_{uuid.uuid4()}" + assistantMessageData = { + "id": message_id, + "workflowId": workflowId, + "parentMessageId": userMessageId, + "message": error_msg, + "role": "assistant", + "status": "last", + "sequenceNr": len(workflow.messages) + 1, + "publishedAt": getUtcTimestamp(), + "success": False, + "roundNumber": workflow.currentRound, + "taskNumber": 0, + "actionNumber": 0 + } + assistantMessage = interfaceDbChat.createMessage(assistantMessageData) + logger.info(f"Stored error message due to failed analysis after {max_analysis_retries} attempts: {assistantMessage.id}") + return + analysis = _extractJsonFromResponse(analysis_content) + if analysis is None: + analysis_retry_count += 1 + if analysis_retry_count < max_analysis_retries: + logger.warning(f"Failed to extract JSON from analysis response (attempt {analysis_retry_count}/{max_analysis_retries}), retrying...") + logger.debug(f"Analysis content: {analysis_content[:500] if analysis_content else 'None'}") + await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, f"JSON-Extraktion fehlgeschlagen, Versuch {analysis_retry_count}/{max_analysis_retries}...", log_type="warning") + # Retry analysis + analysis_result = await method_ai.process({ + "aiPrompt": analysisPrompt, + "documentList": None, + "resultType": "json", + "simpleMode": True + }) + continue + else: + error_msg = "Die Analyse Ihrer Anfrage konnte nach mehreren Versuchen nicht verarbeitet werden. Bitte versuchen Sie es später erneut oder formulieren Sie Ihre Frage anders." + logger.error(f"Failed to extract JSON from analysis response after {max_analysis_retries} attempts. Content: {analysis_content[:500] if analysis_content else 'None'}") + await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, f"Fehler: JSON-Extraktion nach {max_analysis_retries} Versuchen fehlgeschlagen", log_type="error") + # Store error message as assistant response + workflow = interfaceDbChat.getWorkflow(workflowId) + message_id = f"msg_{uuid.uuid4()}" + assistantMessageData = { + "id": message_id, + "workflowId": workflowId, + "parentMessageId": userMessageId, + "message": error_msg, + "role": "assistant", + "status": "last", + "sequenceNr": len(workflow.messages) + 1, + "publishedAt": getUtcTimestamp(), + "success": False, + "roundNumber": workflow.currentRound, + "taskNumber": 0, + "actionNumber": 0 + } + assistantMessage = interfaceDbChat.createMessage(assistantMessageData) + logger.info(f"Stored error message due to failed JSON extraction after {max_analysis_retries} attempts: {assistantMessage.id}") + return + + # Successfully extracted analysis, break retry loop + break # Extract analysis results needsDatabaseQuery = analysis.get("needsDatabaseQuery", False) if analysis else False needsWebResearch = analysis.get("needsWebResearch", False) if analysis else False - sql_queries = analysis.get("sqlQueries", []) + sql_queries = analysis.get("sqlQueries", []) if analysis else [] # Support legacy single query format for backward compatibility - if not sql_queries and analysis.get("sqlQuery"): + if not sql_queries and analysis and analysis.get("sqlQuery"): sql_queries = [{ "query": analysis.get("sqlQuery", ""), "purpose": "Database query", "table": "Unknown" }] - reasoning = analysis.get("reasoning", "") + reasoning = analysis.get("reasoning", "") if analysis else "" - # Check if we need web research for certifications - user_prompt_lower = userInput.prompt.lower() - certification_keywords = ["ul", "ce", "tüv", "vde", "iec", "iso", "zertifiziert", "certified", "certification"] - has_certification = any(keyword in user_prompt_lower for keyword in certification_keywords) - if has_certification and not needsWebResearch: - logger.warning("Certification detected but needsWebResearch is false - forcing to true") - needsWebResearch = True + # CRITICAL: If connectors are configured, ALWAYS use database if user asks about products/articles/inventory + # Override AI decision if it says "no database query" but connectors are available + if chatbot_config.connector_types and len(chatbot_config.connector_types) > 0: + user_prompt_lower = userInput.prompt.lower() + # Keywords that indicate database query is needed + db_keywords = [ + "artikel", "produkt", "ware", "lager", "bestand", "preis", "lieferant", + "led", "lampe", "motor", "kabel", "schraube", "sensor", "netzteil", + "wie viele", "zeig mir", "suche", "finde", "gibt es", "haben wir", + "article", "product", "inventory", "stock", "price", "supplier", + "how many", "show me", "search", "find", "do we have" + ] + has_db_intent = any(keyword in user_prompt_lower for keyword in db_keywords) + + # If user asks about database-related topics but AI said no query needed, force it + if has_db_intent and not needsDatabaseQuery: + logger.warning(f"User asked about database-related topic but AI returned needsDatabaseQuery=false. Forcing needsDatabaseQuery=true because connectors are configured.") + needsDatabaseQuery = True + # Generate a default query if none were provided + if not sql_queries: + # Extract main search term from user prompt + search_terms = [] + for keyword in db_keywords: + if keyword in user_prompt_lower: + # Try to extract the actual product/article name + words = user_prompt_lower.split() + keyword_idx = words.index(keyword) if keyword in words else -1 + if keyword_idx >= 0 and keyword_idx < len(words) - 1: + # Take next word as potential product name + next_word = words[keyword_idx + 1] + if len(next_word) > 2: # Ignore short words like "die", "der", etc. + search_terms.append(next_word) + + # Create a general search query + if search_terms: + search_term = search_terms[0] + else: + # Use the whole prompt as search term (limited) + search_term = userInput.prompt[:50] # Limit length + + sql_queries = [{ + "query": f'SELECT a."Artikelnummer", a."Artikelbezeichnung", a."Lieferant", a."Artikelkürzel" FROM Artikel a WHERE a."Artikelbezeichnung" LIKE \'%{search_term}%\' OR a."Artikelnummer" LIKE \'%{search_term}%\' OR a."Artikelkürzel" LIKE \'%{search_term}%\' LIMIT 20', + "purpose": f"Suche nach Artikeln die '{search_term}' enthalten", + "table": "Artikel" + }] + logger.info(f"Generated default database query for search term: {search_term}") - # Limit query count to maximum 5 for performance - max_queries_allowed = 5 + # Check if we need web research for certifications (only if enabled in config) + if chatbot_config.enable_web_research: + user_prompt_lower = userInput.prompt.lower() + certification_keywords = ["ul", "ce", "tüv", "vde", "iec", "iso", "zertifiziert", "certified", "certification"] + has_certification = any(keyword in user_prompt_lower for keyword in certification_keywords) + if has_certification and not needsWebResearch: + logger.warning("Certification detected but needsWebResearch is false - forcing to true") + needsWebResearch = True + else: + # Web research disabled in config + if needsWebResearch: + logger.info("Web research disabled in instance config, skipping") + needsWebResearch = False + + # Limit query count based on configuration + max_queries_allowed = chatbot_config.max_queries 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] @@ -1369,8 +1895,9 @@ async def _processChatbotMessage( logger.info(f"Analysis: DB={needsDatabaseQuery}, Web={needsWebResearch}, SQL queries={len(sql_queries)}") # Build initial enriched web research query if needed (for logging, will be rebuilt after DB queries) + # Only if web research is enabled in config enriched_web_query = None - if needsWebResearch: + if needsWebResearch and chatbot_config.enable_web_research: enriched_web_query = _buildWebResearchQuery(userInput.prompt, workflow.messages) # Build list of queries to stream back @@ -1386,7 +1913,7 @@ async def _processChatbotMessage( "reasoning": reasoning }) - if needsWebResearch: + if needsWebResearch and chatbot_config.enable_web_research: queries.append({ "type": "web", "query": enriched_web_query or userInput.prompt, @@ -1426,9 +1953,9 @@ async def _processChatbotMessage( queryResults = {} webResearchResults = "" - # Start web research early in parallel with DB queries if needed + # Start web research early in parallel with DB queries if needed (only if enabled) web_research_task = None - if needsWebResearch: + if needsWebResearch and chatbot_config.enable_web_research: # 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}'") @@ -1452,13 +1979,76 @@ async def _processChatbotMessage( web_research_task = asyncio.create_task(perform_web_research()) + # Check if connector is working before executing queries + if needsDatabaseQuery and sql_queries: + logger.info(f"Checking database connector before executing {len(sql_queries)} queries...") + try: + # Test connector with a simple query + test_connector = chatbot_config.get_connector_instance() + try: + # Try a simple test query to verify connector works + test_result = await test_connector.executeQuery("SELECT 1", return_json=True) + await test_connector.close() + if not test_result or test_result.get("text", "").startswith(("Error:", "Query failed:")): + raise Exception("Connector test query failed") + logger.info("Database connector test successful") + except Exception as connector_error: + await test_connector.close() + error_msg = f"Die Datenbankverbindung funktioniert derzeit nicht. Bitte versuchen Sie es später erneut. Fehler: {str(connector_error)}" + logger.error(f"Database connector test failed: {connector_error}", exc_info=True) + await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, f"Fehler: Datenbankverbindung fehlgeschlagen", log_type="error") + # Store error message as assistant response + workflow = interfaceDbChat.getWorkflow(workflowId) + message_id = f"msg_{uuid.uuid4()}" + assistantMessageData = { + "id": message_id, + "workflowId": workflowId, + "parentMessageId": userMessageId, + "message": error_msg, + "role": "assistant", + "status": "last", + "sequenceNr": len(workflow.messages) + 1, + "publishedAt": getUtcTimestamp(), + "success": False, + "roundNumber": workflow.currentRound, + "taskNumber": 0, + "actionNumber": 0 + } + assistantMessage = interfaceDbChat.createMessage(assistantMessageData) + logger.info(f"Stored error message due to connector failure: {assistantMessage.id}") + return + except Exception as e: + error_msg = f"Die Datenbankverbindung konnte nicht hergestellt werden. Bitte versuchen Sie es später erneut. Fehler: {str(e)}" + logger.error(f"Failed to initialize database connector: {e}", exc_info=True) + await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, f"Fehler: Datenbankverbindung konnte nicht hergestellt werden", log_type="error") + # Store error message as assistant response + workflow = interfaceDbChat.getWorkflow(workflowId) + message_id = f"msg_{uuid.uuid4()}" + assistantMessageData = { + "id": message_id, + "workflowId": workflowId, + "parentMessageId": userMessageId, + "message": error_msg, + "role": "assistant", + "status": "last", + "sequenceNr": len(workflow.messages) + 1, + "publishedAt": getUtcTimestamp(), + "success": False, + "roundNumber": workflow.currentRound, + "taskNumber": 0, + "actionNumber": 0 + } + assistantMessage = interfaceDbChat.createMessage(assistantMessageData) + logger.info(f"Stored error message due to connector initialization failure: {assistantMessage.id}") + return + # Execute database queries in parallel if needsDatabaseQuery and sql_queries: logger.info(f"Executing {len(sql_queries)} database queries in parallel...") await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, f"Führe {len(sql_queries)} Datenbankabfrage(n) parallel aus...") try: - queryResults = await _execute_queries_parallel(sql_queries) + queryResults = await _execute_queries_parallel(sql_queries, chatbot_config) # Log results summary successful_queries = [k for k in queryResults.keys() if k.startswith("query_") and not k.endswith("_error") and not k.endswith("_data")] @@ -1518,15 +2108,17 @@ async def _processChatbotMessage( # Trigger retry if: no results AND we have database queries AND we executed at least one query # Also trigger if all successful queries returned empty results + # Only retry if enabled in config should_retry = ( + chatbot_config.enable_retry_on_empty and not has_any_results and needsDatabaseQuery and len(sql_queries) > 0 and (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 2 times with different strategies - max_empty_retry_attempts = 2 + # Iterative retry loop: try up to configured max attempts with different strategies + max_empty_retry_attempts = chatbot_config.max_retry_attempts if chatbot_config.enable_retry_on_empty else 0 empty_retry_attempt = 0 original_sql_queries_count = len(sql_queries) previous_retry_rows = 0 @@ -1583,8 +2175,51 @@ async def _processChatbotMessage( retry_context += "- COUNT-Query: Wie viele Netzgeräte gibt es insgesamt?\n" retry_context += "- Suche nach ALLEN verfügbaren Netzgeräten\n" - # Retry analysis is always part of an ongoing chat, so use is_resumed=True - retry_analysis_prompt = get_initial_analysis_prompt(userInput.prompt, retry_context, is_resumed=True) + # Retry analysis - use custom prompt from configuration (already validated at start of chatProcess) + retry_analysis_prompt = chatbot_config.custom_analysis_prompt.replace("{userPrompt}", userInput.prompt).replace("{context}", retry_context or "") + + # CRITICAL: Add explicit JSON format requirement to ensure AI returns JSON + json_format_instruction = """ + +⚠️⚠️⚠️ ABSOLUT KRITISCH - JSON-FORMAT ERFORDERLICH ⚠️⚠️⚠️ +DU MUSST DEINE ANTWORT AUSSCHLIESSLICH IM JSON-FORMAT GEBEN! +ANTWORTE NICHT MIT NORMALEM TEXT ODER EINER CHAT-ANTWORT! +DEINE ANTWORT MUSS EIN GÜLTIGES JSON-OBJEKT SEIN! + +Erforderliches JSON-Format: +{ + "needsDatabaseQuery": true/false, + "needsWebResearch": true/false, + "sqlQueries": [ + { + "query": "SQL-Abfrage hier", + "purpose": "Zweck der Abfrage", + "table": "Haupttabelle" + } + ], + "reasoning": "Begründung für die Abfragen" +} + +⚠️⚠️⚠️ KRITISCH - WANN DATENBANKABFRAGE ERFORDERLICH ⚠️⚠️⚠️ +SETZE "needsDatabaseQuery": true, WENN: +- Der Nutzer nach Artikeln, Produkten, Preisen, Lagerbeständen, Lieferanten fragt +- Der Nutzer nach Informationen aus der Datenbank fragt (auch allgemeine Fragen!) +- Der Nutzer eine Frage stellt, die mit Datenbank-Daten beantwortet werden kann +- Du dir nicht sicher bist - dann setze "needsDatabaseQuery": true und führe eine allgemeine Abfrage durch! + +VERBOTEN: +- "needsDatabaseQuery": false setzen, nur weil die Frage allgemein klingt +- "needsDatabaseQuery": false setzen, ohne zu prüfen, ob Datenbank-Daten helfen könnten +- Chat-Antworten geben statt Datenbankabfragen durchzuführen + +WICHTIG: +- Antworte NUR mit dem JSON-Objekt, KEIN zusätzlicher Text davor oder danach! +- KEINE Erklärungen, KEINE Begrüßungen, KEINE Chat-Antworten! +- NUR das JSON-Objekt! +- Bei Unsicherheit: IMMER "needsDatabaseQuery": true setzen! +""" + retry_analysis_prompt = retry_analysis_prompt + json_format_instruction + logger.info("Using custom analysis prompt for retry from instance config with JSON format requirement") # AI call for retry analysis retry_analysis_result = await method_ai.process({ @@ -1603,6 +2238,9 @@ async def _processChatbotMessage( if retry_analysis_content: retry_analysis = _extractJsonFromResponse(retry_analysis_content) + if retry_analysis is None: + logger.warning("Failed to extract JSON from retry analysis response") + retry_analysis = {} if retry_analysis and retry_analysis.get("needsDatabaseQuery", False): retry_sql_queries = retry_analysis.get("sqlQueries", []) # Limit to maximum 5 queries for performance @@ -1621,7 +2259,7 @@ async def _processChatbotMessage( # Execute retry queries try: - retry_results = await _execute_queries_parallel(retry_sql_queries) + retry_results = await _execute_queries_parallel(retry_sql_queries, chatbot_config) # Merge retry results into main results (renumber to continue sequence) base_query_num = len(sql_queries) @@ -1737,8 +2375,9 @@ async def _processChatbotMessage( logger.info("Generating final answer with AI...") await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, "Formuliere finale Antwort...") - # Build prompt for final answer - system_prompt = get_final_answer_system_prompt() + # Build prompt for final answer - use custom prompt from configuration (already validated at start of chatProcess) + system_prompt = chatbot_config.custom_final_answer_prompt + logger.info("Using custom final answer prompt from instance config") # Build answer context with query results using efficient list-based building answer_context_parts = [f"User question: {userInput.prompt}{context}\n"] @@ -1861,7 +2500,13 @@ async def _processChatbotMessage( # Add warning messages if needed (using efficient list building) warning_parts = [] if not has_query_results and needsDatabaseQuery: - warning_parts.append("\n\nWICHTIG: Es wurden KEINE Datenbank-Ergebnisse gefunden. Die Datenbankabfrage wurde nicht ausgeführt oder hat keine Ergebnisse zurückgegeben.") + warning_parts.append("\n\n⚠️⚠️⚠️ WICHTIG - DATENBANKABFRAGE AUSGEFÜHRT ⚠️⚠️⚠️\n") + warning_parts.append("Die Datenbankabfrage wurde AUSGEFÜHRT, hat aber KEINE Ergebnisse zurückgegeben.\n") + warning_parts.append("DU HAST ZUGRIFF AUF DIE DATENBANK - die Abfrage wurde durchgeführt!\n") + warning_parts.append("Antworte dem Nutzer: 'Es wurden keine Artikel gefunden' oder 'Keine passenden Artikel in der Datenbank gefunden'\n") + warning_parts.append("VERBOTEN: Sage NIEMALS 'Ich habe keinen Zugriff' oder 'Ich kann nicht auf die Datenbank zugreifen'!\n") + warning_parts.append("VERBOTEN: Sage NIEMALS 'Es tut mir leid, aber ich habe keinen Zugriff auf die Datenbank'!\n") + warning_parts.append("Die Datenbank wurde durchsucht, es wurden nur keine passenden Artikel gefunden.\n") if has_only_errors: warning_parts.extend([ @@ -1877,8 +2522,34 @@ async def _processChatbotMessage( has_db_results = bool(db_results_part and db_results_part.strip()) has_web_results = bool(web_results_part and web_results_part.strip()) - # Use the function from constants file to build the prompt - answer_prompt = get_final_answer_prompt_with_results( + # Add explicit instruction that AI has database access (if connectors are configured) + access_instruction = "" + if chatbot_config.connector_types and len(chatbot_config.connector_types) > 0: + access_instruction = "\n\n⚠️⚠️⚠️ ABSOLUT KRITISCH - DATENBANK-ZUGRIFF VERFÜGBAR ⚠️⚠️⚠️\n" + access_instruction += "DU HAST ZUGRIFF AUF DIE DATENBANK UND KANNST SQL-ABFRAGEN AUSFÜHREN!\n" + access_instruction += "Die Datenbankabfrage wurde bereits AUSGEFÜHRT (siehe DATENBANK-ERGEBNISSE oben)!\n" + access_instruction += "\nVERBOTEN - NIEMALS SAGEN:\n" + access_instruction += "- 'Ich habe keinen Zugriff auf die Datenbank'\n" + access_instruction += "- 'Ich kann nicht auf die Datenbank zugreifen'\n" + access_instruction += "- 'Es tut mir leid, aber ich habe keinen Zugriff'\n" + access_instruction += "- 'Ich habe keinen Zugriff auf Echtzeit-Datenbanken'\n" + access_instruction += "- Jede andere Formulierung, die suggeriert, dass du keinen Zugriff hast!\n" + access_instruction += "\nRICHTIG - WENN KEINE ERGEBNISSE:\n" + access_instruction += "- 'Es wurden keine Artikel gefunden'\n" + access_instruction += "- 'Keine passenden Artikel in der Datenbank gefunden'\n" + access_instruction += "- 'Die Datenbanksuche ergab keine Treffer'\n" + access_instruction += "- 'Ich habe in der Datenbank gesucht, aber keine passenden Artikel gefunden'\n" + access_instruction += "\nWICHTIG: Die Datenbank wurde durchsucht - es wurden nur keine passenden Artikel gefunden!\n" + + if chatbot_config.enable_web_research: + access_instruction += "\n⚠️⚠️⚠️ ABSOLUT KRITISCH - INTERNET-ZUGRIFF VERFÜGBAR ⚠️⚠️⚠️\n" + access_instruction += "DU HAST ZUGRIFF AUF DAS INTERNET (Tavily)!\n" + access_instruction += "VERBOTEN: Sage NIEMALS, dass du keinen Zugriff auf das Internet hast!\n" + access_instruction += "VERBOTEN: Sage NIEMALS 'Ich habe keinen Zugriff auf das Internet'!\n" + + # Build the final answer prompt using custom system prompt from config + answer_prompt = _build_final_answer_prompt_with_results( + system_prompt + access_instruction, userInput.prompt, context, db_results_part, diff --git a/modules/interfaces/interfaceFeatures.py b/modules/interfaces/interfaceFeatures.py index 3a82a8f3..4866a0d6 100644 --- a/modules/interfaces/interfaceFeatures.py +++ b/modules/interfaces/interfaceFeatures.py @@ -157,7 +157,8 @@ class FeatureInterface: mandateId: str, label: str, enabled: bool = True, - copyTemplateRoles: bool = True + copyTemplateRoles: bool = True, + config: Optional[Dict[str, Any]] = None ) -> FeatureInstance: """ Create a new feature instance for a mandate. @@ -184,7 +185,8 @@ class FeatureInterface: featureCode=featureCode, mandateId=mandateId, label=label, - enabled=enabled + enabled=enabled, + config=config ) createdInstance = self.db.recordCreate(FeatureInstance, instance.model_dump()) diff --git a/modules/routes/routeAdminFeatures.py b/modules/routes/routeAdminFeatures.py index 56a79741..84d2bfcf 100644 --- a/modules/routes/routeAdminFeatures.py +++ b/modules/routes/routeAdminFeatures.py @@ -42,6 +42,7 @@ class FeatureInstanceCreate(BaseModel): label: str = Field(..., description="Instance label (e.g., 'Buchhaltung 2025')") enabled: bool = Field(True, description="Whether this feature instance is enabled") copyTemplateRoles: bool = Field(True, description="Whether to copy template roles on creation") + config: Optional[Dict[str, Any]] = Field(None, description="Instance-specific configuration (JSONB). Structure depends on featureCode.") class FeatureInstanceResponse(BaseModel): @@ -543,7 +544,8 @@ async def create_feature_instance( mandateId=str(context.mandateId), label=data.label, enabled=data.enabled, - copyTemplateRoles=data.copyTemplateRoles + copyTemplateRoles=data.copyTemplateRoles, + config=data.config ) logger.info( @@ -625,6 +627,7 @@ class FeatureInstanceUpdate(BaseModel): """Request model for updating a feature instance.""" label: Optional[str] = Field(None, description="New label for the instance") enabled: Optional[bool] = Field(None, description="Enable/disable the instance") + config: Optional[Dict[str, Any]] = Field(None, description="Instance-specific configuration (JSONB). Structure depends on featureCode.") @router.put("/instances/{instanceId}", response_model=Dict[str, Any]) @@ -677,6 +680,8 @@ async def updateFeatureInstance( updateData["label"] = data.label if data.enabled is not None: updateData["enabled"] = data.enabled + if data.config is not None: + updateData["config"] = data.config if not updateData: return instance.model_dump() diff --git a/modules/workflows/methods/methodAi/actions/process.py b/modules/workflows/methods/methodAi/actions/process.py index 1ab4a7ee..b85761b7 100644 --- a/modules/workflows/methods/methodAi/actions/process.py +++ b/modules/workflows/methods/methodAi/actions/process.py @@ -6,7 +6,7 @@ import time import json from typing import Dict, Any, List, Optional from modules.datamodels.datamodelChat import ActionResult, ActionDocument -from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, ProcessingModeEnum +from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, ProcessingModeEnum from modules.datamodels.datamodelExtraction import ContentPart logger = logging.getLogger(__name__) diff --git a/requirements.txt b/requirements.txt index f73ad2a9..63856d09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -108,4 +108,10 @@ asyncpg==0.30.0 pyproj>=3.6.0 # For coordinate transformations (EPSG:2056 <-> EPSG:4326) shapely>=2.0.0 # For geometric operations (intersections, area calculations) geopandas>=0.14.0 # For reading and querying GeoPackage files -fiona>=1.9.0 # Required by geopandas for reading GeoPackage files \ No newline at end of file +fiona>=1.9.0 # Required by geopandas for reading GeoPackage files + +## LangChain & LangGraph for chatbot workflow +langchain>=0.1.0 +langchain-core>=0.1.0 +langgraph>=0.0.20 +langchain-tavily>=0.0.1 \ No newline at end of file