fix:mainchatbot.py created to ensure feature creation

This commit is contained in:
Ida Dittrich 2026-02-18 10:39:31 +01:00
parent ea99711ace
commit d8fb3bf821
7 changed files with 327 additions and 304 deletions

View file

@ -2,15 +2,10 @@
# All rights reserved.
"""
Configuration system for chatbot instances.
Supports loading from:
1. Database (FeatureInstance.config JSONB field) - primary method
2. JSON files from configs/ directory - fallback/legacy method
Loads configuration from the database (FeatureInstance.config JSONB field).
"""
import logging
import json
import warnings
from pathlib import Path
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, TYPE_CHECKING
@ -251,14 +246,8 @@ def load_chatbot_config_from_instance(instance: 'FeatureInstance') -> ChatbotCon
config_data = instance.config
if not config_data:
# No config in instance - try to load default from file as fallback
logger.warning(f"Instance {instance_id} has no config, loading default from file")
try:
return load_chatbot_config_from_file("default")
except FileNotFoundError:
# Create minimal default config
logger.warning(f"No default config file found, using minimal defaults")
config_data = {}
logger.warning(f"Instance {instance_id} has no config, using minimal defaults")
config_data = {}
# Create config from dictionary
config = ChatbotConfig.from_dict(config_data, config_id=instance_id)
@ -286,83 +275,6 @@ def load_chatbot_config_from_dict(config_data: Dict[str, Any], config_id: str =
return ChatbotConfig.from_dict(config_data, config_id=config_id)
def load_chatbot_config_from_file(config_id: str) -> ChatbotConfig:
"""
Load chatbot configuration from JSON file.
This is the legacy/fallback method for loading configuration.
Prefer load_chatbot_config_from_instance() for production use.
Args:
config_id: Configuration ID (e.g., "althaus", "default")
Returns:
ChatbotConfig instance
Raises:
FileNotFoundError: If config file not found
ValueError: If config file is invalid
"""
# Check cache first (by file ID)
cache_key = f"file_{config_id}"
if cache_key in _config_cache:
logger.debug(f"Returning cached config for file {config_id}")
return _config_cache[cache_key]
# Get path to configs directory
current_dir = Path(__file__).parent
configs_dir = current_dir / "configs"
config_file = configs_dir / f"{config_id}.json"
if not config_file.exists():
# Try default config if requested config not found
if config_id != "default":
logger.warning(f"Config file {config_id} not found, trying default")
return load_chatbot_config_from_file("default")
raise FileNotFoundError(f"Chatbot config file not found: {config_file}")
try:
with open(config_file, 'r', encoding='utf-8') as f:
data = json.load(f)
config = ChatbotConfig.from_dict(data, config_id=config_id)
# Cache the config
_config_cache[cache_key] = config
logger.info(f"Loaded chatbot config from file: {config_id} ({config.name})")
return config
except json.JSONDecodeError as e:
logger.error(f"Error parsing chatbot config JSON {config_file}: {e}")
raise ValueError(f"Invalid JSON in config file {config_file}: {e}")
except Exception as e:
logger.error(f"Error loading chatbot config {config_file}: {e}")
raise
def load_chatbot_config(config_id: str) -> ChatbotConfig:
"""
Load chatbot configuration from JSON file.
DEPRECATED: Use load_chatbot_config_from_instance() for database configs
or load_chatbot_config_from_file() for file-based configs.
Args:
config_id: Configuration ID (e.g., "althaus", "default")
Returns:
ChatbotConfig instance
"""
warnings.warn(
"load_chatbot_config() is deprecated. Use load_chatbot_config_from_instance() "
"for database configs or load_chatbot_config_from_file() for file-based configs.",
DeprecationWarning,
stacklevel=2
)
return load_chatbot_config_from_file(config_id)
def clear_config_cache(instance_id: Optional[str] = None):
"""
Clear the configuration cache.

File diff suppressed because one or more lines are too long

View file

@ -1,31 +0,0 @@
{
"id": "default",
"name": "Default Chatbot",
"systemPrompt": "You are a helpful assistant. You have access to SQL query tools and web search tools. Use them to help answer user questions.",
"database": {
"schema": {
"database": {
"path": "/data/database.db",
"type": "SQLite"
},
"tables": {},
"relationships": []
},
"connector": "preprocessor"
},
"tools": {
"sql": {
"enabled": true
},
"tavily": {
"enabled": false
},
"streaming": {
"enabled": true
}
},
"model": {
"operationType": "DATA_ANALYSE",
"processingMode": "DETAILED"
}
}

View file

@ -0,0 +1,294 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Chatbot Feature Container - Main Module.
Handles feature initialization and RBAC catalog registration.
"""
import logging
from typing import Dict, List, Any
logger = logging.getLogger(__name__)
# Feature metadata
FEATURE_CODE = "chatbot"
FEATURE_LABEL = {"en": "Chatbot", "de": "Chatbot", "fr": "Chatbot"}
FEATURE_ICON = "mdi-robot"
# UI Objects for RBAC catalog
UI_OBJECTS = [
{
"objectKey": "ui.feature.chatbot.chat",
"label": {"en": "Chat", "de": "Chat", "fr": "Chat"},
"meta": {"area": "chat"}
},
{
"objectKey": "ui.feature.chatbot.threads",
"label": {"en": "Threads", "de": "Threads", "fr": "Threads"},
"meta": {"area": "threads"}
},
]
# Resource Objects for RBAC catalog
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.chatbot.startStream",
"label": {"en": "Start Chat (Stream)", "de": "Chat starten (Stream)", "fr": "Démarrer chat (Stream)"},
"meta": {"endpoint": "/api/chatbot/{instanceId}/start/stream", "method": "POST"}
},
{
"objectKey": "resource.feature.chatbot.stop",
"label": {"en": "Stop Chat", "de": "Chat stoppen", "fr": "Arrêter chat"},
"meta": {"endpoint": "/api/chatbot/{instanceId}/stop/{workflowId}", "method": "POST"}
},
{
"objectKey": "resource.feature.chatbot.threads",
"label": {"en": "Get Threads", "de": "Threads abrufen", "fr": "Récupérer threads"},
"meta": {"endpoint": "/api/chatbot/{instanceId}/threads", "method": "GET"}
},
{
"objectKey": "resource.feature.chatbot.delete",
"label": {"en": "Delete Chat", "de": "Chat löschen", "fr": "Supprimer chat"},
"meta": {"endpoint": "/api/chatbot/{instanceId}/{workflowId}", "method": "DELETE"}
},
]
# Template roles for this feature
# Role names MUST follow convention: {featureCode}-{roleName}
TEMPLATE_ROLES = [
{
"roleLabel": "chatbot-viewer",
"description": {
"en": "Chatbot Viewer - View chat threads (read-only)",
"de": "Chatbot Betrachter - Chat-Threads ansehen (nur lesen)",
"fr": "Visualiseur Chatbot - Consulter les threads (lecture seule)"
},
"accessRules": [
# UI: only threads view, NO active chat
{"context": "UI", "item": "ui.feature.chatbot.threads", "view": True},
# RESOURCE: can list threads only
{"context": "RESOURCE", "item": "resource.feature.chatbot.threads", "view": True},
# DATA access (own records, read-only)
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
]
},
{
"roleLabel": "chatbot-user",
"description": {
"en": "Chatbot User - Use the chatbot and manage own threads",
"de": "Chatbot Benutzer - Chatbot nutzen und eigene Threads verwalten",
"fr": "Utilisateur Chatbot - Utiliser le chatbot et gérer ses threads"
},
"accessRules": [
# UI: full access to all views
{"context": "UI", "item": "ui.feature.chatbot.chat", "view": True},
{"context": "UI", "item": "ui.feature.chatbot.threads", "view": True},
# Resource access: can start/stop chats, view threads, delete own
{"context": "RESOURCE", "item": "resource.feature.chatbot.startStream", "view": True},
{"context": "RESOURCE", "item": "resource.feature.chatbot.stop", "view": True},
{"context": "RESOURCE", "item": "resource.feature.chatbot.threads", "view": True},
{"context": "RESOURCE", "item": "resource.feature.chatbot.delete", "view": True},
# DATA access (own records)
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "m"},
]
},
{
"roleLabel": "chatbot-admin",
"description": {
"en": "Chatbot Admin - Full access to all chatbot features",
"de": "Chatbot Admin - Vollzugriff auf alle Chatbot-Funktionen",
"fr": "Administrateur Chatbot - Accès complet à toutes les fonctions chatbot"
},
"accessRules": [
# Full UI access
{"context": "UI", "item": None, "view": True},
# Full resource access
{"context": "RESOURCE", "item": None, "view": True},
# Full DATA access
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
]
},
]
def getFeatureDefinition() -> Dict[str, Any]:
"""Return the feature definition for registration."""
return {
"code": FEATURE_CODE,
"label": FEATURE_LABEL,
"icon": FEATURE_ICON,
}
def getUiObjects() -> List[Dict[str, Any]]:
"""Return UI objects for RBAC catalog registration."""
return UI_OBJECTS
def getResourceObjects() -> List[Dict[str, Any]]:
"""Return resource objects for RBAC catalog registration."""
return RESOURCE_OBJECTS
def getTemplateRoles() -> List[Dict[str, Any]]:
"""Return template roles for this feature."""
return TEMPLATE_ROLES
def registerFeature(catalogService) -> bool:
"""
Register this feature's RBAC objects in the catalog.
Args:
catalogService: The RBAC catalog service instance
Returns:
True if registration was successful
"""
try:
# Register UI objects
for uiObj in UI_OBJECTS:
catalogService.registerUiObject(
featureCode=FEATURE_CODE,
objectKey=uiObj["objectKey"],
label=uiObj["label"],
meta=uiObj.get("meta")
)
# Register Resource objects
for resObj in RESOURCE_OBJECTS:
catalogService.registerResourceObject(
featureCode=FEATURE_CODE,
objectKey=resObj["objectKey"],
label=resObj["label"],
meta=resObj.get("meta")
)
# Sync template roles to database
_syncTemplateRolesToDb()
logger.info(f"Feature '{FEATURE_CODE}' registered {len(UI_OBJECTS)} UI objects and {len(RESOURCE_OBJECTS)} resource objects")
return True
except Exception as e:
logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}")
return False
def _syncTemplateRolesToDb() -> int:
"""
Sync template roles and their AccessRules to the database.
Creates global template roles (mandateId=None) if they don't exist.
Returns:
Number of roles created/updated
"""
try:
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
rootInterface = getRootInterface()
# Get existing template roles for this feature (Pydantic models)
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
# Filter to template roles (mandateId is None)
templateRoles = [r for r in existingRoles if r.mandateId is None]
existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles}
createdCount = 0
for roleTemplate in TEMPLATE_ROLES:
roleLabel = roleTemplate["roleLabel"]
if roleLabel in existingRoleLabels:
roleId = existingRoleLabels[roleLabel]
# Ensure AccessRules exist for this role
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
else:
# Create new template role
newRole = Role(
roleLabel=roleLabel,
description=roleTemplate.get("description", {}),
featureCode=FEATURE_CODE,
mandateId=None, # Global template
featureInstanceId=None,
isSystemRole=False
)
createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump())
roleId = createdRole.get("id")
# Create AccessRules for this role
_ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", []))
logger.info(f"Created template role '{roleLabel}' with ID {roleId}")
createdCount += 1
if createdCount > 0:
logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles")
return createdCount
except Exception as e:
logger.error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}")
return 0
def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int:
"""
Ensure AccessRules exist for a role based on templates.
Args:
rootInterface: Root interface instance
roleId: Role ID
ruleTemplates: List of rule templates
Returns:
Number of rules created
"""
from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext
# Get existing rules for this role (Pydantic models)
existingRules = rootInterface.getAccessRulesByRole(roleId)
# Create a set of existing rule signatures to avoid duplicates
# IMPORTANT: Use .value for enum comparison, not str() which gives "AccessRuleContext.DATA" in Python 3.11+
existingSignatures = set()
for rule in existingRules:
sig = (rule.context.value if rule.context else None, rule.item)
existingSignatures.add(sig)
createdCount = 0
for template in ruleTemplates:
context = template.get("context", "UI")
item = template.get("item")
sig = (context, item)
if sig in existingSignatures:
continue
# Map context string to enum
if context == "UI":
contextEnum = AccessRuleContext.UI
elif context == "DATA":
contextEnum = AccessRuleContext.DATA
elif context == "RESOURCE":
contextEnum = AccessRuleContext.RESOURCE
else:
contextEnum = context
newRule = AccessRule(
roleId=roleId,
context=contextEnum,
item=item,
view=template.get("view", False),
read=template.get("read"),
create=template.get("create"),
update=template.get("update"),
delete=template.get("delete"),
)
rootInterface.db.recordCreate(AccessRule, newRule.model_dump())
createdCount += 1
if createdCount > 0:
logger.debug(f"Created {createdCount} AccessRules for role {roleId}")
return createdCount

View file

@ -25,7 +25,6 @@ from modules.features.chatbot.bridges.ai import AICenterChatModel
from modules.features.chatbot.bridges.memory import DatabaseCheckpointer
from modules.features.chatbot.config import (
load_chatbot_config_from_instance,
load_chatbot_config_from_file,
ChatbotConfig
)
from modules.datamodels.datamodelAi import OperationTypeEnum, ProcessingModeEnum
@ -1086,36 +1085,40 @@ async def _bridge_chatbot_events(
async def _load_chatbot_config(featureInstanceId: Optional[str]) -> ChatbotConfig:
"""
Load chatbot configuration from FeatureInstance (database) or file fallback.
Load chatbot configuration from FeatureInstance (database).
Args:
featureInstanceId: Feature instance ID to load config from
Returns:
ChatbotConfig instance
Raises:
ValueError: If no featureInstanceId provided or instance not found
"""
if featureInstanceId:
try:
# Import here to avoid circular imports
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.interfaces.interfaceFeatures import getFeatureInterface
# Get feature instance from database
rootInterface = getRootInterface()
featureInterface = getFeatureInterface(rootInterface.db)
instance = featureInterface.getFeatureInstance(featureInstanceId)
if instance and instance.config:
logger.info(f"Loading chatbot config from FeatureInstance {featureInstanceId}")
return load_chatbot_config_from_instance(instance)
else:
logger.warning(f"FeatureInstance {featureInstanceId} has no config, using file fallback")
except Exception as e:
logger.error(f"Error loading config from FeatureInstance {featureInstanceId}: {e}")
if not featureInstanceId:
raise ValueError("featureInstanceId is required to load chatbot config")
# Fallback to file-based config (default)
logger.info("Using file-based chatbot config (default)")
return load_chatbot_config_from_file("default")
try:
# Import here to avoid circular imports
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.interfaces.interfaceFeatures import getFeatureInterface
# Get feature instance from database
rootInterface = getRootInterface()
featureInterface = getFeatureInterface(rootInterface.db)
instance = featureInterface.getFeatureInstance(featureInstanceId)
if not instance:
raise ValueError(f"FeatureInstance {featureInstanceId} not found")
logger.info(f"Loading chatbot config from FeatureInstance {featureInstanceId}")
return load_chatbot_config_from_instance(instance)
except ValueError:
raise
except Exception as e:
logger.error(f"Error loading config from FeatureInstance {featureInstanceId}: {e}")
raise
async def _processChatbotMessageLangGraph(

View file

@ -483,6 +483,7 @@ def _discoverAicoreProviderObjects() -> List[Dict[str, Any]]:
providerLabels = {
"anthropic": {"en": "Anthropic (Claude)", "de": "Anthropic (Claude)", "fr": "Anthropic (Claude)"},
"openai": {"en": "OpenAI (GPT)", "de": "OpenAI (GPT)", "fr": "OpenAI (GPT)"},
"mistral": {"en": "Mistral (Le Chat)", "de": "Mistral (Le Chat)", "fr": "Mistral (Le Chat)"},
"perplexity": {"en": "Perplexity", "de": "Perplexity", "fr": "Perplexity"},
"tavily": {"en": "Tavily (Web Search)", "de": "Tavily (Websuche)", "fr": "Tavily (Recherche Web)"},
"privatellm": {"en": "Private LLM", "de": "Private LLM", "fr": "LLM Privé"},

View file

@ -26,7 +26,7 @@ import pytest
from modules.features.chatbot.chatbot import Chatbot
from modules.features.chatbot.chatbotAIBridge import AICenterChatModel
from modules.features.chatbot.chatbotMemory import DatabaseCheckpointer
from modules.features.chatbot.chatbotConfig import load_chatbot_config
from modules.features.chatbot.config import load_chatbot_config_from_dict
from modules.features.chatbot.streamingHelper import ChatStreamingHelper
from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelAi import OperationTypeEnum, ProcessingModeEnum
@ -57,7 +57,7 @@ class TestChatbot:
async def test_chatbot_initialization(self, test_user, workflow_id):
"""Test that chatbot can be initialized correctly."""
# Load config
config = load_chatbot_config("althaus")
config = load_chatbot_config_from_dict({}, config_id="test")
# Create system prompt
from datetime import datetime
@ -162,7 +162,7 @@ class TestChatbot:
async def test_chatbot_should_continue_logic(self, test_user, workflow_id):
"""Test that should_continue logic works correctly (no infinite loops)."""
# Load config
config = load_chatbot_config("althaus")
config = load_chatbot_config_from_dict({}, config_id="test")
# Create system prompt
from datetime import datetime