wiki/z-archive/appdoc/doc_workflow_actions_rbac_concept_done.md

43 KiB

Workflow Actions RBAC Integration Concept

Übersicht

Dieses Dokument beschreibt das Konzept für die Umstrukturierung der Workflow Actions, um:

  1. RBAC-Integration zu ermöglichen (Schutz von Actions über RESOURCE-Context)
  2. Strukturierte Parameter-Definitionen statt Docstrings zu verwenden
  3. UI-Rendering-Typen für Parameter zu definieren
  4. Keine Duplikation von Parameter-Definitionen zu haben
  5. Plug-and-Play Funktionalität beizubehalten

WICHTIG:

  • Alle Actions MÜSSEN in _actions Dictionary definiert sein
  • Keine Backward Compatibility - Actions ohne _actions Definition sind nicht verfügbar
  • RBAC-Service ist REQUIRED (kein Fallback ohne RBAC)

Architektur-Konzept

Grundprinzip: Deklarative Action-Definition mit Refactored Structure

Nach dem Refactoring sind Actions in separaten Dateien in actions/ Ordnern organisiert. Die Action-Definitionen werden deklarativ in der Hauptklasse definiert und referenzieren die Execute-Funktionen aus den separaten Action-Dateien.

Neue Ordnerstruktur (nach Refactoring):

methodOutlook/
├── __init__.py
├── methodOutlook.py (Hauptklasse mit Action-Definitionen)
├── helpers/
│   ├── connection.py
│   ├── emailProcessing.py
│   └── folderManagement.py
└── actions/
    ├── readEmails.py (Execute-Funktion)
    ├── searchEmails.py
    └── ...

Action-Definition in Hauptklasse (methodOutlook/methodOutlook.py):

from modules.datamodels.datamodelWorkflowActions import WorkflowActionDefinition, WorkflowActionParameter
from modules.shared.frontendTypes import FrontendType
from .actions.readEmails import readEmails  # Execute-Funktion importieren

class MethodOutlook(MethodBase):
    def __init__(self, services):
        super().__init__(services)
        self.name = "outlook"
        self.description = "Handle Microsoft Outlook email operations"
        
        # Initialize helper modules
        self.connection = ConnectionHelper(self)
        self.emailProcessing = EmailProcessingHelper(self)
        self.folderManagement = FolderManagementHelper(self)
        
        # Actions werden deklarativ definiert
        # Execute-Funktionen werden aus separaten Action-Dateien importiert
        self._actions = {
            "readEmails": WorkflowActionDefinition(
                actionId="outlook.readEmails",  # Für RBAC: RESOURCE context
                description="Read emails and metadata from a mailbox folder",
                parameters={
                    "connectionReference": WorkflowActionParameter(
                        name="connectionReference",
                        type="str",
                        frontendType=FrontendType.USER_CONNECTION,
                        required=True,
                        description="Microsoft connection label"
                    ),
                    "folder": WorkflowActionParameter(
                        name="folder",
                        type="str",
                        frontendType=FrontendType.SELECT,
                        frontendOptions="outlook.folder",
                        required=False,
                        default="Inbox",
                        description="Folder to read from"
                    ),
                    "limit": WorkflowActionParameter(
                        name="limit",
                        type="int",
                        frontendType=FrontendType.NUMBER,
                        required=False,
                        default=10,
                        description="Maximum items to return",
                        validation={"min": 1, "max": 1000}
                    ),
                    "filter": WorkflowActionParameter(
                        name="filter",
                        type="str",
                        frontendType=FrontendType.TEXT,
                        required=False,
                        description="Sender, query operators, or subject text"
                    ),
                    "outputMimeType": WorkflowActionParameter(
                        name="outputMimeType",
                        type="str",
                        frontendType=FrontendType.SELECT,
                        frontendOptions=["application/json", "text/plain", "text/csv"],
                        required=False,
                        default="application/json",
                        description="MIME type for output file"
                    )
                },
                execute=readEmails.__get__(self, self.__class__)  # Referenz auf Execute-Funktion
            ),
            "searchEmails": WorkflowActionDefinition(
                actionId="outlook.searchEmails",
                description="Search emails by query and return matching items",
                parameters={...},
                execute=searchEmails.__get__(self, self.__class__)
            )
        }
        
        # Register actions as methods (optional, für direkten Zugriff)
        # MethodBase lädt Actions primär aus _actions Dictionary
        self.readEmails = readEmails.__get__(self, self.__class__)
        self.searchEmails = searchEmails.__get__(self, self.__class__)

Execute-Funktion in separater Datei (methodOutlook/actions/readEmails.py):

from modules.workflows.methods.methodBase import action
from modules.datamodels.datamodelChat import ActionResult

@action
async def readEmails(self, parameters: Dict[str, Any]) -> ActionResult:
    """
    Execute function - Parameter-Definition ist jetzt in WorkflowActionDefinition.
    Diese Funktion enthält nur noch die Implementierung.
    """
    # Implementation bleibt gleich...
    connectionReference = parameters.get("connectionReference")
    folder = parameters.get("folder", "Inbox")
    # ... rest of implementation

Globale Frontend-Type-Definition

WICHTIG: Frontend-Types werden zentral in modules/shared/frontendTypes.py definiert, nicht redundant pro Action.

Die globale FrontendType Enum enthält:

  • Standard Types: text, textarea, number, select, multiselect, checkbox, date, datetime, email, timestamp, json, multilingual, file
  • Custom Types für Actions: userConnection, documentReference, workflowAction

Custom-Types unterstützen dynamische Option-Listen über API-Endpoints:

  • userConnection/api/options/user.connection (Connections des aktuellen Users)
  • documentReference/api/options/workflow.documentReference (Document-Referenzen aus Workflow-Context)
  • workflowAction/api/options/workflow.action (Verfügbare Actions aus Workflow-Context)

Datenmodelle

WorkflowActionParameter

WICHTIG:

  • Frontend-Types werden global definiert in modules/shared/frontendTypes.py und nicht redundant in Actions
  • Diese Klasse heißt WorkflowActionParameter (nicht ActionParameter) um Konflikte mit ActionParameters aus datamodelChat.py zu vermeiden
from typing import Optional, Any, Union, List, Dict
from pydantic import BaseModel, Field
from modules.shared.frontendTypes import FrontendType  # Globale Definition

class WorkflowActionParameter(BaseModel):
    """
    Parameter schema definition for a workflow action.
    
    This defines the structure and UI rendering for a single action parameter,
    NOT the actual parameter values (those are in ActionDefinition.parameters).
    """
    name: str = Field(description="Parameter name")
    type: str = Field(description="Python type as string (e.g., 'str', 'int', 'bool', 'List[str]')")
    frontendType: FrontendType = Field(description="UI rendering type (from global FrontendType enum)")
    frontendOptions: Optional[Union[str, List[Dict[str, Any]]]] = Field(
        None,
        description="Options for select/multiselect/custom types. String reference (e.g., 'user.connection') or static list. For custom types like userConnection, this is automatically set to the API endpoint."
    )
    required: bool = Field(False, description="Whether parameter is required")
    default: Optional[Any] = Field(None, description="Default value")
    description: str = Field("", description="Parameter description")
    validation: Optional[Dict[str, Any]] = Field(
        None,
        description="Validation rules (e.g., {'min': 1, 'max': 100})"
    )

Custom Frontend Types:

  • FrontendType.USER_CONNECTION: User connection selector - dynamische Options von /api/options/user.connection
  • FrontendType.DOCUMENT_REFERENCE: Document reference selector - dynamische Options aus Workflow-Context
  • FrontendType.WORKFLOW_ACTION: Workflow action selector - dynamische Options aus verfügbaren Actions

Für Custom-Types wird frontendOptions automatisch auf den entsprechenden API-Endpoint gesetzt (z.B. "user.connection").

WorkflowActionDefinition

WICHTIG: Diese Klasse heißt WorkflowActionDefinition (nicht ActionDefinition) um Konflikte mit der bestehenden ActionDefinition aus datamodelWorkflow.py zu vermeiden:

  • Bestehende ActionDefinition: Für Workflow-Execution-Planning (enthält konkrete Werte: action, actionObjective, parameters mit Werten)
  • Neue WorkflowActionDefinition: Für Action-Schema-Definitionen (enthält Metadaten: actionId, description, parameters als Schemas)
from typing import Dict, Callable, Awaitable, Optional, List
from pydantic import BaseModel, Field
from modules.datamodels.datamodelChat import ActionResult

class WorkflowActionDefinition(BaseModel):
    """
    Complete schema definition of a workflow action.
    
    This defines the metadata, parameters, and execution function for an action.
    This is different from datamodelWorkflow.ActionDefinition which contains
    actual execution values (action, actionObjective, parameters with values).
    
    This class defines the ACTION SCHEMA, not the execution plan.
    """
    actionId: str = Field(
        description="Unique action identifier for RBAC (format: 'module.actionName', e.g., 'outlook.readEmails')"
    )
    description: str = Field(description="Action description")
    parameters: Dict[str, WorkflowActionParameter] = Field(
        default_factory=dict,
        description="Parameter schema definitions"
    )
    execute: Optional[Callable[[Dict[str, Any]], Awaitable[ActionResult]]] = Field(
        None,
        description="Execution function - async function that takes parameters dict and returns ActionResult. Set dynamically."
    )
    category: Optional[str] = Field(None, description="Action category for grouping")
    tags: List[str] = Field(default_factory=list, description="Tags for search/filtering")

Integration mit Refactored Structure

Kompatibilität mit Folder-basierter Struktur

Nach dem Refactoring sind alle Methods in Folder-Strukturen organisiert:

  • Actions in actions/ Unterordnern
  • Helper-Funktionen in helpers/ Unterordnern
  • Hauptklasse minimal gehalten

RBAC-Integration funktioniert nahtlos mit dieser Struktur:

  1. Action-Definitionen werden in der Hauptklasse (methodOutlook.py) im _actions Dictionary definiert
  2. Execute-Funktionen bleiben in separaten Action-Dateien (actions/readEmails.py)
  3. Helper-Klassen werden in der Hauptklasse initialisiert und von Actions verwendet

Vorteile:

  • Zentrale Verwaltung aller Action-Definitionen (inkl. RBAC-IDs)
  • Actions bleiben modular und testbar
  • Helper-Funktionen bleiben wiederverwendbar
  • Einfache Migration: Schrittweise _actions Dictionary hinzufügen

Beispiel: Vollständige Integration

Struktur:

methodOutlook/
├── __init__.py
├── methodOutlook.py (Hauptklasse mit _actions Dictionary)
├── helpers/
│   ├── connection.py
│   ├── emailProcessing.py
│   └── folderManagement.py
└── actions/
    ├── readEmails.py (Execute-Funktion)
    ├── searchEmails.py
    └── ...

Hauptklasse (methodOutlook/methodOutlook.py):

class MethodOutlook(MethodBase):
    def __init__(self, services):
        super().__init__(services)
        self.name = "outlook"
        self.description = "Handle Microsoft Outlook email operations"
        
        # Initialize helper modules
        self.connection = ConnectionHelper(self)
        self.emailProcessing = EmailProcessingHelper(self)
        self.folderManagement = FolderManagementHelper(self)
        
        # RBAC-Integration: Action-Definitionen mit actionId
        self._actions = {
            "readEmails": WorkflowActionDefinition(
                actionId="outlook.readEmails",  # RBAC-ID
                description="Read emails and metadata from a mailbox folder",
                parameters={...},
                execute=readEmails.__get__(self, self.__class__)
            ),
            # ... weitere Actions
        }
        
        # Actions als Methoden registrieren (optional, für direkten Zugriff)
        self.readEmails = readEmails.__get__(self, self.__class__)

Action-Datei (methodOutlook/actions/readEmails.py):

@action
async def readEmails(self, parameters: Dict[str, Any]) -> ActionResult:
    """Execute function - verwendet Helper-Klassen"""
    connection = self.connection.getMicrosoftConnection(...)
    params = self.emailProcessing.buildSearchParameters(...)
    # ... implementation

MethodBase Erweiterung

Neue MethodBase Struktur

class MethodBase:
    """Base class for all methods"""
    
    def __init__(self, services: Any):
        self.services = services
        self.name: str
        self.description: str
        self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
        
        # Actions MÜSSEN als Dictionary definiert sein
        # Jede Method-Klasse muss _actions Dictionary in __init__ definieren
        self._actions: Dict[str, WorkflowActionDefinition] = {}
        
        # Nach Initialisierung: Actions validieren
        self._validateActions()
    
    def _validateActions(self):
        """Validate that _actions dictionary is properly defined"""
        if not hasattr(self, '_actions') or not isinstance(self._actions, dict):
            raise ValueError(f"Method {self.name} must define _actions dictionary in __init__")
        
        for actionName, actionDef in self._actions.items():
            if not isinstance(actionDef, WorkflowActionDefinition):
                raise ValueError(f"Action '{actionName}' in {self.name} must be WorkflowActionDefinition instance")
            
            if not actionDef.actionId:
                raise ValueError(f"Action '{actionName}' in {self.name} must have actionId")
            
            if not actionDef.execute:
                raise ValueError(f"Action '{actionName}' in {self.name} must have execute function")
    
    @property
    def actions(self) -> Dict[str, Dict[str, Any]]:
        """
        Dynamically collect all actions from _actions dictionary.
        Returns format for API/UI consumption.
        
        REQUIREMENT: Alle Actions müssen in _actions Dictionary definiert sein.
        Actions ohne _actions Definition sind nicht verfügbar.
        """
        result = {}
        
        # Actions müssen in _actions Dictionary definiert sein
        if not hasattr(self, '_actions') or not self._actions:
            logger.error(f"Method {self.name} has no _actions dictionary defined. Actions will not be available.")
            return result
        
        for actionName, actionDef in self._actions.items():
            # RBAC-Check: Prüfe ob Action für aktuellen User verfügbar ist
            if not self._checkActionPermission(actionDef.actionId):
                continue  # Skip if user doesn't have permission
            
            # Konvertiere WorkflowActionDefinition zu System-Format
            result[actionName] = {
                'description': actionDef.description,
                'parameters': self._convertParametersToSystemFormat(actionDef.parameters),
                'method': self._createActionWrapper(actionDef)
            }
        
        return result
    
    def _checkActionPermission(self, actionId: str) -> bool:
        """
        Check if current user has permission to execute this action.
        Uses RBAC RESOURCE context.
        
        REQUIREMENT: RBAC-Service muss verfügbar sein.
        """
        if not hasattr(self.services, 'rbac') or not self.services.rbac:
            logger.error(f"RBAC service not available. Action {actionId} will be denied.")
            return False
        
        currentUser = self.services.chat.getCurrentUser()
        if not currentUser:
            logger.warning(f"No current user found. Action {actionId} will be denied.")
            return False
        
        # RBAC-Check: RESOURCE context, item = actionId
        permissions = self.services.rbac.getUserPermissions(
            user=currentUser,
            context=AccessRuleContext.RESOURCE,
            item=actionId
        )
        
        return permissions.view
    
    def _convertParametersToSystemFormat(self, parameters: Dict[str, WorkflowActionParameter]) -> Dict[str, Dict[str, Any]]:
        """Convert WorkflowActionParameter dict to system format for API/UI consumption"""
        result = {}
        for paramName, param in parameters.items():
            result[paramName] = {
                'type': param.type,
                'required': param.required,
                'description': param.description,
                'default': param.default,
                'frontendType': param.frontendType.value,
                'frontendOptions': param.frontendOptions,
                'validation': param.validation
            }
        return result
    
    def _createActionWrapper(self, actionDef: WorkflowActionDefinition):
        """Create wrapper function for action execution with parameter validation"""
        async def wrapper(parameters: Dict[str, Any], *args, **kwargs):
            # Parameter-Validierung basierend auf WorkflowActionParameter definitions
            validatedParams = self._validateParameters(parameters, actionDef.parameters)
            
            # Execute action
            return await actionDef.execute(validatedParams, *args, **kwargs)
        
        wrapper.is_action = True
        return wrapper
    
    def _validateParameters(self, parameters: Dict[str, Any], paramDefs: Dict[str, WorkflowActionParameter]) -> Dict[str, Any]:
        """Validate parameters against definitions"""
        validated = {}
        
        for paramName, paramDef in paramDefs.items():
            value = parameters.get(paramName)
            
            # Check required
            if paramDef.required and value is None:
                raise ValueError(f"Required parameter '{paramName}' is missing")
            
            # Use default if not provided
            if value is None and paramDef.default is not None:
                value = paramDef.default
            
            # Type validation
            if value is not None:
                value = self._validateType(value, paramDef.type)
            
            # Custom validation rules
            if paramDef.validation and value is not None:
                self._applyValidationRules(value, paramDef.validation)
            
            validated[paramName] = value
        
        return validated
    
    def _validateType(self, value: Any, expectedType: type) -> Any:
        """Validate and convert value to expected type"""
        # Type validation logic...
        if expectedType == int:
            return int(value)
        elif expectedType == str:
            return str(value)
        # ... weitere Typen
        return value
    
    def _applyValidationRules(self, value: Any, rules: Dict[str, Any]):
        """Apply custom validation rules"""
        if 'min' in rules and value < rules['min']:
            raise ValueError(f"Value must be >= {rules['min']}")
        if 'max' in rules and value > rules['max']:
            raise ValueError(f"Value must be <= {rules['max']}")
        # ... weitere Validierungsregeln

Migrationsstrategie

Schritt 1: Neue Datenmodelle erstellen

WICHTIG: Die bestehenden Klassen ActionDefinition (in datamodelWorkflow.py) und ActionParameters (in datamodelChat.py) haben einen anderen Zweck:

  • ActionDefinition (existing): Für Workflow-Execution-Planning (enthält konkrete Werte)
  • ActionParameters (existing): Einfacher Parameter-Wrapper

Lösung: Neue Klassen mit klaren Namen für Action-Schema-Definitionen erstellen.

Datei: gateway/modules/datamodels/datamodelWorkflowActions.py

from typing import Optional, Any, Union, List, Dict, Callable, Awaitable
from pydantic import BaseModel, Field
from modules.datamodels.datamodelChat import ActionResult
from modules.shared.frontendTypes import FrontendType  # Globale Definition verwenden
from modules.shared.attributeUtils import registerModelLabels

class WorkflowActionParameter(BaseModel):
    """
    Parameter schema definition for a workflow action.
    
    This defines the structure and UI rendering for a single action parameter,
    NOT the actual parameter values (those are in ActionDefinition.parameters).
    """
    name: str = Field(description="Parameter name")
    type: str = Field(description="Python type as string: 'str', 'int', 'bool', 'List[str]', etc.")
    frontendType: FrontendType = Field(description="UI rendering type (from global FrontendType enum)")
    frontendOptions: Optional[Union[str, List[Dict[str, Any]]]] = Field(
        None,
        description="Options for select/multiselect/custom types. String reference (e.g., 'user.connection') or static list. For custom types, this is automatically set to the API endpoint."
    )
    required: bool = Field(False, description="Whether parameter is required")
    default: Optional[Any] = Field(None, description="Default value")
    description: str = Field("", description="Parameter description")
    validation: Optional[Dict[str, Any]] = Field(
        None,
        description="Validation rules (e.g., {'min': 1, 'max': 100})"
    )

class WorkflowActionDefinition(BaseModel):
    """
    Complete schema definition of a workflow action.
    
    This defines the metadata, parameters, and execution function for an action.
    This is different from datamodelWorkflow.ActionDefinition which contains
    actual execution values (action, actionObjective, parameters with values).
    
    This class defines the ACTION SCHEMA, not the execution plan.
    """
    actionId: str = Field(
        description="Unique action identifier for RBAC (format: 'module.actionName', e.g., 'outlook.readEmails')"
    )
    description: str = Field(description="Action description")
    parameters: Dict[str, WorkflowActionParameter] = Field(
        default_factory=dict,
        description="Parameter schema definitions"
    )
    execute: Optional[Callable] = Field(
        None,
        description="Execution function - async function that takes parameters dict and returns ActionResult. Set dynamically."
    )
    category: Optional[str] = Field(None, description="Action category for grouping")
    tags: List[str] = Field(default_factory=list, description="Tags for search/filtering")

# Register model labels for UI
registerModelLabels(
    "WorkflowActionDefinition",
    {"en": "Workflow Action Definition", "fr": "Définition d'action de workflow"},
    {
        "actionId": {"en": "Action ID", "fr": "ID d'action"},
        "description": {"en": "Description", "fr": "Description"},
        "parameters": {"en": "Parameters", "fr": "Paramètres"},
        "category": {"en": "Category", "fr": "Catégorie"},
        "tags": {"en": "Tags", "fr": "Étiquettes"},
    },
)

registerModelLabels(
    "WorkflowActionParameter",
    {"en": "Workflow Action Parameter", "fr": "Paramètre d'action de workflow"},
    {
        "name": {"en": "Name", "fr": "Nom"},
        "type": {"en": "Type", "fr": "Type"},
        "frontendType": {"en": "Frontend Type", "fr": "Type frontend"},
        "frontendOptions": {"en": "Frontend Options", "fr": "Options frontend"},
        "required": {"en": "Required", "fr": "Requis"},
        "default": {"en": "Default", "fr": "Par défaut"},
        "description": {"en": "Description", "fr": "Description"},
        "validation": {"en": "Validation", "fr": "Validation"},
    },
)

Schritt 2: MethodBase erweitern

Datei: gateway/modules/workflows/methods/methodBase.py

  • Neue _actions Dictionary Property (REQUIRED)
  • RBAC-Check Integration (REQUIRED)
  • Parameter-Validierung
  • Unterstützung für Refactored Structure (Actions in separaten Dateien)

WICHTIG:

  • MethodBase unterstützt NUR noch die _actions Dictionary-Struktur
  • Alle Actions MÜSSEN in _actions Dictionary definiert sein
  • RBAC-Service ist REQUIRED (kein Fallback ohne RBAC)
  • @action Decorator wird weiterhin verwendet, aber nur für Execute-Funktionen (nicht für Discovery)

Schritt 3: Beispiel-Migration mit Refactored Structure

Vorher (monolithische methodOutlook.py):

@action
async def readEmails(self, parameters: Dict[str, Any]) -> ActionResult:
    """
    GENERAL:
    - Purpose: Read emails from Outlook mailbox
    
    Parameters:
    - connectionReference (str, required): Microsoft connection label.
    - query (str, optional): Search query for emails.
    - folder (str, optional): Folder name.
    - limit (int, optional): Maximum number of emails. Default: 50.
    """
    # Implementation...

Nachher (Refactored Structure):

1. Hauptklasse (methodOutlook/methodOutlook.py):

from modules.datamodels.datamodelWorkflowActions import WorkflowActionDefinition, WorkflowActionParameter
from modules.shared.frontendTypes import FrontendType
from .actions.readEmails import readEmails
from .helpers.connection import ConnectionHelper
from .helpers.emailProcessing import EmailProcessingHelper
from .helpers.folderManagement import FolderManagementHelper

class MethodOutlook(MethodBase):
    def __init__(self, services):
        super().__init__(services)
        self.name = "outlook"
        self.description = "Handle Microsoft Outlook email operations"
        
        # Initialize helper modules
        self.connection = ConnectionHelper(self)
        self.emailProcessing = EmailProcessingHelper(self)
        self.folderManagement = FolderManagementHelper(self)
        
        # Actions werden deklarativ definiert
        self._actions = {
            "readEmails": WorkflowActionDefinition(
                actionId="outlook.readEmails",
                description="Read emails and metadata from a mailbox folder",
                parameters={
                    "connectionReference": WorkflowActionParameter(
                        name="connectionReference",
                        type="str",
                        frontendType=FrontendType.USER_CONNECTION,
                        required=True,
                        description="Microsoft connection label"
                    ),
                    "folder": WorkflowActionParameter(
                        name="folder",
                        type="str",
                        frontendType=FrontendType.SELECT,
                        frontendOptions="outlook.folder",
                        required=False,
                        default="Inbox",
                        description="Folder to read from"
                    ),
                    "limit": WorkflowActionParameter(
                        name="limit",
                        type="int",
                        frontendType=FrontendType.NUMBER,
                        required=False,
                        default=10,
                        description="Maximum items to return",
                        validation={"min": 1, "max": 1000}
                    ),
                    "filter": WorkflowActionParameter(
                        name="filter",
                        type="str",
                        frontendType=FrontendType.TEXT,
                        required=False,
                        description="Sender, query operators, or subject text"
                    ),
                    "outputMimeType": WorkflowActionParameter(
                        name="outputMimeType",
                        type="str",
                        frontendType=FrontendType.SELECT,
                        frontendOptions=["application/json", "text/plain", "text/csv"],
                        required=False,
                        default="application/json",
                        description="MIME type for output file"
                    )
                },
                execute=readEmails.__get__(self, self.__class__)  # Referenz auf Execute-Funktion
            )
        }
        
        # Register actions as methods (optional, für direkten Zugriff)
        self.readEmails = readEmails.__get__(self, self.__class__)

2. Action-Datei (methodOutlook/actions/readEmails.py):

from modules.workflows.methods.methodBase import action
from modules.datamodels.datamodelChat import ActionResult

@action
async def readEmails(self, parameters: Dict[str, Any]) -> ActionResult:
    """
    Execute function - Parameter-Definition ist jetzt in WorkflowActionDefinition.
    Diese Funktion enthält nur noch die Implementierung.
    """
    # Implementation bleibt gleich...
    connectionReference = parameters.get("connectionReference")
    folder = parameters.get("folder", "Inbox")
    # ... rest of implementation using self.connection, self.emailProcessing, etc.

RBAC-Integration

Action-IDs Format

Actions werden im RBAC-System als RESOURCE-Context Items behandelt:

  • Format: {moduleName}.{actionName}
  • Beispiele:
    • outlook.readEmails
    • outlook.sendEmail
    • sharepoint.uploadDocument
    • ai.process

RBAC-Regeln für Actions

{
  "roleLabel": "user",
  "context": "RESOURCE",
  "item": "outlook.readEmails",
  "view": true
}
{
  "roleLabel": "admin",
  "context": "RESOURCE",
  "item": "outlook",
  "view": true
}

Hierarchie: Spezifische Action-Regeln überschreiben generische Module-Regeln.

Bootstrap: Default RBAC Rules für Actions

In interfaceBootstrap.py:

def initRbacRules(db: DatabaseConnector) -> None:
    # ... existing rules ...
    
    # Action Rules (RESOURCE context)
    createActionRules(db)

def createActionRules(db: DatabaseConnector):
    """Create default RBAC rules for workflow actions"""
    
    # SysAdmin: Access to all actions
    db.recordCreate(AccessRule(
        roleLabel="sysadmin",
        context=AccessRuleContext.RESOURCE,
        item=None,  # All resources
        view=True
    ))
    
    # Admin: Access to all actions
    db.recordCreate(AccessRule(
        roleLabel="admin",
        context=AccessRuleContext.RESOURCE,
        item=None,
        view=True
    ))
    
    # User: Access to specific actions only
    userActions = [
        "outlook.readEmails",
        "outlook.sendEmail",
        "sharepoint.readDocuments",
        "ai.process"
    ]
    
    for actionId in userActions:
        db.recordCreate(AccessRule(
            roleLabel="user",
            context=AccessRuleContext.RESOURCE,
            item=actionId,
            view=True
        ))
    
    # Viewer: Read-only actions
    viewerActions = [
        "outlook.readEmails",
        "sharepoint.readDocuments"
    ]
    
    for actionId in viewerActions:
        db.recordCreate(AccessRule(
            roleLabel="viewer",
            context=AccessRuleContext.RESOURCE,
            item=actionId,
            view=True
        ))

Organisation der Action-Definitionen

Zentrale Definition in Hauptklasse

Prinzip: Alle Action-Definitionen werden zentral in der Hauptklasse (methodOutlook.py) im _actions Dictionary definiert.

Vorteile:

  • Übersichtliche Verwaltung aller Actions einer Method
  • Einfache RBAC-Integration (alle Action-IDs an einem Ort)
  • Einfache API-Discovery (MethodBase kann alle Actions sammeln)
  • Type-Safety durch Pydantic Models

Execute-Funktionen bleiben in separaten Dateien

Prinzip: Die Execute-Funktionen bleiben in den separaten Action-Dateien (actions/readEmails.py).

Vorteile:

  • Modulare Struktur (eine Datei pro Action)
  • Einfache Wartung und Tests
  • Parallele Entwicklung möglich
  • Helper-Klassen bleiben zugänglich über self

Beispiel-Struktur

methodOutlook/
├── methodOutlook.py
│   └── _actions = {
│       "readEmails": WorkflowActionDefinition(...),
│       "searchEmails": WorkflowActionDefinition(...)
│   }
└── actions/
    ├── readEmails.py
    │   └── async def readEmails(self, parameters) -> ActionResult
    └── searchEmails.py
        └── async def searchEmails(self, parameters) -> ActionResult

Migration-Strategie

  1. Schritt 1: Action-Definitionen in _actions Dictionary hinzufügen
  2. Schritt 2: Execute-Funktionen aus Action-Dateien referenzieren
  3. Schritt 3: RBAC-Regeln in Bootstrap erstellen
  4. Schritt 4: Tests und Validierung

Wichtig: Execute-Funktionen müssen nicht geändert werden - sie bleiben identisch!

Vorteile

1. Keine Duplikation

  • Parameter werden nur einmal definiert (in WorkflowActionDefinition)
  • Keine Docstring-Parsing mehr nötig
  • Type-Safety durch Pydantic Models
  • Zentrale Verwaltung in Hauptklasse

2. RBAC-Integration

  • Jede Action hat eine eindeutige ID für RBAC
  • Granulare Kontrolle pro Action möglich
  • Hierarchische Regeln (Module → Action)
  • Action-IDs zentral in _actions Dictionary verwaltet

3. UI-Rendering

  • Frontend-Typen explizit definiert
  • Options-Referenzen für dynamische Optionen
  • Validierung auf Backend-Ebene
  • Strukturierte Parameter-Definitionen für Frontend

4. Plug-and-Play

  • Actions bleiben als separate Method-Klassen
  • Einfache Erweiterung durch neue Method-Klassen
  • Refactored Structure bleibt erhalten
  • Klare Anforderungen: Alle Actions müssen _actions Dictionary haben

5. Type Safety

  • Pydantic Models für Validierung
  • Type-Hints für bessere IDE-Unterstützung
  • Runtime-Validierung
  • Zentrale Definitionen für bessere Wartbarkeit

6. Refactored Structure Kompatibilität

  • Funktioniert nahtlos mit Folder-basierter Struktur
  • Execute-Funktionen bleiben in separaten Dateien
  • Helper-Klassen bleiben wiederverwendbar
  • Einfache Migration ohne Code-Änderungen in Action-Dateien

Migration Timeline

Phase 1: Foundation (Woche 1)

  • Datenmodelle erstellen (datamodelWorkflowActions.py)
  • MethodBase erweitern
  • RBAC-Integration in MethodBase
  • Refactoring aller Methods abgeschlossen (Ordnerstruktur)

Phase 2: Beispiel-Migration (Woche 2)

  • 📝 Ein Method-Beispiel migrieren (z.B. methodOutlook mit RBAC-Definitionen)
  • 📝 Action-Definitionen in Hauptklasse (_actions Dictionary) hinzufügen
  • 📝 Execute-Funktionen in Action-Dateien bleiben unverändert
  • 📝 Tests schreiben
  • 📝 Dokumentation aktualisieren

Hinweis: Da alle Methods bereits refactored sind (Actions in separaten Dateien), ist die Migration einfacher: Nur _actions Dictionary in Hauptklasse hinzufügen, Execute-Funktionen bleiben unverändert.

Phase 3: Vollständige Migration (Woche 3-4)

  • 📝 Alle Methods migrieren (Action-Definitionen hinzufügen)
  • 📝 RBAC-Regeln in Bootstrap erstellen
  • 📝 Frontend-Integration
  • 📝 API-Endpunkte für Action-Discovery implementieren

Phase 4: Testing & Cleanup (Woche 5)

  • 📝 Unit Tests
  • 📝 Integration Tests
  • 📝 Performance Tests
  • 📝 Alte Docstring-Parsing-Logik entfernen (nicht mehr benötigt)
  • 📝 Sicherstellen, dass alle Methods _actions Dictionary haben

Praktische Umsetzung

Schritt-für-Schritt: Action-Definition hinzufügen

Voraussetzung: Method ist bereits refactored (Actions in separaten Dateien).

Schritt 1: Importiere benötigte Klassen in Hauptklasse

# In methodOutlook/methodOutlook.py
from modules.datamodels.datamodelWorkflowActions import WorkflowActionDefinition, WorkflowActionParameter
from modules.shared.frontendTypes import FrontendType

Schritt 2: Erstelle _actions Dictionary in __init__

def __init__(self, services):
    super().__init__(services)
    # ... existing code ...
    
    # RBAC-Integration: Action-Definitionen
    self._actions = {
        "readEmails": WorkflowActionDefinition(
            actionId="outlook.readEmails",
            description="Read emails and metadata from a mailbox folder",
            parameters={
                "connectionReference": WorkflowActionParameter(
                    name="connectionReference",
                    type="str",
                    frontendType=FrontendType.USER_CONNECTION,
                    required=True,
                    description="Microsoft connection label"
                ),
                # ... weitere Parameter
            },
            execute=readEmails.__get__(self, self.__class__)
        )
    }

Schritt 3: Execute-Funktion bleibt unverändert

# In methodOutlook/actions/readEmails.py
# Keine Änderungen nötig - Funktion bleibt identisch
@action
async def readEmails(self, parameters: Dict[str, Any]) -> ActionResult:
    # Implementation bleibt gleich...

Schritt 4: RBAC-Regel in Bootstrap hinzufügen

# In interfaceBootstrap.py
db.recordCreate(AccessRule(
    roleLabel="user",
    context=AccessRuleContext.RESOURCE,
    item="outlook.readEmails",
    view=True
))

Migration-Checkliste pro Method

  • _actions Dictionary in Hauptklasse erstellen
  • Alle Actions mit WorkflowActionDefinition definieren
  • Parameter mit WorkflowActionParameter definieren
  • Frontend-Types aus globaler FrontendType Enum verwenden
  • Execute-Funktionen aus Action-Dateien referenzieren
  • RBAC-Regeln in Bootstrap hinzufügen
  • Tests durchführen

Offene Fragen

  1. Backward Compatibility: Werden alte Actions ohne _actions Dictionary unterstützt?

    • Antwort: Nein. Alle Actions MÜSSEN in _actions Dictionary definiert sein. Es gibt keinen Fallback auf @action Decorator. Actions ohne _actions Definition sind nicht verfügbar.
  2. Parameter-Validierung: Soll Validierung strikt sein oder tolerant?

    • Antwort: Konfigurierbar pro Action
  3. Action-Discovery: Sollen Actions zur Laufzeit registriert werden können?

    • Antwort: Ja, über _registerActions() Methode
  4. Frontend-Integration: Wie werden Actions im Frontend angezeigt?

    • Antwort: API-Endpoint /api/workflows/actions liefert strukturierte Action-Definitionen aus _actions Dictionary (gefiltert nach RBAC)
  5. Action-Definitionen in separaten Dateien: Sollen Action-Definitionen auch in den Action-Dateien stehen?

    • Antwort: Nein, Action-Definitionen bleiben in der Hauptklasse (_actions Dictionary). Die Action-Dateien enthalten nur die Execute-Funktionen. Dies ermöglicht zentrale Verwaltung und einfache RBAC-Integration.
  6. Migration-Reihenfolge: Sollen alle Actions einer Method gleichzeitig migriert werden?

    • Antwort: Empfohlen: Schrittweise pro Action, um Risiko zu minimieren. Aber auch vollständige Migration pro Method ist möglich.

API-Endpunkte

WICHTIG: Diese API-Endpunkte beziehen sich auf Action-Definitionen (Schema), nicht auf ausführbare Workflows oder Templates.

GET /api/workflows/actions

Liefert alle verfügbaren Actions für den aktuellen User (gefiltert nach RBAC):

Zweck: Action-Discovery für Workflow-Editor und dynamische Workflows Verwendung:

  • Workflow-Editor: Zeigt verfügbare Actions in Toolbox
  • Dynamic Workflows: Zeigt verfügbare Actions für AI-Auswahl
{
  "actions": [
    {
      "module": "outlook",
      "actionId": "outlook.readEmails",
      "name": "readEmails",
      "description": "Read emails from Outlook mailbox",
      "parameters": {
        "connectionReference": {
          "type": "str",
          "frontendType": "userConnection",
          "frontendOptions": "user.connection",  # Automatisch für Custom-Types
          "required": true,
          "description": "Microsoft connection label"
        },
        "documentList": {
          "type": "List[str]",
          "frontendType": "documentReference",
          "frontendOptions": "workflow.documentReference",  # Automatisch für Custom-Types
          "required": false,
          "description": "Document list reference(s) from previous actions"
        },
        ...
      }
    },
    ...
  ]
}

GET /api/workflows/actions/{module}

Liefert Actions für ein spezifisches Modul.

POST /api/workflows/actions/{module}/{action}/execute

Führt eine Action aus (mit RBAC-Check).

Custom Frontend Types für Actions

Verfügbare Custom Types

  1. FrontendType.USER_CONNECTION

    • API-Endpoint: /api/options/user.connection
    • Beschreibung: Zeigt alle aktiven Connections des aktuellen Users
    • Verwendung: Für Parameter wie connectionReference in Outlook/SharePoint Actions
    • Beispiel:
      WorkflowActionParameter(
          name="connectionReference",
          type="str",
          frontendType=FrontendType.USER_CONNECTION,
          required=True,
          description="Microsoft connection label"
      )
      
  2. FrontendType.DOCUMENT_REFERENCE

    • API-Endpoint: /api/options/workflow.documentReference (zu implementieren)
    • Beschreibung: Zeigt verfügbare Document-Referenzen aus dem aktuellen Workflow-Context
    • Verwendung: Für Parameter wie documentList in Actions, die auf vorherige Action-Ergebnisse verweisen
    • Beispiel:
      WorkflowActionParameter(
          name="documentList",
          type="List[str]",
          frontendType=FrontendType.DOCUMENT_REFERENCE,
          required=False,
          description="Document list reference(s) from previous actions"
      )
      
  3. FrontendType.WORKFLOW_ACTION

    • API-Endpoint: /api/options/workflow.action (zu implementieren)
    • Beschreibung: Zeigt verfügbare Actions aus dem Workflow-Context
    • Verwendung: Für Parameter, die auf andere Actions verweisen

Custom Types erweitern

Neue Custom-Types können über frontendTypes.py registriert werden:

from modules.shared.frontendTypes import FrontendType, registerCustomType

# Neuer Custom-Type hinzufügen
FrontendType.SHAREPOINT_FOLDER = "sharepointFolder"

# Registrieren
registerCustomType(
    frontendType=FrontendType.SHAREPOINT_FOLDER,
    optionsApiEndpoint="sharepoint.folder",
    description={
        "en": "SharePoint Folder",
        "fr": "Dossier SharePoint",
        "de": "SharePoint-Ordner"
    }
)

Frontend-Integration

Das Frontend muss:

  1. Custom-Types erkennen (z.B. frontendType === "userConnection")
  2. Automatisch Options von /api/options/{optionsName} laden
  3. Die Options als Select/Multiselect rendern

Beispiel Frontend-Logik:

if (param.frontendType === 'userConnection') {
  // Automatisch Options von /api/options/user.connection laden
  const options = await fetch(`/api/options/${param.frontendOptions}`);
  // Als Select rendern
}