althause update
This commit is contained in:
parent
14ec8b7007
commit
e4662b19e2
12 changed files with 1846 additions and 1092 deletions
|
|
@ -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"},
|
||||
},
|
||||
)
|
||||
|
|
|
|||
170
modules/features/chatbot/aiCenterAdapter.py
Normal file
170
modules/features/chatbot/aiCenterAdapter.py
Normal file
|
|
@ -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
|
||||
231
modules/features/chatbot/chatbotConfig.py
Normal file
231
modules/features/chatbot/chatbotConfig.py
Normal file
|
|
@ -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()
|
||||
File diff suppressed because it is too large
Load diff
160
modules/features/chatbot/chatbotUtils.py
Normal file
160
modules/features/chatbot/chatbotUtils.py
Normal file
|
|
@ -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
|
||||
"""
|
||||
345
modules/features/chatbot/langgraphChatbot.py
Normal file
345
modules/features/chatbot/langgraphChatbot.py
Normal file
|
|
@ -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}"}
|
||||
166
modules/features/chatbot/langgraphTools.py
Normal file
166
modules/features/chatbot/langgraphTools.py
Normal file
|
|
@ -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]
|
||||
|
|
@ -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,6 +1716,13 @@ async def _processChatbotMessage(
|
|||
logger.info(f"Workflow {workflowId} was stopped during analysis, aborting processing")
|
||||
return
|
||||
|
||||
# Retry logic for failed analysis (max 3 attempts)
|
||||
max_analysis_retries = 3
|
||||
analysis_retry_count = 0
|
||||
analysis = None
|
||||
analysis_content = None
|
||||
|
||||
while analysis_retry_count < max_analysis_retries:
|
||||
# Extract content from ActionResult
|
||||
analysis_content = None
|
||||
if analysis_result.success and analysis_result.documents:
|
||||
|
|
@ -1333,35 +1730,164 @@ async def _processChatbotMessage(
|
|||
if isinstance(analysis_content, bytes):
|
||||
analysis_content = analysis_content.decode('utf-8')
|
||||
|
||||
# Validate analysis was successful
|
||||
if not analysis_content:
|
||||
logger.warning("Analysis failed, using fallback")
|
||||
analysis = {}
|
||||
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
|
||||
# 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}")
|
||||
|
||||
# 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 to maximum 5 for performance
|
||||
max_queries_allowed = 5
|
||||
# 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,
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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__)
|
||||
|
|
|
|||
|
|
@ -109,3 +109,9 @@ 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
|
||||
|
||||
## LangChain & LangGraph for chatbot workflow
|
||||
langchain>=0.1.0
|
||||
langchain-core>=0.1.0
|
||||
langgraph>=0.0.20
|
||||
langchain-tavily>=0.0.1
|
||||
Loading…
Reference in a new issue