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."""
|
"""Feature models: Feature, FeatureInstance."""
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Optional
|
from typing import Optional, Dict, Any
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from modules.shared.attributeUtils import registerModelLabels
|
from modules.shared.attributeUtils import registerModelLabels
|
||||||
from modules.datamodels.datamodelUtils import TextMultilingual
|
from modules.datamodels.datamodelUtils import TextMultilingual
|
||||||
|
|
@ -68,6 +68,11 @@ class FeatureInstance(BaseModel):
|
||||||
description="Whether this feature instance is enabled",
|
description="Whether this feature instance is enabled",
|
||||||
json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
|
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(
|
registerModelLabels(
|
||||||
|
|
@ -79,5 +84,6 @@ registerModelLabels(
|
||||||
"mandateId": {"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
|
"mandateId": {"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
|
||||||
"label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"},
|
"label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"},
|
||||||
"enabled": {"en": "Enabled", "de": "Aktiviert", "fr": "Activé"},
|
"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.services import getInterface as getServices
|
||||||
from modules.features.chatbot import interfaceFeatureChatbot
|
from modules.features.chatbot import interfaceFeatureChatbot
|
||||||
from modules.features.chatbot.eventManager import get_event_manager
|
from modules.features.chatbot.eventManager import get_event_manager
|
||||||
from modules.workflows.methods.methodAi.methodAi import MethodAi
|
from modules.features.chatbot.chatbotUtils import (
|
||||||
from modules.connectors.connectorPreprocessor import PreprocessorConnector
|
|
||||||
from modules.features.chatbot.chatbotConstants import (
|
|
||||||
get_initial_analysis_prompt,
|
|
||||||
generate_conversation_name,
|
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
|
import base64
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -460,6 +457,16 @@ async def chatProcess(
|
||||||
ChatWorkflow instance
|
ChatWorkflow instance
|
||||||
"""
|
"""
|
||||||
try:
|
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.)
|
# Get services normally (for other services like chat, ai, etc.)
|
||||||
services = getServices(currentUser, None, mandateId=mandateId)
|
services = getServices(currentUser, None, mandateId=mandateId)
|
||||||
|
|
||||||
|
|
@ -612,7 +619,8 @@ async def chatProcess(
|
||||||
services,
|
services,
|
||||||
workflow.id,
|
workflow.id,
|
||||||
userInput,
|
userInput,
|
||||||
userMessage.id
|
userMessage.id,
|
||||||
|
chatbot_config
|
||||||
))
|
))
|
||||||
|
|
||||||
# Reload workflow to include new message
|
# Reload workflow to include new message
|
||||||
|
|
@ -624,7 +632,7 @@ async def chatProcess(
|
||||||
raise
|
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.
|
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
|
- "query": SQL query string
|
||||||
- "purpose": Description of what the query retrieves
|
- "purpose": Description of what the query retrieves
|
||||||
- "table": Primary table name
|
- "table": Primary table name
|
||||||
|
chatbot_config: ChatbotConfig instance for connector selection
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary mapping query indices to results:
|
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_data", "query_2_data", etc.: Raw data arrays
|
||||||
- "query_1_error", "query_2_error", etc.: Error messages if query failed
|
- "query_1_error", "query_2_error", etc.: Error messages if query failed
|
||||||
"""
|
"""
|
||||||
# Create single connector instance to reuse across all queries
|
# Create connector instance based on configuration
|
||||||
connector = PreprocessorConnector()
|
connector = chatbot_config.get_connector_instance()
|
||||||
try:
|
try:
|
||||||
async def execute_single_query(idx: int, query_info: Dict[str, Any]):
|
async def execute_single_query(idx: int, query_info: Dict[str, Any]):
|
||||||
"""Execute a single query using shared connector."""
|
"""Execute a single query using shared connector."""
|
||||||
|
|
@ -775,6 +784,248 @@ async def _check_workflow_stopped(interfaceDbChat, workflowId: str) -> bool:
|
||||||
return False
|
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:
|
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.
|
Build enriched web research query by extracting product context from conversation history and current prompt.
|
||||||
|
|
@ -1248,11 +1499,12 @@ async def _processChatbotMessage(
|
||||||
services,
|
services,
|
||||||
workflowId: str,
|
workflowId: str,
|
||||||
userInput: UserInputRequest,
|
userInput: UserInputRequest,
|
||||||
userMessageId: str
|
userMessageId: str,
|
||||||
|
chatbot_config: ChatbotConfig
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Process chatbot message in background.
|
Process chatbot message using LangGraph workflow.
|
||||||
Analyzes user input and generates list of queries, then streams them back.
|
Uses LangGraph to handle the conversation flow with tools (SQL, Tavily, streaming).
|
||||||
"""
|
"""
|
||||||
event_manager = get_event_manager()
|
event_manager = get_event_manager()
|
||||||
|
|
||||||
|
|
@ -1278,39 +1530,177 @@ async def _processChatbotMessage(
|
||||||
logger.info(f"Workflow {workflowId} was stopped, aborting processing")
|
logger.info(f"Workflow {workflowId} was stopped, aborting processing")
|
||||||
return
|
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()
|
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...")
|
logger.info("Analyzing user input to generate queries...")
|
||||||
await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, "Analysiere Benutzeranfrage...")
|
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
|
# AI call for analysis
|
||||||
method_ai = MethodAi(services)
|
method_ai = MethodAi(services)
|
||||||
|
|
@ -1326,42 +1716,178 @@ async def _processChatbotMessage(
|
||||||
logger.info(f"Workflow {workflowId} was stopped during analysis, aborting processing")
|
logger.info(f"Workflow {workflowId} was stopped during analysis, aborting processing")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Extract content from ActionResult
|
# Retry logic for failed analysis (max 3 attempts)
|
||||||
|
max_analysis_retries = 3
|
||||||
|
analysis_retry_count = 0
|
||||||
|
analysis = None
|
||||||
analysis_content = None
|
analysis_content = None
|
||||||
if analysis_result.success and analysis_result.documents:
|
|
||||||
analysis_content = analysis_result.documents[0].documentData
|
|
||||||
if isinstance(analysis_content, bytes):
|
|
||||||
analysis_content = analysis_content.decode('utf-8')
|
|
||||||
|
|
||||||
if not analysis_content:
|
while analysis_retry_count < max_analysis_retries:
|
||||||
logger.warning("Analysis failed, using fallback")
|
# Extract content from ActionResult
|
||||||
analysis = {}
|
analysis_content = None
|
||||||
else:
|
if analysis_result.success and analysis_result.documents:
|
||||||
|
analysis_content = analysis_result.documents[0].documentData
|
||||||
|
if isinstance(analysis_content, bytes):
|
||||||
|
analysis_content = analysis_content.decode('utf-8')
|
||||||
|
|
||||||
|
# Validate analysis was successful
|
||||||
|
if not analysis_content:
|
||||||
|
analysis_retry_count += 1
|
||||||
|
if analysis_retry_count < max_analysis_retries:
|
||||||
|
logger.warning(f"Analysis failed (attempt {analysis_retry_count}/{max_analysis_retries}): No content returned from AI, retrying...")
|
||||||
|
await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, f"Analyse fehlgeschlagen, Versuch {analysis_retry_count}/{max_analysis_retries}...", log_type="warning")
|
||||||
|
# Retry analysis
|
||||||
|
analysis_result = await method_ai.process({
|
||||||
|
"aiPrompt": analysisPrompt,
|
||||||
|
"documentList": None,
|
||||||
|
"resultType": "json",
|
||||||
|
"simpleMode": True
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
error_msg = "Die Analyse Ihrer Anfrage ist nach mehreren Versuchen fehlgeschlagen. Bitte versuchen Sie es später erneut oder formulieren Sie Ihre Frage anders."
|
||||||
|
logger.error(f"Analysis failed after {max_analysis_retries} attempts: No content returned from AI")
|
||||||
|
await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, f"Fehler: Analyse nach {max_analysis_retries} Versuchen fehlgeschlagen", log_type="error")
|
||||||
|
# Store error message as assistant response
|
||||||
|
workflow = interfaceDbChat.getWorkflow(workflowId)
|
||||||
|
message_id = f"msg_{uuid.uuid4()}"
|
||||||
|
assistantMessageData = {
|
||||||
|
"id": message_id,
|
||||||
|
"workflowId": workflowId,
|
||||||
|
"parentMessageId": userMessageId,
|
||||||
|
"message": error_msg,
|
||||||
|
"role": "assistant",
|
||||||
|
"status": "last",
|
||||||
|
"sequenceNr": len(workflow.messages) + 1,
|
||||||
|
"publishedAt": getUtcTimestamp(),
|
||||||
|
"success": False,
|
||||||
|
"roundNumber": workflow.currentRound,
|
||||||
|
"taskNumber": 0,
|
||||||
|
"actionNumber": 0
|
||||||
|
}
|
||||||
|
assistantMessage = interfaceDbChat.createMessage(assistantMessageData)
|
||||||
|
logger.info(f"Stored error message due to failed analysis after {max_analysis_retries} attempts: {assistantMessage.id}")
|
||||||
|
return
|
||||||
|
|
||||||
analysis = _extractJsonFromResponse(analysis_content)
|
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
|
# Extract analysis results
|
||||||
needsDatabaseQuery = analysis.get("needsDatabaseQuery", False) if analysis else False
|
needsDatabaseQuery = analysis.get("needsDatabaseQuery", False) if analysis else False
|
||||||
needsWebResearch = analysis.get("needsWebResearch", 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
|
# 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 = [{
|
sql_queries = [{
|
||||||
"query": analysis.get("sqlQuery", ""),
|
"query": analysis.get("sqlQuery", ""),
|
||||||
"purpose": "Database query",
|
"purpose": "Database query",
|
||||||
"table": "Unknown"
|
"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
|
||||||
user_prompt_lower = userInput.prompt.lower()
|
# Override AI decision if it says "no database query" but connectors are available
|
||||||
certification_keywords = ["ul", "ce", "tüv", "vde", "iec", "iso", "zertifiziert", "certified", "certification"]
|
if chatbot_config.connector_types and len(chatbot_config.connector_types) > 0:
|
||||||
has_certification = any(keyword in user_prompt_lower for keyword in certification_keywords)
|
user_prompt_lower = userInput.prompt.lower()
|
||||||
if has_certification and not needsWebResearch:
|
# Keywords that indicate database query is needed
|
||||||
logger.warning("Certification detected but needsWebResearch is false - forcing to true")
|
db_keywords = [
|
||||||
needsWebResearch = True
|
"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)
|
||||||
|
|
||||||
# Limit query count to maximum 5 for performance
|
# If user asks about database-related topics but AI said no query needed, force it
|
||||||
max_queries_allowed = 5
|
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 based on configuration
|
||||||
|
max_queries_allowed = chatbot_config.max_queries
|
||||||
if needsDatabaseQuery and len(sql_queries) > max_queries_allowed:
|
if needsDatabaseQuery and len(sql_queries) > max_queries_allowed:
|
||||||
logger.info(f"Limiting queries from {len(sql_queries)} to {max_queries_allowed} for performance")
|
logger.info(f"Limiting queries from {len(sql_queries)} to {max_queries_allowed} for performance")
|
||||||
sql_queries = sql_queries[:max_queries_allowed]
|
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)}")
|
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)
|
# 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
|
enriched_web_query = None
|
||||||
if needsWebResearch:
|
if needsWebResearch and chatbot_config.enable_web_research:
|
||||||
enriched_web_query = _buildWebResearchQuery(userInput.prompt, workflow.messages)
|
enriched_web_query = _buildWebResearchQuery(userInput.prompt, workflow.messages)
|
||||||
|
|
||||||
# Build list of queries to stream back
|
# Build list of queries to stream back
|
||||||
|
|
@ -1386,7 +1913,7 @@ async def _processChatbotMessage(
|
||||||
"reasoning": reasoning
|
"reasoning": reasoning
|
||||||
})
|
})
|
||||||
|
|
||||||
if needsWebResearch:
|
if needsWebResearch and chatbot_config.enable_web_research:
|
||||||
queries.append({
|
queries.append({
|
||||||
"type": "web",
|
"type": "web",
|
||||||
"query": enriched_web_query or userInput.prompt,
|
"query": enriched_web_query or userInput.prompt,
|
||||||
|
|
@ -1426,9 +1953,9 @@ async def _processChatbotMessage(
|
||||||
queryResults = {}
|
queryResults = {}
|
||||||
webResearchResults = ""
|
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
|
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)
|
# Start with basic query (will enrich later with DB results if available)
|
||||||
basic_web_query = _buildWebResearchQuery(userInput.prompt, workflow.messages, None)
|
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}'")
|
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())
|
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
|
# Execute database queries in parallel
|
||||||
if needsDatabaseQuery and sql_queries:
|
if needsDatabaseQuery and sql_queries:
|
||||||
logger.info(f"Executing {len(sql_queries)} database queries in parallel...")
|
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...")
|
await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, f"Führe {len(sql_queries)} Datenbankabfrage(n) parallel aus...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
queryResults = await _execute_queries_parallel(sql_queries)
|
queryResults = await _execute_queries_parallel(sql_queries, chatbot_config)
|
||||||
|
|
||||||
# Log results summary
|
# 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")]
|
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
|
# 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
|
# Also trigger if all successful queries returned empty results
|
||||||
|
# Only retry if enabled in config
|
||||||
should_retry = (
|
should_retry = (
|
||||||
|
chatbot_config.enable_retry_on_empty and
|
||||||
not has_any_results and
|
not has_any_results and
|
||||||
needsDatabaseQuery and
|
needsDatabaseQuery and
|
||||||
len(sql_queries) > 0 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)
|
(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
|
# Iterative retry loop: try up to configured max attempts with different strategies
|
||||||
max_empty_retry_attempts = 2
|
max_empty_retry_attempts = chatbot_config.max_retry_attempts if chatbot_config.enable_retry_on_empty else 0
|
||||||
empty_retry_attempt = 0
|
empty_retry_attempt = 0
|
||||||
original_sql_queries_count = len(sql_queries)
|
original_sql_queries_count = len(sql_queries)
|
||||||
previous_retry_rows = 0
|
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 += "- COUNT-Query: Wie viele Netzgeräte gibt es insgesamt?\n"
|
||||||
retry_context += "- Suche nach ALLEN verfügbaren Netzgeräten\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 - use custom prompt from configuration (already validated at start of chatProcess)
|
||||||
retry_analysis_prompt = get_initial_analysis_prompt(userInput.prompt, retry_context, is_resumed=True)
|
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
|
# AI call for retry analysis
|
||||||
retry_analysis_result = await method_ai.process({
|
retry_analysis_result = await method_ai.process({
|
||||||
|
|
@ -1603,6 +2238,9 @@ async def _processChatbotMessage(
|
||||||
|
|
||||||
if retry_analysis_content:
|
if retry_analysis_content:
|
||||||
retry_analysis = _extractJsonFromResponse(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):
|
if retry_analysis and retry_analysis.get("needsDatabaseQuery", False):
|
||||||
retry_sql_queries = retry_analysis.get("sqlQueries", [])
|
retry_sql_queries = retry_analysis.get("sqlQueries", [])
|
||||||
# Limit to maximum 5 queries for performance
|
# Limit to maximum 5 queries for performance
|
||||||
|
|
@ -1621,7 +2259,7 @@ async def _processChatbotMessage(
|
||||||
|
|
||||||
# Execute retry queries
|
# Execute retry queries
|
||||||
try:
|
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)
|
# Merge retry results into main results (renumber to continue sequence)
|
||||||
base_query_num = len(sql_queries)
|
base_query_num = len(sql_queries)
|
||||||
|
|
@ -1737,8 +2375,9 @@ async def _processChatbotMessage(
|
||||||
logger.info("Generating final answer with AI...")
|
logger.info("Generating final answer with AI...")
|
||||||
await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, "Formuliere finale Antwort...")
|
await _emit_log_and_event(interfaceDbChat, workflowId, event_manager, "Formuliere finale Antwort...")
|
||||||
|
|
||||||
# Build prompt for final answer
|
# Build prompt for final answer - use custom prompt from configuration (already validated at start of chatProcess)
|
||||||
system_prompt = get_final_answer_system_prompt()
|
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
|
# Build answer context with query results using efficient list-based building
|
||||||
answer_context_parts = [f"User question: {userInput.prompt}{context}\n"]
|
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)
|
# Add warning messages if needed (using efficient list building)
|
||||||
warning_parts = []
|
warning_parts = []
|
||||||
if not has_query_results and needsDatabaseQuery:
|
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:
|
if has_only_errors:
|
||||||
warning_parts.extend([
|
warning_parts.extend([
|
||||||
|
|
@ -1877,8 +2522,34 @@ async def _processChatbotMessage(
|
||||||
has_db_results = bool(db_results_part and db_results_part.strip())
|
has_db_results = bool(db_results_part and db_results_part.strip())
|
||||||
has_web_results = bool(web_results_part and web_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
|
# Add explicit instruction that AI has database access (if connectors are configured)
|
||||||
answer_prompt = get_final_answer_prompt_with_results(
|
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,
|
userInput.prompt,
|
||||||
context,
|
context,
|
||||||
db_results_part,
|
db_results_part,
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,8 @@ class FeatureInterface:
|
||||||
mandateId: str,
|
mandateId: str,
|
||||||
label: str,
|
label: str,
|
||||||
enabled: bool = True,
|
enabled: bool = True,
|
||||||
copyTemplateRoles: bool = True
|
copyTemplateRoles: bool = True,
|
||||||
|
config: Optional[Dict[str, Any]] = None
|
||||||
) -> FeatureInstance:
|
) -> FeatureInstance:
|
||||||
"""
|
"""
|
||||||
Create a new feature instance for a mandate.
|
Create a new feature instance for a mandate.
|
||||||
|
|
@ -184,7 +185,8 @@ class FeatureInterface:
|
||||||
featureCode=featureCode,
|
featureCode=featureCode,
|
||||||
mandateId=mandateId,
|
mandateId=mandateId,
|
||||||
label=label,
|
label=label,
|
||||||
enabled=enabled
|
enabled=enabled,
|
||||||
|
config=config
|
||||||
)
|
)
|
||||||
createdInstance = self.db.recordCreate(FeatureInstance, instance.model_dump())
|
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')")
|
label: str = Field(..., description="Instance label (e.g., 'Buchhaltung 2025')")
|
||||||
enabled: bool = Field(True, description="Whether this feature instance is enabled")
|
enabled: bool = Field(True, description="Whether this feature instance is enabled")
|
||||||
copyTemplateRoles: bool = Field(True, description="Whether to copy template roles on creation")
|
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):
|
class FeatureInstanceResponse(BaseModel):
|
||||||
|
|
@ -543,7 +544,8 @@ async def create_feature_instance(
|
||||||
mandateId=str(context.mandateId),
|
mandateId=str(context.mandateId),
|
||||||
label=data.label,
|
label=data.label,
|
||||||
enabled=data.enabled,
|
enabled=data.enabled,
|
||||||
copyTemplateRoles=data.copyTemplateRoles
|
copyTemplateRoles=data.copyTemplateRoles,
|
||||||
|
config=data.config
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
@ -625,6 +627,7 @@ class FeatureInstanceUpdate(BaseModel):
|
||||||
"""Request model for updating a feature instance."""
|
"""Request model for updating a feature instance."""
|
||||||
label: Optional[str] = Field(None, description="New label for the instance")
|
label: Optional[str] = Field(None, description="New label for the instance")
|
||||||
enabled: Optional[bool] = Field(None, description="Enable/disable 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])
|
@router.put("/instances/{instanceId}", response_model=Dict[str, Any])
|
||||||
|
|
@ -677,6 +680,8 @@ async def updateFeatureInstance(
|
||||||
updateData["label"] = data.label
|
updateData["label"] = data.label
|
||||||
if data.enabled is not None:
|
if data.enabled is not None:
|
||||||
updateData["enabled"] = data.enabled
|
updateData["enabled"] = data.enabled
|
||||||
|
if data.config is not None:
|
||||||
|
updateData["config"] = data.config
|
||||||
|
|
||||||
if not updateData:
|
if not updateData:
|
||||||
return instance.model_dump()
|
return instance.model_dump()
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import time
|
||||||
import json
|
import json
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
from modules.datamodels.datamodelChat import ActionResult, ActionDocument
|
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
|
from modules.datamodels.datamodelExtraction import ContentPart
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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)
|
shapely>=2.0.0 # For geometric operations (intersections, area calculations)
|
||||||
geopandas>=0.14.0 # For reading and querying GeoPackage files
|
geopandas>=0.14.0 # For reading and querying GeoPackage files
|
||||||
fiona>=1.9.0 # Required by geopandas for reading 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