isolate features

This commit is contained in:
ValueOn AG 2026-01-22 17:00:29 +01:00
parent 04ba89a0e8
commit 362080791a
195 changed files with 4966 additions and 1461 deletions

68
app.py
View file

@ -19,8 +19,9 @@ from datetime import datetime
from modules.shared.configuration import APP_CONFIG
from modules.shared.eventManagement import eventManager
from modules.features import featuresLifecycle as featuresLifecycle
from modules.workflows.automation import subAutomationSchedule
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.features.featureRegistry import loadFeatureMainModules
class DailyRotatingFileHandler(RotatingFileHandler):
"""
@ -282,18 +283,27 @@ instanceLabel = APP_CONFIG.get("APP_ENV_LABEL")
async def lifespan(app: FastAPI):
logger.info("Application is starting up")
# Initialize AI connectors once at startup to avoid per-request discovery
from modules.aicore.aicoreModelRegistry import modelRegistry
modelRegistry.ensureConnectorsRegistered()
# Get event user for feature lifecycle (system-level user for background operations)
rootInterface = getRootInterface()
eventUser = rootInterface.getUserByUsername("event")
if not eventUser:
logger.error("Could not get event user - some features may not start properly")
# --- Init Feature Containers (Plug&Play) ---
try:
mainModules = loadFeatureMainModules()
for featureName, module in mainModules.items():
if hasattr(module, "onStart"):
try:
await module.onStart(eventUser)
logger.info(f"Feature '{featureName}' started")
except Exception as e:
logger.error(f"Feature '{featureName}' failed to start: {e}")
except Exception as e:
logger.warning(f"Could not initialize feature containers: {e}")
# --- Init Managers ---
await featuresLifecycle.start(eventUser)
await subAutomationSchedule.start(eventUser) # Automation scheduler
eventManager.start()
# Register audit log cleanup scheduler
@ -304,7 +314,21 @@ async def lifespan(app: FastAPI):
# --- Stop Managers ---
eventManager.stop()
await featuresLifecycle.stop(eventUser)
await subAutomationSchedule.stop(eventUser) # Automation scheduler
# --- Stop Feature Containers (Plug&Play) ---
try:
mainModules = loadFeatureMainModules()
for featureName, module in mainModules.items():
if hasattr(module, "onStop"):
try:
await module.onStop(eventUser)
logger.info(f"Feature '{featureName}' stopped")
except Exception as e:
logger.error(f"Feature '{featureName}' failed to stop: {e}")
except Exception as e:
logger.warning(f"Could not shutdown feature containers: {e}")
logger.info("Application has been shut down")
@ -412,9 +436,6 @@ app.include_router(userRouter)
from modules.routes.routeDataFiles import router as fileRouter
app.include_router(fileRouter)
from modules.routes.routeFeatureNeutralization import router as neutralizationRouter
app.include_router(neutralizationRouter)
from modules.routes.routeDataPrompts import router as promptRouter
app.include_router(promptRouter)
@ -424,12 +445,6 @@ app.include_router(connectionsRouter)
from modules.routes.routeDataWorkflows import router as dataWorkflowsRouter
app.include_router(dataWorkflowsRouter)
from modules.routes.routeFeatureChatDynamic import router as chatPlaygroundRouter
app.include_router(chatPlaygroundRouter)
from modules.routes.routeFeatureRealEstate import router as realEstateRouter
app.include_router(realEstateRouter)
from modules.routes.routeSecurityLocal import router as localRouter
app.include_router(localRouter)
@ -448,27 +463,15 @@ app.include_router(adminSecurityRouter)
from modules.routes.routeSharepoint import router as sharepointRouter
app.include_router(sharepointRouter)
from modules.routes.routeDataAutomation import router as automationRouter
app.include_router(automationRouter)
from modules.routes.routeAdminAutomationEvents import router as adminAutomationEventsRouter
app.include_router(adminAutomationEventsRouter)
from modules.routes.routeAdminRbacRules import router as rbacAdminRulesRouter
app.include_router(rbacAdminRulesRouter)
from modules.routes.routeOptions import router as optionsRouter
app.include_router(optionsRouter)
from modules.routes.routeMessaging import router as messagingRouter
app.include_router(messagingRouter)
from modules.routes.routeFeatureChatbot import router as chatbotRouter
app.include_router(chatbotRouter)
from modules.routes.routeFeatureTrustee import router as trusteeRouter
app.include_router(trusteeRouter)
# Phase 8: New Feature Routes
from modules.routes.routeAdminFeatures import router as featuresAdminRouter
app.include_router(featuresAdminRouter)
@ -481,3 +484,12 @@ app.include_router(rbacAdminExportRouter)
from modules.routes.routeGdpr import router as gdprRouter
app.include_router(gdprRouter)
# ============================================================================
# PLUG&PLAY FEATURE ROUTERS
# Dynamically load routers from feature containers in modules/features/
# ============================================================================
from modules.features.featureRegistry import loadFeatureRouters
featureLoadResults = loadFeatureRouters(app)
logger.info(f"Feature router load results: {featureLoadResults}")

View file

@ -12,12 +12,6 @@ from modules.shared.jsonUtils import extractJsonString, tryParseJson, repairBrok
# Import DocumentReferenceList at runtime (needed for ActionDefinition)
from modules.datamodels.datamodelDocref import DocumentReferenceList
# Forward references for circular imports (use string annotations)
if TYPE_CHECKING:
from modules.datamodels.datamodelChat import ChatDocument, ActionResult
from modules.datamodels.datamodelExtraction import ExtractionOptions
class ActionDefinition(BaseModel):
"""Action definition with selection and parameters from planning phase"""

View file

@ -10,7 +10,7 @@ import importlib
import os
from typing import Dict, List, Optional, Any
from modules.datamodels.datamodelAi import AiModel
from modules.aicore.aicoreBase import BaseConnectorAi
from .aicoreBase import BaseConnectorAi
from modules.datamodels.datamodelUam import User
from modules.security.rbacHelpers import checkResourceAccess
from modules.security.rbac import RbacClass

View file

@ -6,7 +6,7 @@ import os
from typing import Dict, Any, List
from fastapi import HTTPException
from modules.shared.configuration import APP_CONFIG
from modules.aicore.aicoreBase import BaseConnectorAi
from .aicoreBase import BaseConnectorAi
from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse, createOperationTypeRatings
# Configure logger

View file

@ -2,7 +2,7 @@
# All rights reserved.
import logging
from typing import List
from modules.aicore.aicoreBase import BaseConnectorAi
from .aicoreBase import BaseConnectorAi
from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse, createOperationTypeRatings
# Configure logger

View file

@ -5,7 +5,7 @@ import httpx
from typing import List
from fastapi import HTTPException
from modules.shared.configuration import APP_CONFIG
from modules.aicore.aicoreBase import BaseConnectorAi
from .aicoreBase import BaseConnectorAi
from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse, createOperationTypeRatings
# Configure logger

View file

@ -5,7 +5,7 @@ import httpx
from typing import List
from fastapi import HTTPException
from modules.shared.configuration import APP_CONFIG
from modules.aicore.aicoreBase import BaseConnectorAi
from .aicoreBase import BaseConnectorAi
from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse, createOperationTypeRatings, AiCallPromptWebSearch, AiCallPromptWebCrawl
from modules.datamodels.datamodelTools import CountryCodes

View file

@ -10,7 +10,7 @@ from dataclasses import dataclass
from typing import Optional, List, Dict
from tavily import AsyncTavilyClient
from modules.shared.configuration import APP_CONFIG
from modules.aicore.aicoreBase import BaseConnectorAi
from .aicoreBase import BaseConnectorAi
from modules.datamodels.datamodelAi import AiModel, PriorityEnum, ProcessingModeEnum, OperationTypeEnum, AiModelCall, AiModelResponse, createOperationTypeRatings, AiCallPromptWebSearch, AiCallPromptWebCrawl
from modules.datamodels.datamodelTools import CountryCodes

View file

@ -16,7 +16,7 @@ from modules.security.rbac import RbacClass
from modules.datamodels.datamodelRbac import AccessRuleContext
from modules.datamodels.datamodelUam import AccessLevel
from modules.datamodels.datamodelChat import (
from .datamodelFeatureAiChat import (
ChatDocument,
ChatStat,
ChatLog,
@ -1095,26 +1095,6 @@ class ChatObjects:
actionName=createdMessage.get("actionName")
)
# Emit message event for streaming (if event manager is available)
try:
from modules.features.chatbot.eventManager import get_event_manager
event_manager = get_event_manager()
message_timestamp = parseTimestamp(chat_message.publishedAt, default=getUtcTimestamp())
# Emit message event in exact chatData format: {type, createdAt, item}
asyncio.create_task(event_manager.emit_event(
context_id=workflowId,
event_type="chatdata",
data={
"type": "message",
"createdAt": message_timestamp,
"item": chat_message.dict()
},
event_category="chat"
))
except Exception as e:
# Event manager not available or error - continue without emitting
logger.debug(f"Could not emit message event: {e}")
# Debug: Store message and documents for debugging - only if debug enabled
storeDebugMessageAndDocuments(chat_message, self.currentUser)
@ -1481,29 +1461,6 @@ class ChatObjects:
# Create log in normalized table
createdLog = self.db.recordCreate(ChatLog, log_model)
# Emit log event for streaming (only for chatbot workflows)
# Only emit events for chatbot workflows, not for automation or dynamic workflows
if workflow.workflowMode == WorkflowModeEnum.WORKFLOW_CHATBOT:
try:
from modules.features.chatbot.eventManager import get_event_manager
event_manager = get_event_manager()
log_timestamp = parseTimestamp(createdLog.get("timestamp"), default=getUtcTimestamp())
# Emit log event in exact chatData format: {type, createdAt, item}
asyncio.create_task(event_manager.emit_event(
workflowId,
"chatdata",
"New log",
"log",
{
"type": "log",
"createdAt": log_timestamp,
"item": ChatLog(**createdLog).model_dump()
}
))
except Exception as e:
# Event manager not available or error - continue without emitting
logger.debug(f"Could not emit log event: {e}")
# Return validated ChatLog instance
return ChatLog(**createdLog)
@ -1888,14 +1845,6 @@ class ChatObjects:
if not self.checkRbacPermission(AutomationDefinition, "delete", automationId):
raise PermissionError(f"No permission to delete automation {automationId}")
# Remove event if exists
if existing.get("eventId"):
from modules.shared.eventManagement import eventManager
try:
eventManager.remove(existing["eventId"])
except Exception as e:
logger.warning(f"Error removing event {existing['eventId']}: {str(e)}")
# Delete automation from database
self.db.recordDelete(AutomationDefinition, automationId)

View file

@ -0,0 +1,166 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
AIChat Feature Container - Main Module.
Handles feature initialization and RBAC catalog registration.
AIChat is the dynamic chat workflow feature that handles:
- AI-powered document processing
- Dynamic workflow execution
- Automation definitions
"""
import logging
from typing import Dict, List, Any
logger = logging.getLogger(__name__)
# Feature metadata
FEATURE_CODE = "chatworkflow"
FEATURE_LABEL = {"en": "Chat Workflow", "de": "Chat-Workflow", "fr": "Workflow de Chat"}
FEATURE_ICON = "mdi-message-cog"
# UI Objects for RBAC catalog
UI_OBJECTS = [
{
"objectKey": "ui.feature.aichat.workflows",
"label": {"en": "Workflows", "de": "Workflows", "fr": "Workflows"},
"meta": {"area": "workflows"}
},
{
"objectKey": "ui.feature.aichat.automations",
"label": {"en": "Automations", "de": "Automatisierungen", "fr": "Automatisations"},
"meta": {"area": "automations"}
},
{
"objectKey": "ui.feature.aichat.logs",
"label": {"en": "Logs", "de": "Logs", "fr": "Journaux"},
"meta": {"area": "logs"}
},
]
# Resource Objects for RBAC catalog
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.aichat.workflow.start",
"label": {"en": "Start Workflow", "de": "Workflow starten", "fr": "Démarrer workflow"},
"meta": {"endpoint": "/api/chat/playground/start", "method": "POST"}
},
{
"objectKey": "resource.feature.aichat.workflow.stop",
"label": {"en": "Stop Workflow", "de": "Workflow stoppen", "fr": "Arrêter workflow"},
"meta": {"endpoint": "/api/chat/playground/stop/{workflowId}", "method": "POST"}
},
{
"objectKey": "resource.feature.aichat.workflow.delete",
"label": {"en": "Delete Workflow", "de": "Workflow löschen", "fr": "Supprimer workflow"},
"meta": {"endpoint": "/api/chat/playground/workflow/{workflowId}", "method": "DELETE"}
},
]
# Template roles for this feature
TEMPLATE_ROLES = [
{
"roleLabel": "workflow-admin",
"description": {
"en": "Workflow Administrator - Full access to workflow configuration and execution",
"de": "Workflow-Administrator - Vollzugriff auf Workflow-Konfiguration und Ausführung",
"fr": "Administrateur workflow - Accès complet à la configuration et exécution"
}
},
{
"roleLabel": "workflow-editor",
"description": {
"en": "Workflow Editor - Create and modify workflows",
"de": "Workflow-Editor - Workflows erstellen und bearbeiten",
"fr": "Éditeur workflow - Créer et modifier les workflows"
}
},
{
"roleLabel": "workflow-viewer",
"description": {
"en": "Workflow Viewer - View workflows and execution results",
"de": "Workflow-Betrachter - Workflows und Ausführungsergebnisse einsehen",
"fr": "Visualiseur workflow - Consulter les workflows et résultats"
}
},
]
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")
)
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
async def onStart(eventUser) -> None:
"""
Called when the feature container starts.
Initializes AI connectors for model registry.
"""
try:
from .aicore.aicoreModelRegistry import modelRegistry
modelRegistry.ensureConnectorsRegistered()
logger.info(f"Feature '{FEATURE_CODE}' started - AI connectors initialized")
except Exception as e:
logger.error(f"Feature '{FEATURE_CODE}' failed to initialize AI connectors: {e}")
async def onStop(eventUser) -> None:
"""Called when the feature container stops."""
logger.info(f"Feature '{FEATURE_CODE}' stopped")

View file

@ -13,13 +13,13 @@ from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Reques
from modules.auth import limiter, getRequestContext, RequestContext
# Import interfaces
import modules.interfaces.interfaceDbChat as interfaceDbChat
from . import interfaceFeatureAiChat as interfaceDbChat
# Import models
from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum
from .datamodelFeatureAiChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum
# Import workflow control functions
from modules.features.workflow import chatStart, chatStop
from modules.workflows.automation import chatStart, chatStop
# Configure logger
logger = logging.getLogger(__name__)

View file

@ -6,8 +6,8 @@ import re
import time
import base64
from typing import Dict, Any, List, Optional, Tuple
from modules.datamodels.datamodelChat import PromptPlaceholder, ChatDocument
from modules.services.serviceExtraction.mainServiceExtraction import ExtractionService
from modules.features.aichat.datamodelFeatureAiChat import PromptPlaceholder, ChatDocument
from modules.features.aichat.serviceExtraction.mainServiceExtraction import ExtractionService
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
from modules.datamodels.datamodelExtraction import ContentPart, DocumentIntent
from modules.datamodels.datamodelWorkflow import AiResponse, AiResponseMetadata, DocumentData
@ -16,7 +16,7 @@ from modules.interfaces.interfaceAiObjects import AiObjects
from modules.shared.jsonUtils import (
parseJsonWithModel
)
from modules.services.serviceAi.subJsonResponseHandling import JsonResponseHandler
from .subJsonResponseHandling import JsonResponseHandler
from modules.datamodels.datamodelAi import JsonAccumulationState
logger = logging.getLogger(__name__)
@ -49,12 +49,12 @@ class AiService:
self.extractionService = ExtractionService(self.services)
# Initialize new submodules
from modules.services.serviceAi.subResponseParsing import ResponseParser
from modules.services.serviceAi.subDocumentIntents import DocumentIntentAnalyzer
from modules.services.serviceAi.subContentExtraction import ContentExtractor
from modules.services.serviceAi.subStructureGeneration import StructureGenerator
from modules.services.serviceAi.subStructureFilling import StructureFiller
from modules.services.serviceAi.subAiCallLooping import AiCallLooper
from .subResponseParsing import ResponseParser
from .subDocumentIntents import DocumentIntentAnalyzer
from .subContentExtraction import ContentExtractor
from .subStructureGeneration import StructureGenerator
from .subStructureFilling import StructureFiller
from .subAiCallLooping import AiCallLooper
if not hasattr(self, 'responseParser'):
logger.info("Initializing ResponseParser...")
@ -329,7 +329,7 @@ Respond with ONLY a JSON object in this exact format:
parentOperationId: Optional[str]
) -> AiResponse:
"""Handle IMAGE_GENERATE operation type using image generation path."""
from modules.services.serviceGeneration.paths.imagePath import ImageGenerationPath
from modules.features.aichat.serviceGeneration.paths.imagePath import ImageGenerationPath
imagePath = ImageGenerationPath(self.services)
@ -514,7 +514,7 @@ Respond with ONLY a JSON object in this exact format:
)
try:
from modules.services.serviceGeneration.mainServiceGeneration import GenerationService
from modules.features.aichat.serviceGeneration.mainServiceGeneration import GenerationService
generationService = GenerationService(self.services)
@ -829,7 +829,7 @@ Respond with ONLY a JSON object in this exact format:
parentOperationId: Optional[str]
) -> AiResponse:
"""Handle code generation using code generation path."""
from modules.services.serviceGeneration.paths.codePath import CodeGenerationPath
from modules.features.aichat.serviceGeneration.paths.codePath import CodeGenerationPath
codePath = CodeGenerationPath(self.services)
return await codePath.generateCode(
@ -852,7 +852,7 @@ Respond with ONLY a JSON object in this exact format:
parentOperationId: Optional[str]
) -> AiResponse:
"""Handle document generation using document generation path."""
from modules.services.serviceGeneration.paths.documentPath import DocumentGenerationPath
from modules.features.aichat.serviceGeneration.paths.documentPath import DocumentGenerationPath
# Set compression options for document generation
options.compressPrompt = False

View file

@ -53,8 +53,8 @@ from modules.datamodels.datamodelAi import (
AiCallRequest, AiCallOptions
)
from modules.datamodels.datamodelExtraction import ContentPart
from modules.services.serviceAi.subJsonResponseHandling import JsonResponseHandler
from modules.services.serviceAi.subLoopingUseCases import LoopingUseCaseRegistry
from .subJsonResponseHandling import JsonResponseHandler
from .subLoopingUseCases import LoopingUseCaseRegistry
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
from modules.shared.jsonContinuation import getContexts
from modules.shared.jsonUtils import buildContinuationContext, extractJsonString, tryParseJson

View file

@ -14,7 +14,7 @@ import logging
import base64
from typing import Dict, Any, List, Optional
from modules.datamodels.datamodelChat import ChatDocument
from modules.features.aichat.datamodelFeatureAiChat import ChatDocument
from modules.datamodels.datamodelExtraction import ContentPart, DocumentIntent
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped

View file

@ -12,7 +12,7 @@ import json
import logging
from typing import Dict, Any, List, Optional
from modules.datamodels.datamodelChat import ChatDocument
from modules.features.aichat.datamodelFeatureAiChat import ChatDocument
from modules.datamodels.datamodelExtraction import DocumentIntent
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped

View file

@ -1346,7 +1346,7 @@ class JsonResponseHandler:
# Use new modular merger
try:
from modules.services.serviceAi.subJsonMerger import ModularJsonMerger
from .subJsonMerger import ModularJsonMerger
result, hasOverlap = ModularJsonMerger.merge(accumulated, newFragment)
# IMPORTANT: ModularJsonMerger returns unclosed JSON if overlap found (with incomplete element at end)
# If no overlap, returns closed JSON (iterations should stop)

View file

@ -15,7 +15,7 @@ import logging
from typing import Dict, Any, List, Optional, Tuple
from modules.shared.jsonUtils import extractJsonString, repairBrokenJson, extractSectionsFromDocument
from modules.services.serviceAi.subJsonResponseHandling import JsonResponseHandler
from .subJsonResponseHandling import JsonResponseHandler
from modules.datamodels.datamodelAi import JsonAccumulationState
logger = logging.getLogger(__name__)

View file

@ -2531,7 +2531,7 @@ CRITICAL:
List of accepted section content types (e.g., ["table", "code_block"])
"""
try:
from modules.services.serviceGeneration.renderers.registry import getRenderer
from modules.features.aichat.serviceGeneration.renderers.registry import getRenderer
# Get renderer for this format
renderer = getRenderer(outputFormat, self.services)

View file

@ -231,7 +231,7 @@ CRITICAL:
raise ValueError("Structure has no documents - cannot generate without documents")
# Import renderer registry for format validation (existing infrastructure)
from modules.services.serviceGeneration.renderers.registry import getRenderer
from modules.features.aichat.serviceGeneration.renderers.registry import getRenderer
# Validate and fix each document
for doc in documents:

View file

@ -11,10 +11,10 @@ import json
from .subRegistry import ExtractorRegistry, ChunkerRegistry
from .subPipeline import runExtraction
from modules.datamodels.datamodelExtraction import ContentExtracted, ContentPart, MergeStrategy, ExtractionOptions, PartResult, DocumentIntent
from modules.datamodels.datamodelChat import ChatDocument
from modules.features.aichat.datamodelFeatureAiChat import ChatDocument
from modules.datamodels.datamodelAi import AiCallResponse, AiCallRequest, AiCallOptions, OperationTypeEnum, AiModelCall
from modules.aicore.aicoreModelRegistry import modelRegistry
from modules.aicore.aicoreModelSelector import modelSelector
from modules.features.aichat.aicore.aicoreModelRegistry import modelRegistry
from modules.features.aichat.aicore.aicoreModelSelector import modelSelector
from modules.shared.jsonUtils import stripCodeFences

View file

@ -13,7 +13,7 @@ from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, Operati
# Type hint for renderer parameter
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from modules.services.serviceGeneration.renderers.documentRendererBaseTemplate import BaseRenderer
from modules.features.aichat.serviceGeneration.renderers.documentRendererBaseTemplate import BaseRenderer
_RendererLike = BaseRenderer
else:
_RendererLike = Any

View file

@ -71,7 +71,7 @@ class ExtractorRegistry:
module_name = file_path.stem
try:
# Import the module
module = importlib.import_module(f".{module_name}", package="modules.services.serviceExtraction.extractors")
module = importlib.import_module(f".{module_name}", package="modules.features.aichat.serviceExtraction.extractors")
# Find all extractor classes in the module
for attr_name in dir(module):

View file

@ -6,8 +6,8 @@ import base64
import traceback
from typing import Any, Dict, List, Optional, Callable
from modules.datamodels.datamodelDocument import RenderedDocument
from modules.datamodels.datamodelChat import ChatDocument
from modules.services.serviceGeneration.subDocumentUtility import (
from modules.features.aichat.datamodelFeatureAiChat import ChatDocument
from modules.features.aichat.serviceGeneration.subDocumentUtility import (
getFileExtension,
getMimeTypeFromExtension,
detectMimeTypeFromContent,
@ -414,7 +414,7 @@ class GenerationService:
continue
# Check output style classification (code/document/image/etc.) from renderer
from modules.services.serviceGeneration.renderers.registry import getOutputStyle
from modules.features.aichat.serviceGeneration.renderers.registry import getOutputStyle
outputStyle = getOutputStyle(docFormat)
if outputStyle:
logger.debug(f"Document {doc.get('id', docIndex)} format '{docFormat}' classified as '{outputStyle}' style")
@ -471,8 +471,8 @@ class GenerationService:
Complete document structure with populated elements ready for rendering
"""
try:
from modules.services.serviceGeneration.subStructureGenerator import StructureGenerator
from modules.services.serviceGeneration.subContentGenerator import ContentGenerator
from modules.features.aichat.serviceGeneration.subStructureGenerator import StructureGenerator
from modules.features.aichat.serviceGeneration.subContentGenerator import ContentGenerator
# Phase 1: Generate structure skeleton
if progressCallback:
@ -537,7 +537,7 @@ class GenerationService:
aiService=None
) -> str:
"""Get adaptive extraction prompt."""
from modules.services.serviceExtraction.subPromptBuilderExtraction import buildExtractionPrompt
from modules.features.aichat.serviceExtraction.subPromptBuilderExtraction import buildExtractionPrompt
return await buildExtractionPrompt(
outputFormat=outputFormat,
userPrompt=userPrompt,

View file

@ -920,7 +920,7 @@ CRITICAL:
def _getCodeRenderer(self, fileType: str):
"""Get code renderer for file type."""
from modules.services.serviceGeneration.renderers.registry import getRenderer
from modules.features.aichat.serviceGeneration.renderers.registry import getRenderer
# Map file types to renderer formats
formatMap = {

View file

@ -12,7 +12,7 @@ import base64
import re
import traceback
from typing import Dict, Any, Optional, List, Callable
from modules.services.serviceGeneration.subContentIntegrator import ContentIntegrator
from modules.features.aichat.serviceGeneration.subContentIntegrator import ContentIntegrator
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
logger = logging.getLogger(__name__)

View file

@ -0,0 +1,148 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Automation 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 = "automation"
FEATURE_LABEL = {"en": "Automation", "de": "Automatisierung", "fr": "Automatisation"}
FEATURE_ICON = "mdi-cog-clockwise"
# UI Objects for RBAC catalog
UI_OBJECTS = [
{
"objectKey": "ui.feature.automation.definitions",
"label": {"en": "Automation Definitions", "de": "Automatisierungs-Definitionen", "fr": "Définitions d'automatisation"},
"meta": {"area": "definitions"}
},
{
"objectKey": "ui.feature.automation.templates",
"label": {"en": "Templates", "de": "Vorlagen", "fr": "Modèles"},
"meta": {"area": "templates"}
},
{
"objectKey": "ui.feature.automation.logs",
"label": {"en": "Execution Logs", "de": "Ausführungsprotokolle", "fr": "Journaux d'exécution"},
"meta": {"area": "logs"}
},
]
# Resource Objects for RBAC catalog
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.automation.create",
"label": {"en": "Create Automation", "de": "Automatisierung erstellen", "fr": "Créer automatisation"},
"meta": {"endpoint": "/api/automations", "method": "POST"}
},
{
"objectKey": "resource.feature.automation.update",
"label": {"en": "Update Automation", "de": "Automatisierung aktualisieren", "fr": "Modifier automatisation"},
"meta": {"endpoint": "/api/automations/{automationId}", "method": "PUT"}
},
{
"objectKey": "resource.feature.automation.delete",
"label": {"en": "Delete Automation", "de": "Automatisierung löschen", "fr": "Supprimer automatisation"},
"meta": {"endpoint": "/api/automations/{automationId}", "method": "DELETE"}
},
{
"objectKey": "resource.feature.automation.execute",
"label": {"en": "Execute Automation", "de": "Automatisierung ausführen", "fr": "Exécuter automatisation"},
"meta": {"endpoint": "/api/automations/{automationId}/execute", "method": "POST"}
},
]
# Template roles for this feature
TEMPLATE_ROLES = [
{
"roleLabel": "automation-admin",
"description": {
"en": "Automation Administrator - Full access to automation configuration and execution",
"de": "Automatisierungs-Administrator - Vollzugriff auf Automatisierungs-Konfiguration und Ausführung",
"fr": "Administrateur automatisation - Accès complet à la configuration et exécution"
}
},
{
"roleLabel": "automation-editor",
"description": {
"en": "Automation Editor - Create and modify automations",
"de": "Automatisierungs-Editor - Automatisierungen erstellen und bearbeiten",
"fr": "Éditeur automatisation - Créer et modifier les automatisations"
}
},
{
"roleLabel": "automation-viewer",
"description": {
"en": "Automation Viewer - View automations and execution results",
"de": "Automatisierungs-Betrachter - Automatisierungen und Ausführungsergebnisse einsehen",
"fr": "Visualiseur automatisation - Consulter les automatisations et résultats"
}
},
]
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")
)
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

View file

@ -13,13 +13,13 @@ import logging
import json
# Import interfaces and models
from modules.interfaces.interfaceDbChat import getInterface as getChatInterface
from modules.features.aichat.interfaceFeatureAiChat import getInterface as getChatInterface
from modules.auth import getCurrentUser, limiter
from modules.datamodels.datamodelChat import AutomationDefinition, ChatWorkflow
from modules.features.aichat.datamodelFeatureAiChat import AutomationDefinition, ChatWorkflow
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
from modules.shared.attributeUtils import getModelAttributeDefinitions
from modules.features.workflow import executeAutomation
from modules.features.workflow.subAutomationTemplates import getAutomationTemplates
from modules.workflows.automation import executeAutomation
from .subAutomationTemplates import getAutomationTemplates
# Configure logger
logger = logging.getLogger(__name__)

View file

@ -16,7 +16,7 @@ from modules.security.rbac import RbacClass
from modules.datamodels.datamodelRbac import AccessRuleContext
from modules.datamodels.datamodelUam import AccessLevel
from modules.datamodels.datamodelChat import (
from .datamodelFeatureChatbot import (
ChatDocument,
ChatStat,
ChatLog,

View file

@ -4,16 +4,120 @@
Simple chatbot feature - basic implementation.
User input is processed by AI to create list of needed queries.
Those queries get streamed back.
This module also handles feature initialization and RBAC catalog registration.
"""
import logging
# Feature metadata for RBAC catalog
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.conversations",
"label": {"en": "Conversations", "de": "Konversationen", "fr": "Conversations"},
"meta": {"area": "conversations"}
},
{
"objectKey": "ui.feature.chatbot.settings",
"label": {"en": "Settings", "de": "Einstellungen", "fr": "Paramètres"},
"meta": {"area": "settings"}
},
]
# Resource Objects for RBAC catalog
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.chatbot.start",
"label": {"en": "Start Chatbot", "de": "Chatbot starten", "fr": "Démarrer chatbot"},
"meta": {"endpoint": "/api/chatbot/start/stream", "method": "POST"}
},
{
"objectKey": "resource.feature.chatbot.stop",
"label": {"en": "Stop Chatbot", "de": "Chatbot stoppen", "fr": "Arrêter chatbot"},
"meta": {"endpoint": "/api/chatbot/stop/{workflowId}", "method": "POST"}
},
]
# Template roles for this feature
TEMPLATE_ROLES = [
{
"roleLabel": "chatbot-admin",
"description": {
"en": "Chatbot Administrator - Full access to chatbot settings and all conversations",
"de": "Chatbot-Administrator - Vollzugriff auf Chatbot-Einstellungen und alle Konversationen",
"fr": "Administrateur chatbot - Accès complet aux paramètres et conversations"
}
},
{
"roleLabel": "chatbot-user",
"description": {
"en": "Chatbot User - Use chatbot and view own conversations",
"de": "Chatbot-Benutzer - Chatbot nutzen und eigene Konversationen einsehen",
"fr": "Utilisateur chatbot - Utiliser le chatbot et consulter ses conversations"
}
},
]
def getFeatureDefinition():
"""Return the feature definition for registration."""
return {
"code": FEATURE_CODE,
"label": FEATURE_LABEL,
"icon": FEATURE_ICON
}
def getUiObjects():
"""Return UI objects for RBAC catalog registration."""
return UI_OBJECTS
def getResourceObjects():
"""Return resource objects for RBAC catalog registration."""
return RESOURCE_OBJECTS
def getTemplateRoles():
"""Return template roles for this feature."""
return TEMPLATE_ROLES
def registerFeature(catalogService) -> bool:
"""Register this feature's RBAC objects in the catalog."""
try:
for uiObj in UI_OBJECTS:
catalogService.registerUiObject(
featureCode=FEATURE_CODE,
objectKey=uiObj["objectKey"],
label=uiObj["label"],
meta=uiObj.get("meta")
)
for resObj in RESOURCE_OBJECTS:
catalogService.registerResourceObject(
featureCode=FEATURE_CODE,
objectKey=resObj["objectKey"],
label=resObj["label"],
meta=resObj.get("meta")
)
return True
except Exception as e:
logging.getLogger(__name__).error(f"Failed to register feature '{FEATURE_CODE}': {e}")
return False
import json
import uuid
import asyncio
import re
from typing import Optional, Dict, Any, List
from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum, ChatLog, ChatDocument
from modules.features.aichat.datamodelFeatureAiChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum, ChatLog, ChatDocument
from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, ProcessingModeEnum
from modules.datamodels.datamodelDocref import DocumentReferenceList, DocumentItemReference
@ -335,7 +439,7 @@ async def _emit_log_and_event(
# Emit event directly for streaming (using correct signature)
if created_log and event_manager:
try:
from modules.datamodels.datamodelChat import ChatLog
from modules.features.aichat.datamodelFeatureAiChat import ChatLog
# Convert to dict if it's a Pydantic model
if hasattr(created_log, "model_dump"):
log_dict = created_log.model_dump()

View file

@ -18,19 +18,19 @@ from modules.shared.timeUtils import parseTimestamp
from modules.auth import limiter, getRequestContext, RequestContext
# Import interfaces
import modules.interfaces.interfaceDbChat as interfaceDbChat
from . import interfaceFeatureChatbot as interfaceDbChat
from modules.interfaces.interfaceRbac import getRecordsetWithRBAC
# Import models
from modules.datamodels.datamodelChat import ChatWorkflow, UserInputRequest, WorkflowModeEnum
from .datamodelFeatureChatbot import ChatWorkflow, UserInputRequest, WorkflowModeEnum
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse
# Import chatbot feature
from modules.features.chatbot import chatProcess
from modules.features.chatbot.eventManager import get_event_manager
from . import chatProcess
from .eventManager import get_event_manager
# Import workflow control functions
from modules.features.workflow import chatStop
from modules.workflows.automation import chatStop
# Configure logger
logger = logging.getLogger(__name__)

View file

@ -1,237 +0,0 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Dynamic Options API feature module.
Provides dynamic options for frontend select/multiselect fields.
"""
import logging
from typing import List, Dict, Any, Optional
from modules.datamodels.datamodelUam import User
logger = logging.getLogger(__name__)
# Standard role definitions (fallback if database is not available)
STANDARD_ROLES = [
{"value": "sysadmin", "label": {"en": "System Administrator", "fr": "Administrateur système"}},
{"value": "admin", "label": {"en": "Administrator", "fr": "Administrateur"}},
{"value": "user", "label": {"en": "User", "fr": "Utilisateur"}},
{"value": "viewer", "label": {"en": "Viewer", "fr": "Visualiseur"}},
]
# Authentication authority options
AUTH_AUTHORITY_OPTIONS = [
{"value": "local", "label": {"en": "Local", "fr": "Local"}},
{"value": "google", "label": {"en": "Google", "fr": "Google"}},
{"value": "msft", "label": {"en": "Microsoft", "fr": "Microsoft"}},
]
# Connection status options
# Note: Matches ConnectionStatus enum values (active, expired, revoked, pending)
# Plus "error" for error states (not in enum but used in UI)
CONNECTION_STATUS_OPTIONS = [
{"value": "active", "label": {"en": "Active", "fr": "Actif"}},
{"value": "expired", "label": {"en": "Expired", "fr": "Expiré"}},
{"value": "revoked", "label": {"en": "Revoked", "fr": "Révoqué"}},
{"value": "pending", "label": {"en": "Pending", "fr": "En attente"}},
{"value": "error", "label": {"en": "Error", "fr": "Erreur"}},
]
def getOptions(optionsName: str, services, currentUser: Optional[User] = None) -> List[Dict[str, Any]]:
"""
Get options for a given options name.
Args:
optionsName: Name of the options set to retrieve (e.g., "user.role", "user.connection")
services: Services instance for data access
currentUser: Optional current user for context-aware options
Returns:
List of option dictionaries with "value" and "label" keys
Raises:
ValueError: If optionsName is not recognized
"""
logger.debug(f"getOptions called with optionsName='{optionsName}' (repr: {repr(optionsName)})")
optionsNameLower = optionsName.lower()
logger.debug(f"optionsNameLower='{optionsNameLower}'")
if optionsNameLower == "user.role":
# Fetch roles from database
if currentUser:
try:
roles = services.interfaceDbApp.getAllRoles()
# Convert Role objects to options format
options = []
for role in roles:
# Use English description as label, fallback to roleLabel
# Handle TextMultilingual object
if hasattr(role.description, 'get_text'):
# TextMultilingual object
label = role.description.get_text('en')
elif isinstance(role.description, dict):
# Dict format (backward compatibility)
label = role.description.get("en", role.roleLabel)
else:
# Fallback to roleLabel
label = role.roleLabel
options.append({
"value": role.roleLabel,
"label": label
})
# If no roles in database, return standard roles as fallback
if options:
return options
except Exception as e:
logger.warning(f"Error fetching roles from database, using fallback: {e}")
# Fallback to standard roles if database fetch fails or no user context
return STANDARD_ROLES
elif optionsNameLower == "auth.authority":
return AUTH_AUTHORITY_OPTIONS
elif optionsNameLower == "connection.status":
return CONNECTION_STATUS_OPTIONS
elif optionsNameLower == "user.connection":
# Dynamic options: Get user connections from database
if not currentUser:
return []
try:
connections = services.interfaceDbApp.getUserConnections(currentUser.id)
return [
{
"value": conn.id,
"label": {
"en": f"{conn.authority.value} - {conn.externalUsername or conn.externalId}",
"fr": f"{conn.authority.value} - {conn.externalUsername or conn.externalId}"
}
}
for conn in connections
]
except Exception as e:
logger.error(f"Error fetching user connections for options: {e}")
return []
elif optionsNameLower in ("user", "users"):
# Dynamic options: Get all users for the current mandate
if not currentUser:
return []
try:
users = services.interfaceDbApp.getUsersByMandate(services.mandateId)
# Handle both list and PaginatedResult
if hasattr(users, 'items'):
userList = users.items
else:
userList = users
return [
{
"value": user.id,
"label": user.fullName or user.username or user.email or user.id
}
for user in userList
]
except Exception as e:
logger.error(f"Error fetching users for options: {e}")
return []
elif optionsNameLower in ("trusteeorganisation", "trustee.organisation"):
# Dynamic options: Get all trustee organisations
if not currentUser:
return []
try:
result = services.interfaceDbTrustee.getAllOrganisations()
# Handle PaginatedResult
items = result.items if hasattr(result, 'items') else result
return [
{
"value": org.get("id") if isinstance(org, dict) else org.id,
"label": org.get("label") if isinstance(org, dict) else org.label
}
for org in items
]
except Exception as e:
logger.error(f"Error fetching trustee organisations for options: {e}")
return []
elif optionsNameLower in ("trusteerole", "trustee.role"):
# Dynamic options: Get all trustee roles
if not currentUser:
return []
try:
result = services.interfaceDbTrustee.getAllRoles()
# Handle PaginatedResult
items = result.items if hasattr(result, 'items') else result
return [
{
"value": role.get("id") if isinstance(role, dict) else role.id,
# TrusteeRole uses 'desc' field, not 'label'
"label": role.get("desc", role.get("id")) if isinstance(role, dict) else getattr(role, "desc", role.id)
}
for role in items
]
except Exception as e:
logger.error(f"Error fetching trustee roles for options: {e}")
return []
elif optionsNameLower in ("trusteecontract", "trustee.contract"):
# Dynamic options: Get all trustee contracts
if not currentUser:
return []
try:
result = services.interfaceDbTrustee.getAllContracts()
# Handle PaginatedResult
items = result.items if hasattr(result, 'items') else result
return [
{
"value": contract.get("id") if isinstance(contract, dict) else contract.id,
"label": contract.get("label") if isinstance(contract, dict) else (contract.get("name") if isinstance(contract, dict) else getattr(contract, "label", getattr(contract, "name", contract.id)))
}
for contract in items
]
except Exception as e:
logger.error(f"Error fetching trustee contracts for options: {e}")
return []
else:
logger.error(f"Unknown options name: '{optionsName}' (lower: '{optionsNameLower}')")
raise ValueError(f"Unknown options name: {optionsName}")
def getAvailableOptionsNames() -> List[str]:
"""
Get list of all available options names.
Returns:
List of available options names
"""
return [
"user.role",
"auth.authority",
"connection.status",
"user.connection",
"User",
"TrusteeOrganisation",
"TrusteeRole",
"TrusteeContract",
]

View file

@ -0,0 +1,117 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Feature Registry for Plug&Play Feature Container Loading.
Dynamically discovers and loads feature containers from the features directory.
"""
import os
import glob
import importlib
import logging
from typing import List, Dict, Any
from fastapi import FastAPI
logger = logging.getLogger(__name__)
# Path to the features directory
FEATURES_DIR = os.path.dirname(os.path.abspath(__file__))
def discoverFeatureContainers() -> List[str]:
"""
Discover all feature container directories by filename pattern.
A valid feature container has a routeFeature*.py file.
"""
containers = []
pattern = os.path.join(FEATURES_DIR, "*", "routeFeature*.py")
for filepath in glob.glob(pattern):
featureDir = os.path.basename(os.path.dirname(filepath))
if featureDir not in containers and not featureDir.startswith("_"):
containers.append(featureDir)
return sorted(containers)
def loadFeatureRouters(app: FastAPI) -> Dict[str, Any]:
"""
Dynamically load and register routers from all discovered feature containers.
"""
results = {}
pattern = os.path.join(FEATURES_DIR, "*", "routeFeature*.py")
for filepath in glob.glob(pattern):
featureDir = os.path.basename(os.path.dirname(filepath))
routerFile = os.path.basename(filepath)[:-3] # Remove .py
if featureDir.startswith("_"):
continue
try:
modulePath = f"modules.features.{featureDir}.{routerFile}"
module = importlib.import_module(modulePath)
if hasattr(module, "router"):
app.include_router(module.router)
logger.info(f"Loaded router: {featureDir}")
results[featureDir] = {"status": "loaded", "module": modulePath}
else:
logger.warning(f"No 'router' in {modulePath}")
results[featureDir] = {"status": "no_router_object"}
except Exception as e:
logger.error(f"Failed to load router from {featureDir}: {e}")
results[featureDir] = {"status": "error", "error": str(e)}
return results
def loadFeatureMainModules() -> Dict[str, Any]:
"""
Dynamically load main modules from all discovered feature containers.
"""
mainModules = {}
pattern = os.path.join(FEATURES_DIR, "*", "main*.py")
for filepath in glob.glob(pattern):
filename = os.path.basename(filepath)
if filename == "__init__.py":
continue
featureDir = os.path.basename(os.path.dirname(filepath))
if featureDir.startswith("_"):
continue
mainFile = filename[:-3] # Remove .py
try:
modulePath = f"modules.features.{featureDir}.{mainFile}"
module = importlib.import_module(modulePath)
mainModules[featureDir] = module
logger.debug(f"Loaded main module: {featureDir}")
except Exception as e:
logger.error(f"Failed to load main module from {featureDir}: {e}")
return mainModules
def registerAllFeaturesInCatalog(catalogService) -> Dict[str, bool]:
"""
Register all features' RBAC objects in the catalog.
"""
mainModules = loadFeatureMainModules()
results = {}
for featureName, module in mainModules.items():
if hasattr(module, "registerFeature"):
try:
success = module.registerFeature(catalogService)
results[featureName] = success
if success:
logger.info(f"Registered RBAC objects: {featureName}")
except Exception as e:
logger.error(f"Error registering {featureName}: {e}")
results[featureName] = False
return results

View file

@ -1,62 +0,0 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
import logging
from modules.services import getInterface as getServices
logger = logging.getLogger(__name__)
async def start(eventUser) -> None:
""" Start feature triggers and background managers
Args:
eventUser: System-level event user for background operations (provided by app.py)
"""
# Feature Workflow (Automation)
if eventUser:
try:
from modules.features.workflow import syncAutomationEvents
from modules.shared.callbackRegistry import callbackRegistry
# Get services for event user (provides access to interfaces)
services = getServices(eventUser, None)
# Register callback for automation changes
async def onAutomationChanged(chatInterface):
"""Callback triggered when automations are created/updated/deleted."""
# Get services for event user to pass to syncAutomationEvents
eventServices = getServices(eventUser, None)
await syncAutomationEvents(eventServices, eventUser)
callbackRegistry.register('automation.changed', onAutomationChanged)
logger.info("Workflow: Registered change callback")
# Initial sync on startup - use services
await syncAutomationEvents(services, eventUser)
logger.info("Workflow: Events synced on startup")
except Exception as e:
logger.error(f"Workflow: Error setting up events on startup: {str(e)}")
# Don't fail startup if automation sync fails
# Feature ...
return True
async def stop(eventUser) -> None:
""" Stop feature triggers and background managers
Args:
eventUser: System-level event user (provided by app.py)
"""
# Feature Workflow (Automation)
# Callbacks will remain registered (acceptable for shutdown)
logger.info("Workflow: Callbacks remain registered (will be cleaned up on shutdown)")
# Feature ...
return True

View file

@ -6,7 +6,7 @@ from typing import Any, Dict, List, Optional
from urllib.parse import urlparse, unquote
from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelNeutralizer import DataNeutralizerAttributes, DataNeutraliserConfig
from .datamodelFeatureNeutralizer import DataNeutralizerAttributes, DataNeutraliserConfig
from modules.services import getInterface as getServices
logger = logging.getLogger(__name__)

View file

@ -0,0 +1,125 @@
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Neutralizer 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 = "neutralization"
FEATURE_LABEL = {"en": "Neutralization", "de": "Neutralisierung", "fr": "Neutralisation"}
FEATURE_ICON = "mdi-shield-check"
# UI Objects for RBAC catalog
UI_OBJECTS = [
{
"objectKey": "ui.feature.neutralizer.playground",
"label": {"en": "Playground", "de": "Spielwiese", "fr": "Bac à sable"},
"meta": {"area": "playground"}
},
{
"objectKey": "ui.feature.neutralizer.config",
"label": {"en": "Configuration", "de": "Konfiguration", "fr": "Configuration"},
"meta": {"area": "config"}
},
{
"objectKey": "ui.feature.neutralizer.attributes",
"label": {"en": "Attributes", "de": "Attribute", "fr": "Attributs"},
"meta": {"area": "attributes"}
},
]
# Resource Objects for RBAC catalog
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.neutralizer.process.text",
"label": {"en": "Process Text", "de": "Text verarbeiten", "fr": "Traiter texte"},
"meta": {"endpoint": "/api/neutralization/process/text", "method": "POST"}
},
{
"objectKey": "resource.feature.neutralizer.process.files",
"label": {"en": "Process Files", "de": "Dateien verarbeiten", "fr": "Traiter fichiers"},
"meta": {"endpoint": "/api/neutralization/process/files", "method": "POST"}
},
{
"objectKey": "resource.feature.neutralizer.config.update",
"label": {"en": "Update Config", "de": "Konfiguration aktualisieren", "fr": "Mettre à jour config"},
"meta": {"endpoint": "/api/neutralization/config", "method": "PUT"}
},
]
# Template roles for this feature
TEMPLATE_ROLES = [
{
"roleLabel": "neutralization-admin",
"description": {
"en": "Neutralization Administrator - Full access to neutralization settings and data",
"de": "Neutralisierungs-Administrator - Vollzugriff auf Neutralisierungs-Einstellungen und Daten",
"fr": "Administrateur neutralisation - Accès complet aux paramètres et données"
}
},
{
"roleLabel": "neutralization-analyst",
"description": {
"en": "Neutralization Analyst - Analyze and process neutralization data",
"de": "Neutralisierungs-Analyst - Neutralisierungsdaten analysieren und verarbeiten",
"fr": "Analyste neutralisation - Analyser et traiter les données de neutralisation"
}
},
]
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."""
try:
for uiObj in UI_OBJECTS:
catalogService.registerUiObject(
featureCode=FEATURE_CODE,
objectKey=uiObj["objectKey"],
label=uiObj["label"],
meta=uiObj.get("meta")
)
for resObj in RESOURCE_OBJECTS:
catalogService.registerResourceObject(
featureCode=FEATURE_CODE,
objectKey=resObj["objectKey"],
label=resObj["label"],
meta=resObj.get("meta")
)
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

View file

@ -8,8 +8,8 @@ import logging
from modules.auth import limiter, getRequestContext, RequestContext
# Import interfaces
from modules.datamodels.datamodelNeutralizer import DataNeutraliserConfig, DataNeutralizerAttributes
from modules.features.neutralizePlayground.mainNeutralizePlayground import NeutralizationPlayground
from .datamodelFeatureNeutralizer import DataNeutraliserConfig, DataNeutralizerAttributes
from .mainNeutralizePlayground import NeutralizationPlayground
# Configure logger
logger = logging.getLogger(__name__)

View file

@ -13,14 +13,14 @@ import re
import json
from typing import Dict, List, Any, Optional
from modules.datamodels.datamodelNeutralizer import DataNeutraliserConfig, DataNeutralizerAttributes
from modules.features.neutralizer.datamodelFeatureNeutralizer import DataNeutraliserConfig, DataNeutralizerAttributes
# Import all necessary classes and functions for neutralization
from modules.services.serviceNeutralization.subProcessCommon import CommonUtils, NeutralizationResult, NeutralizationAttribute
from modules.services.serviceNeutralization.subProcessText import TextProcessor, PlainText
from modules.services.serviceNeutralization.subProcessList import ListProcessor, TableData
from modules.services.serviceNeutralization.subProcessBinary import BinaryProcessor
from modules.services.serviceNeutralization.subPatterns import HeaderPatterns, DataPatterns, TextTablePatterns
from .subProcessCommon import CommonUtils, NeutralizationResult, NeutralizationAttribute
from .subProcessText import TextProcessor, PlainText
from .subProcessList import ListProcessor, TableData
from .subProcessBinary import BinaryProcessor
from .subPatterns import HeaderPatterns, DataPatterns, TextTablePatterns
logger = logging.getLogger(__name__)

View file

@ -8,7 +8,7 @@ Handles pattern matching and replacement for emails, phones, addresses, IDs and
import re
import uuid
from typing import Dict, List, Tuple, Any
from modules.services.serviceNeutralization.subPatterns import DataPatterns, findPatternsInText
from .subPatterns import DataPatterns, findPatternsInText
class StringParser:
"""Handles string parsing and replacement operations"""

Some files were not shown because too many files have changed in this diff Show more