gateway/modules/features/chatbot/chatbotConfig.py
2026-01-30 11:24:24 +01:00

231 lines
8.9 KiB
Python

# 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()