wiki/z-archive/c-work/2026-03-workflow-editor.md
2026-04-06 00:46:32 +02:00

45 KiB

Graphischer Workflow-Editor Konzept

Übersicht

Dieses Dokument beschreibt das Konzept für einen graphischen Workflow-Editor, der es ermöglicht, Workflow-Templates visuell zu modellieren. Der Editor wird für die Erstellung und Bearbeitung von Automation-Templates verwendet, die in formAutomations.js verwaltet werden.

Wichtige Terminologie: Workflow vs. WorkflowTemplate

WICHTIG: Es gibt eine klare Trennung zwischen ausführbaren Workflows und Templates:

ChatWorkflow (ausführbares Objekt)

  • Klasse: ChatWorkflow (in datamodelChat.py)
  • Zweck: Ausführbare Workflow-Instanz
  • Eigenschaften:
    • Hat id, status (running/completed/stopped/error)
    • Enthält tasks (Liste von TaskItem-Objekten)
    • Enthält messages, logs, stats
    • Hat workflowMode (automatic oder dynamic)
  • Ausführung:
    • Kann über Route mit start oder stop ausgeführt werden
    • Wird automatisch (scheduled) oder dynamisch (on-demand) gestartet
  • Lebenszyklus: Wird zur Laufzeit erstellt und ausgeführt

AutomationDefinition (Template-Objekt)

  • Klasse: AutomationDefinition (in datamodelChat.py)
  • Zweck: Vorlage für ChatWorkflows
  • Eigenschaften:
    • Hat id, label, schedule (Cron-Pattern)
    • Enthält template (JSON-String mit Workflow-Struktur)
    • Enthält placeholders (Dict mit Platzhalter-Werten)
    • Hat active (ob Automation aktiviert ist)
  • Verwendung:
    • Wird im Workflow-Editor bearbeitet
    • Wird verwendet, um ChatWorkflows zu erstellen
    • Template wird mit Placeholders gefüllt → ChatWorkflow wird erstellt
  • Lebenszyklus: Wird einmal erstellt und mehrfach verwendet

WorkflowTemplateModel (Editor-Modell)

  • Klasse: WorkflowTemplateModel (nur für Editor, nicht in Datenbank)
  • Zweck: Strukturiertes Modell für graphischen Editor
  • Verwendung:
    • Wird aus AutomationDefinition.template (JSON-String) geparst
    • Wird im Editor bearbeitet
    • Wird zurück zu AutomationDefinition.template konvertiert
  • Lebenszyklus: Nur während Editor-Session vorhanden

Beziehung zwischen Objekten

AutomationDefinition (Template)
    ↓ (wird verwendet, um zu erstellen)
ChatWorkflow (ausführbare Instanz)
    ↓ (wird ausgeführt)
TaskItem → ActionItem → Ergebnisse

Beispiel:

  1. AutomationDefinition mit template: '{"tasks": [...]}' und placeholders: {"connectionName": "MyConn"}
  2. Template wird mit Placeholders gefüllt → ChatWorkflow wird erstellt
  3. ChatWorkflow wird gestartet → Tasks werden ausgeführt → Actions werden ausgeführt

Zielsetzung

  1. Visuelle Modellierung: Workflows mit Tasks und Actions graphisch erstellen
  2. Parameter-Konfiguration: Action-Parameter direkt im Editor setzen
  3. Dependency-Management: Document-Dependencies zwischen Actions visuell darstellen
  4. Template-Generierung: Automatische Generierung von JSON-Templates aus graphischem Modell

Datenstruktur-Analyse

Bestehende Datenmodelle

ChatWorkflow (datamodelChat.py) - AUSFÜHRBARES OBJEKT

class ChatWorkflow(BaseModel):
    """Ausführbare Workflow-Instanz"""
    id: str
    mandateId: str
    status: str  # "running", "completed", "stopped", "error"
    workflowMode: WorkflowModeEnum  # "automatic" oder "dynamic"
    tasks: list  # Liste von TaskItem-Objekten (zur Laufzeit erstellt)
    messages: List[ChatMessage]
    logs: List[ChatLog]
    stats: List[ChatStat]
    # ... weitere Felder

WICHTIG: ChatWorkflow ist das ausführbare Objekt, das über Route mit start/stop gesteuert werden kann.

TaskItem (datamodelChat.py) - Teil von ChatWorkflow

class TaskItem(BaseModel):
    """Task innerhalb eines ausführbaren ChatWorkflows"""
    id: str
    workflowId: str  # Foreign Key zu ChatWorkflow
    userInput: str
    status: TaskStatus
    actionList: List[ActionItem]  # Liste von Actions
    dependencies: List[str]  # Task IDs, von denen dieser Task abhängt
    # ... weitere Felder

WICHTIG: TaskItem existiert nur in ausführbaren ChatWorkflow-Instanzen, nicht in Templates.

ActionItem (datamodelChat.py) - Teil von TaskItem

class ActionItem(BaseModel):
    """Action innerhalb eines TaskItems"""
    id: str
    execMethod: str  # z.B. "outlook", "sharepoint", "ai"
    execAction: str  # z.B. "readEmails", "uploadDocument"
    execParameters: Dict[str, Any]  # Action-Parameter (mit konkreten Werten)
    execResultLabel: Optional[str]  # Label für resultierende Documents
    # ... weitere Felder

WICHTIG: ActionItem existiert nur in ausführbaren ChatWorkflow-Instanzen, nicht in Templates.

AutomationDefinition (datamodelChat.py) - TEMPLATE-OBJEKT

class AutomationDefinition(BaseModel):
    """Template/Vorlage für ChatWorkflows"""
    id: str
    mandateId: str
    label: str  # User-friendly name
    template: str  # JSON-String mit Workflow-Struktur (enthält Tasks und Actions als Template)
    placeholders: Dict[str, str]  # Platzhalter-Werte (z.B. {"connectionName": "MyConn"})
    schedule: str  # Cron-Schedule für automatische Ausführung
    active: bool  # Ob Automation aktiviert ist
    eventId: Optional[str]  # Event ID für Event-Management
    executionLogs: List[Dict[str, Any]]  # Logs von Workflow-Ausführungen
    # ... weitere Felder

WICHTIG:

  • AutomationDefinition ist das Template-Objekt, das im Workflow-Editor bearbeitet wird
  • template Feld enthält JSON-String mit Workflow-Struktur (Tasks und Actions als Vorlage)
  • placeholders enthält Werte für Platzhalter im Template
  • Wird verwendet, um ChatWorkflow-Instanzen zu erstellen

Document Dependencies

Actions können documentList Parameter haben, die auf Ergebnisse von anderen Actions verweisen:

Formate:

  • docList:label - Referenz zu einem Document-Label aus einer vorherigen Action
  • docList:messageId:label - Cross-Round Referenz mit Message-ID

Beispiel:

{
  "execMethod": "sharepoint",
  "execAction": "readDocuments",
  "execParameters": {
    "connectionReference": "{{connectionName}}",
    "documentList": ["docList:emails_found"]  // Verweist auf execResultLabel "emails_found"
  }
}

Workflow-Editor UI-Konzept

Layout-Struktur

┌─────────────────────────────────────────────────────────────┐
│  Workflow Editor - [Template Name]                         │
├─────────────────────────────────────────────────────────────┤
│  [Toolbar] [Save] [Validate] [Preview] [Help]             │
├─────────────────────────────────────────────────────────────┤
│  ┌──────────────────┐  ┌─────────────────────────────────┐ │
│  │  Toolbox         │  │  Canvas (Graph Editor)          │ │
│  │                  │  │                                 │ │
│  │  📋 Tasks        │  │  ┌─────────┐                    │ │
│  │  ┌─────────────┐ │  │  │ Task 1 │                    │ │
│  │  │ + Task      │ │  │  └────┬───┘                    │ │
│  │  └─────────────┘ │  │       │                        │ │
│  │                  │  │  ┌────▼───┐                    │ │
│  │  ⚙️ Actions      │  │  │Action 1 │                    │ │
│  │  ┌─────────────┐ │  │  └────┬───┘                    │ │
│  │  │ outlook     │ │  │       │                        │ │
│  │  │ sharepoint  │ │  │  ┌────▼───┐                    │ │
│  │  │ ai          │ │  │  │Action 2│                    │ │
│  │  │ ...         │ │  │  └────────┘                    │ │
│  │  └─────────────┘ │  │                                 │ │
│  │                  │  │  ┌─────────┐                    │ │
│  │  🔗 Connections  │  │  │ Task 2 │                    │ │
│  │  (Drag to link)  │  │  └─────────┘                    │ │
│  └──────────────────┘  └─────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│  [Properties Panel] - Selected: Task 1                      │
│  ┌───────────────────────────────────────────────────────┐ │
│  │ Task Properties:                                       │ │
│  │   ID: task_1                                          │ │
│  │   User Input: {{userPrompt}}                          │ │
│  │   Dependencies: [task_0]                              │ │
│  │                                                        │ │
│  │ Actions:                                               │ │
│  │   [Action 1] [Action 2] [+ Add Action]                │ │
│  └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Komponenten

  • Tasks: Drag & Drop für neue Tasks
  • Actions: Gruppiert nach Methoden (outlook, sharepoint, ai, etc.)
  • Connections: Visueller Hinweis für Dependency-Verbindungen

2. Canvas (Mitte)

  • Graph-Editor: Visualisierung des Workflows
  • Nodes: Tasks (große Boxen) und Actions (kleinere Boxen innerhalb von Tasks)
  • Edges: Verbindungen zwischen Tasks (Dependencies) und Actions (Document-Flows)

3. Properties Panel (Unten)

  • Task Properties: Wenn Task selektiert
  • Action Properties: Wenn Action selektiert
  • Parameter Editor: Dynamisches Formular basierend auf WorkflowActionParameter Definitionen

Graphische Darstellung

Task-Node

┌─────────────────────────────────────┐
│  📋 Task 1                          │
│  ─────────────────────────────────  │
│  User Input: {{userPrompt}}        │
│                                     │
│  ┌─────────────┐  ┌─────────────┐ │
│  │ Action 1.1  │  │ Action 1.2  │ │
│  │ outlook.    │  │ sharepoint.  │ │
│  │ readEmails  │  │ uploadDoc    │ │
│  └─────────────┘  └─────────────┘ │
│                                     │
│  Dependencies: [Task 0]             │
└─────────────────────────────────────┘

Action-Node

┌─────────────────────────┐
│ ⚙️ outlook.readEmails   │
│ ─────────────────────── │
│ connectionRef: {{...}} │
│ query: "unread"         │
│ limit: 50               │
│                         │
│ Result Label: emails    │
│ ─────────────────────── │
│ [Edit] [Delete]         │
└─────────────────────────┘

Dependency-Verbindungen

Task Dependencies (gestrichelte Linie):

Task 0 ──┐
         │ (depends on)
         ▼
      Task 1

Document Flow (durchgezogene Linie mit Label):

Action 1.1 (emails) ──[docList:emails]──► Action 2.1

Datenmodell für Editor

WorkflowTemplateModel (neues Datenmodell)

class WorkflowTemplateModel(BaseModel):
    """Template-Modell für graphischen Editor"""
    
    # Metadaten
    name: str = Field(description="Template name")
    description: Optional[str] = Field(None, description="Template description")
    version: str = Field(default="1.0", description="Template version")
    
    # Graph-Struktur
    tasks: List[TaskTemplateModel] = Field(
        default_factory=list,
        description="List of task templates"
    )
    
    # Placeholders (werden aus Template extrahiert)
    placeholders: Dict[str, str] = Field(
        default_factory=dict,
        description="Placeholder definitions with default values"
    )
    
    # UI-Metadaten (für Editor)
    uiMetadata: Optional[Dict[str, Any]] = Field(
        None,
        description="UI metadata (node positions, zoom level, etc.)"
    )

class TaskTemplateModel(BaseModel):
    """Task-Template für Editor"""
    
    id: str = Field(description="Task ID (unique within template)")
    label: str = Field(description="Task label/name")
    userInput: str = Field(
        description="User input template (can contain placeholders like {{userPrompt}})"
    )
    dependencies: List[str] = Field(
        default_factory=list,
        description="List of task IDs this task depends on"
    )
    actions: List[ActionTemplateModel] = Field(
        default_factory=list,
        description="List of actions in this task"
    )
    
    # UI-Metadaten
    position: Optional[Dict[str, float]] = Field(
        None,
        description="Node position in editor: {x: number, y: number}"
    )

class ActionTemplateModel(BaseModel):
    """Action-Template für Editor"""
    
    id: str = Field(description="Action ID (unique within task)")
    method: str = Field(description="Method name (e.g., 'outlook', 'sharepoint')")
    action: str = Field(description="Action name (e.g., 'readEmails', 'uploadDocument')")
    parameters: Dict[str, Any] = Field(
        default_factory=dict,
        description="Action parameters (can contain placeholders and document references)"
    )
    resultLabel: Optional[str] = Field(
        None,
        description="Result label for document output (used in documentList references)"
    )
    
    # Document Dependencies (automatisch aus parameters extrahiert)
    documentDependencies: List[str] = Field(
        default_factory=list,
        description="List of result labels this action depends on (from documentList parameter)"
    )
    
    # UI-Metadaten
    position: Optional[Dict[str, float]] = Field(
        None,
        description="Node position within task: {x: number, y: number}"
    )

Konvertierung: Template ↔ Editor-Modell

AutomationDefinition.template → Editor-Modell

def parseTemplateToEditorModel(templateJson: str) -> WorkflowTemplateModel:
    """
    Parse AutomationDefinition.template (JSON string) to WorkflowTemplateModel
    
    INPUT: AutomationDefinition.template (JSON-String)
    OUTPUT: WorkflowTemplateModel (Editor-Modell)
    
    Template-Format (aktuell in AutomationDefinition.template):
    {
      "tasks": [
        {
          "id": "task_1",
          "userInput": "{{userPrompt}}",
          "dependencies": [],
          "actions": [
            {
              "method": "outlook",
              "action": "readEmails",
              "parameters": {
                "connectionReference": "{{connectionName}}",
                "query": "unread",
                "limit": 50
              },
              "resultLabel": "emails"
            }
          ]
        }
      ]
    }
    """
    templateData = json.loads(templateJson)
    
    tasks = []
    for taskData in templateData.get("tasks", []):
        actions = []
        for actionData in taskData.get("actions", []):
            # Extract document dependencies from documentList parameter
            documentDeps = []
            if "documentList" in actionData.get("parameters", {}):
                docList = actionData["parameters"]["documentList"]
                if isinstance(docList, list):
                    for ref in docList:
                        if isinstance(ref, str) and ref.startswith("docList:"):
                            # Extract label: docList:label or docList:messageId:label
                            parts = ref.split(":")
                            if len(parts) >= 2:
                                label = parts[-1]  # Last part is always the label
                                documentDeps.append(label)
            
            actions.append(ActionTemplateModel(
                id=actionData.get("id", f"action_{len(actions)}"),
                method=actionData["method"],
                action=actionData["action"],
                parameters=actionData.get("parameters", {}),
                resultLabel=actionData.get("resultLabel"),
                documentDependencies=documentDeps
            ))
        
        tasks.append(TaskTemplateModel(
            id=taskData["id"],
            label=taskData.get("label", taskData["id"]),
            userInput=taskData.get("userInput", ""),
            dependencies=taskData.get("dependencies", []),
            actions=actions
        ))
    
    # Extract placeholders from template
    placeholders = extractPlaceholders(templateJson)
    
    return WorkflowTemplateModel(
        name="Template",
        tasks=tasks,
        placeholders=placeholders
    )

Editor-Modell → AutomationDefinition.template

def convertEditorModelToTemplate(
    editorModel: WorkflowTemplateModel,
    placeholders: Dict[str, str]
) -> str:
    """
    Convert WorkflowTemplateModel to AutomationDefinition.template JSON string
    
    INPUT: WorkflowTemplateModel (Editor-Modell) + placeholders Dict
    OUTPUT: JSON-String für AutomationDefinition.template Feld
    """
    templateData = {
        "tasks": []
    }
    
    for task in editorModel.tasks:
        taskData = {
            "id": task.id,
            "userInput": task.userInput,
            "dependencies": task.dependencies,
            "actions": []
        }
        
        for action in task.actions:
            # Build documentList from documentDependencies
            parameters = action.parameters.copy()
            if action.documentDependencies:
                documentList = [f"docList:{label}" for label in action.documentDependencies]
                parameters["documentList"] = documentList
            
            actionData = {
                "method": action.method,
                "action": action.action,
                "parameters": parameters
            }
            
            if action.resultLabel:
                actionData["resultLabel"] = action.resultLabel
            
            taskData["actions"].append(actionData)
        
        templateData["tasks"].append(taskData)
    
    return json.dumps(templateData, indent=2)

UI-Implementierung

Technologie-Stack

Empfehlung:

  • Graph-Library: Cytoscape.js oder React Flow oder JointJS
  • Framework: Vanilla JavaScript (konsistent mit bestehendem Frontend) oder React (falls Migration geplant)
  • Styling: CSS mit bestehenden Styles aus formGeneric.js

HTML-Struktur

<!DOCTYPE html>
<html>
<head>
    <title>Workflow Editor</title>
    <link rel="stylesheet" href="/css/workflow-editor.css">
</head>
<body>
    <div id="workflow-editor" class="workflow-editor">
        <!-- Toolbar -->
        <div class="editor-toolbar">
            <button id="save-btn" class="btn btn-primary">
                <i class="fas fa-save"></i> Save Template
            </button>
            <button id="validate-btn" class="btn btn-secondary">
                <i class="fas fa-check"></i> Validate
            </button>
            <button id="preview-btn" class="btn btn-secondary">
                <i class="fas fa-eye"></i> Preview JSON
            </button>
        </div>
        
        <!-- Main Editor Area -->
        <div class="editor-main">
            <!-- Toolbox (Left) -->
            <div class="editor-toolbox" id="editor-toolbox">
                <div class="toolbox-section">
                    <h4>Tasks</h4>
                    <div class="toolbox-item" draggable="true" data-type="task">
                        <i class="fas fa-tasks"></i> Task
                    </div>
                </div>
                
                <div class="toolbox-section">
                    <h4>Actions</h4>
                    <div class="toolbox-group">
                        <h5>Outlook</h5>
                        <div class="toolbox-item" draggable="true" data-type="action" data-method="outlook" data-action="readEmails">
                            readEmails
                        </div>
                        <div class="toolbox-item" draggable="true" data-type="action" data-method="outlook" data-action="sendEmail">
                            sendEmail
                        </div>
                    </div>
                    <div class="toolbox-group">
                        <h5>SharePoint</h5>
                        <div class="toolbox-item" draggable="true" data-type="action" data-method="sharepoint" data-action="readDocuments">
                            readDocuments
                        </div>
                        <!-- ... weitere Actions ... -->
                    </div>
                </div>
            </div>
            
            <!-- Canvas (Center) -->
            <div class="editor-canvas" id="editor-canvas">
                <!-- Graph wird hier gerendert -->
            </div>
            
            <!-- Properties Panel (Right) -->
            <div class="editor-properties" id="editor-properties">
                <div class="properties-header">
                    <h4>Properties</h4>
                </div>
                <div class="properties-content" id="properties-content">
                    <p class="properties-empty">Select a node to edit properties</p>
                </div>
            </div>
        </div>
        
        <!-- Placeholders Panel (Bottom) -->
        <div class="editor-placeholders" id="editor-placeholders">
            <h4>Placeholders</h4>
            <div id="placeholders-list"></div>
        </div>
    </div>
    
    <script src="/js/workflow-editor.js"></script>
</body>
</html>

JavaScript-Architektur

// workflow-editor.js

class WorkflowEditor {
    constructor(containerId, initialTemplate = null) {
        this.container = document.getElementById(containerId);
        this.graph = null; // Graph-Library Instanz
        this.model = null; // WorkflowTemplateModel
        this.selectedNode = null;
        
        this.init();
        if (initialTemplate) {
            this.loadTemplate(initialTemplate);
        }
    }
    
    init() {
        this.initGraph();
        this.initToolbox();
        this.initPropertiesPanel();
        this.initEventHandlers();
    }
    
    initGraph() {
        // Initialize graph library (z.B. Cytoscape.js)
        this.graph = cytoscape({
            container: document.getElementById('editor-canvas'),
            elements: [],
            style: [
                {
                    selector: 'node[type="task"]',
                    style: {
                        'shape': 'roundrectangle',
                        'width': 200,
                        'height': 150,
                        'background-color': '#e8f4f8',
                        'label': 'data(label)',
                        'text-valign': 'top',
                        'text-margin-y': 10
                    }
                },
                {
                    selector: 'node[type="action"]',
                    style: {
                        'shape': 'roundrectangle',
                        'width': 150,
                        'height': 80,
                        'background-color': '#fff4e6',
                        'label': 'data(label)',
                        'text-valign': 'center'
                    }
                },
                {
                    selector: 'edge[type="task-dependency"]',
                    style: {
                        'line-style': 'dashed',
                        'target-arrow-shape': 'triangle',
                        'curve-style': 'bezier'
                    }
                },
                {
                    selector: 'edge[type="document-flow"]',
                    style: {
                        'line-style': 'solid',
                        'target-arrow-shape': 'triangle',
                        'label': 'data(label)',
                        'curve-style': 'bezier',
                        'line-color': '#4a90e2'
                    }
                }
            ],
            layout: {
                name: 'dagre',
                rankDir: 'TB',
                spacingFactor: 1.5
            }
        });
        
        // Event handlers
        this.graph.on('tap', 'node', (evt) => {
            this.selectNode(evt.target);
        });
        
        this.graph.on('tap', (evt) => {
            if (evt.target === this.graph) {
                this.deselectNode();
            }
        });
    }
    
    loadTemplate(templateJson) {
        // Parse template to editor model
        this.model = parseTemplateToEditorModel(templateJson);
        
        // Render graph
        this.renderGraph();
        
        // Update placeholders panel
        this.updatePlaceholdersPanel();
    }
    
    renderGraph() {
        const elements = [];
        
        // Add task nodes
        this.model.tasks.forEach((task, taskIndex) => {
            // Task node
            elements.push({
                data: {
                    id: `task_${task.id}`,
                    type: 'task',
                    label: task.label || task.id,
                    taskId: task.id,
                    nodeType: 'task'
                },
                position: task.position || { x: 100, y: taskIndex * 200 + 100 }
            });
            
            // Action nodes within task
            task.actions.forEach((action, actionIndex) => {
                const actionNodeId = `action_${task.id}_${action.id}`;
                elements.push({
                    data: {
                        id: actionNodeId,
                        type: 'action',
                        label: `${action.method}.${action.action}`,
                        taskId: task.id,
                        actionId: action.id,
                        nodeType: 'action'
                    },
                    position: action.position || {
                        x: 100 + actionIndex * 180,
                        y: taskIndex * 200 + 150
                    }
                });
                
                // Parent-child relationship (action belongs to task)
                elements.push({
                    data: {
                        id: `edge_${task.id}_${action.id}`,
                        source: `task_${task.id}`,
                        target: actionNodeId,
                        type: 'contains'
                    }
                });
            });
        });
        
        // Add task dependencies
        this.model.tasks.forEach(task => {
            task.dependencies.forEach(depId => {
                elements.push({
                    data: {
                        id: `dep_${depId}_${task.id}`,
                        source: `task_${depId}`,
                        target: `task_${task.id}`,
                        type: 'task-dependency'
                    }
                });
            });
        });
        
        // Add document flow edges
        this.model.tasks.forEach(task => {
            task.actions.forEach(action => {
                action.documentDependencies.forEach(depLabel => {
                    // Find source action with matching resultLabel
                    const sourceAction = this.findActionByResultLabel(depLabel);
                    if (sourceAction) {
                        elements.push({
                            data: {
                                id: `doc_${sourceAction.id}_${action.id}`,
                                source: `action_${sourceAction.taskId}_${sourceAction.id}`,
                                target: `action_${task.id}_${action.id}`,
                                type: 'document-flow',
                                label: depLabel
                            }
                        });
                    }
                });
            });
        });
        
        this.graph.elements().remove();
        this.graph.add(elements);
        this.graph.layout({ name: 'dagre' }).run();
    }
    
    selectNode(node) {
        this.selectedNode = node;
        const nodeData = node.data();
        
        if (nodeData.nodeType === 'task') {
            this.showTaskProperties(nodeData.taskId);
        } else if (nodeData.nodeType === 'action') {
            this.showActionProperties(nodeData.taskId, nodeData.actionId);
        }
        
        // Highlight selected node
        this.graph.elements().removeClass('selected');
        node.addClass('selected');
    }
    
    showActionProperties(taskId, actionId) {
        const task = this.model.tasks.find(t => t.id === taskId);
        const action = task?.actions.find(a => a.id === actionId);
        
        if (!action) return;
        
        // Load action schema from API
        this.loadActionSchema(action.method, action.action).then(schema => {
            const propertiesHtml = this.renderActionPropertiesForm(action, schema);
            document.getElementById('properties-content').innerHTML = propertiesHtml;
            
            // Bind form handlers
            this.bindActionFormHandlers(taskId, actionId, schema);
        });
    }
    
    async loadActionSchema(method, actionName) {
        // Fetch from /api/workflows/actions/{method}/{actionName}
        const response = await fetch(`/api/workflows/actions/${method}/${actionName}`);
        return await response.json();
    }
    
    renderActionPropertiesForm(action, schema) {
        let html = `
            <div class="action-properties-form">
                <h5>${action.method}.${action.action}</h5>
                <div class="form-group">
                    <label>Result Label</label>
                    <input type="text" id="action-result-label" 
                           value="${action.resultLabel || ''}" 
                           placeholder="e.g., emails_found">
                    <small>Label for document output (used in documentList references)</small>
                </div>
                <div class="form-group">
                    <label>Parameters</label>
        `;
        
        // Render parameter fields based on schema
        Object.entries(schema.parameters || {}).forEach(([paramName, paramDef]) => {
            html += this.renderParameterField(paramName, paramDef, action.parameters[paramName]);
        });
        
        html += `
                </div>
                <div class="form-actions">
                    <button class="btn btn-primary" onclick="editor.saveActionProperties('${action.id}')">
                        Save
                    </button>
                    <button class="btn btn-danger" onclick="editor.deleteAction('${action.id}')">
                        Delete
                    </button>
                </div>
            </div>
        `;
        
        return html;
    }
    
    renderParameterField(paramName, paramDef, currentValue) {
        const value = currentValue !== undefined ? currentValue : (paramDef.default || '');
        const valueStr = typeof value === 'object' ? JSON.stringify(value) : String(value);
        
        let html = `<div class="parameter-field">`;
        html += `<label>${paramName} ${paramDef.required ? '<span class="required">*</span>' : ''}</label>`;
        
        switch (paramDef.frontendType) {
            case 'text':
            case 'textarea':
                html += `<textarea class="form-control" 
                          id="param-${paramName}" 
                          ${paramDef.required ? 'required' : ''}
                          placeholder="${paramDef.description || ''}">${valueStr}</textarea>`;
                break;
            case 'number':
                html += `<input type="number" class="form-control" 
                          id="param-${paramName}" 
                          value="${valueStr}"
                          ${paramDef.required ? 'required' : ''}>`;
                break;
            case 'select':
            case 'userConnection':
            case 'documentReference':
                html += `<select class="form-control" id="param-${paramName}">`;
                if (!paramDef.required) {
                    html += `<option value="">-- None --</option>`;
                }
                // Options werden dynamisch geladen
                html += `</select>`;
                break;
            case 'checkbox':
                html += `<input type="checkbox" class="form-control" 
                          id="param-${paramName}" 
                          ${value ? 'checked' : ''}>`;
                break;
            default:
                html += `<input type="text" class="form-control" 
                          id="param-${paramName}" 
                          value="${valueStr}">`;
        }
        
        html += `<small class="form-text text-muted">${paramDef.description || ''}</small>`;
        html += `</div>`;
        
        return html;
    }
    
    saveActionProperties(actionId) {
        const task = this.model.tasks.find(t => 
            t.actions.some(a => a.id === actionId)
        );
        const action = task?.actions.find(a => a.id === actionId);
        
        if (!action) return;
        
        // Collect parameter values from form
        const parameters = {};
        // ... collect from form fields ...
        
        // Update action
        action.parameters = parameters;
        action.resultLabel = document.getElementById('action-result-label').value;
        
        // Update document dependencies
        action.documentDependencies = this.extractDocumentDependencies(parameters);
        
        // Re-render graph to show updated connections
        this.renderGraph();
    }
    
    extractDocumentDependencies(parameters) {
        const deps = [];
        if (parameters.documentList && Array.isArray(parameters.documentList)) {
            parameters.documentList.forEach(ref => {
                if (typeof ref === 'string' && ref.startsWith('docList:')) {
                    const parts = ref.split(':');
                    if (parts.length >= 2) {
                        deps.push(parts[parts.length - 1]); // Last part is label
                    }
                }
            });
        }
        return deps;
    }
    
    saveTemplate() {
        // Convert editor model to template JSON
        const templateJson = convertEditorModelToTemplate(
            this.model,
            this.getPlaceholders()
        );
        
        // Save via API to AutomationDefinition (Template-Objekt)
        // WICHTIG: Dies speichert das Template, nicht das ausführbare ChatWorkflow
        return this.saveToAutomationDefinition(templateJson);
    }
    
    validateTemplate() {
        const errors = [];
        
        // Check: All tasks have at least one action
        this.model.tasks.forEach(task => {
            if (task.actions.length === 0) {
                errors.push(`Task "${task.id}" has no actions`);
            }
        });
        
        // Check: All document dependencies resolve to valid result labels
        this.model.tasks.forEach(task => {
            task.actions.forEach(action => {
                action.documentDependencies.forEach(depLabel => {
                    const sourceAction = this.findActionByResultLabel(depLabel);
                    if (!sourceAction) {
                        errors.push(`Action "${action.method}.${action.action}" depends on unknown result label "${depLabel}"`);
                    }
                });
            });
        });
        
        // Check: Circular dependencies
        const circularDeps = this.detectCircularDependencies();
        if (circularDeps.length > 0) {
            errors.push(`Circular dependencies detected: ${circularDeps.join(', ')}`);
        }
        
        // Check: Required parameters are set
        // ... weitere Validierungen ...
        
        return {
            valid: errors.length === 0,
            errors: errors
        };
    }
    
    findActionByResultLabel(resultLabel) {
        for (const task of this.model.tasks) {
            for (const action of task.actions) {
                if (action.resultLabel === resultLabel) {
                    return { ...action, taskId: task.id };
                }
            }
        }
        return null;
    }
    
    detectCircularDependencies() {
        // Graph-based cycle detection
        // ... Implementation ...
        return [];
    }
}

// Initialize editor
let editor;
document.addEventListener('DOMContentLoaded', () => {
    const templateJson = getInitialTemplate(); // From formAutomations.js
    editor = new WorkflowEditor('workflow-editor', templateJson);
});

Integration mit formAutomations.js

Erweiterte Automation-Form

// In formAutomations.js

// Add "Edit Template" button to automation form
function initAutomationForm() {
    // ... existing code ...
    
    // Add workflow editor button
    const templateField = form.querySelector('#entity-template');
    if (templateField) {
        const editorButton = document.createElement('button');
        editorButton.type = 'button';
        editorButton.className = 'btn btn-secondary';
        editorButton.innerHTML = '<i class="fas fa-project-diagram"></i> Open Workflow Editor';
        editorButton.addEventListener('click', () => {
            openWorkflowEditor(templateField.value);
        });
        templateField.parentNode.appendChild(editorButton);
    }
}

function openWorkflowEditor(templateJson) {
    // Open workflow editor in modal or new tab
    const modal = document.createElement('div');
    modal.className = 'workflow-editor-modal';
    modal.innerHTML = `
        <div class="workflow-editor-modal-content">
            <div class="workflow-editor-modal-header">
                <h3>Workflow Editor</h3>
                <button class="workflow-editor-modal-close">&times;</button>
            </div>
            <div class="workflow-editor-modal-body">
                <div id="workflow-editor-container"></div>
            </div>
            <div class="workflow-editor-modal-footer">
                <button class="btn btn-primary" id="workflow-editor-save">Save & Close</button>
                <button class="btn btn-secondary" id="workflow-editor-cancel">Cancel</button>
            </div>
        </div>
    `;
    
    document.body.appendChild(modal);
    
    // Initialize editor
    const editor = new WorkflowEditor('workflow-editor-container', templateJson);
    
    // Save handler
    document.getElementById('workflow-editor-save').addEventListener('click', () => {
        const updatedTemplate = editor.saveTemplate();
        // Update template field in form
        document.querySelector('#entity-template').value = updatedTemplate;
        modal.remove();
    });
    
    // Cancel handler
    document.getElementById('workflow-editor-cancel').addEventListener('click', () => {
        modal.remove();
    });
}

API-Endpunkte

GET /api/workflows/actions

Liefert alle verfügbaren Actions (bereits im RBAC-Konzept definiert).

GET /api/workflows/actions/{method}/{action}

Liefert Action-Schema mit Parameter-Definitionen:

{
  "method": "outlook",
  "action": "readEmails",
  "actionId": "outlook.readEmails",
  "description": "Read emails from Outlook mailbox",
  "parameters": {
    "connectionReference": {
      "name": "connectionReference",
      "type": "str",
      "frontendType": "userConnection",
      "frontendOptions": "user.connection",
      "required": true,
      "description": "Microsoft connection label"
    },
    "query": {
      "name": "query",
      "type": "str",
      "frontendType": "text",
      "required": false,
      "description": "Search query for emails"
    }
  }
}

GET /api/automations/{automationId}

Liefert AutomationDefinition für Editor.

Zweck: Lädt AutomationDefinition (Template) für Bearbeitung im Editor Rückgabe: AutomationDefinition Objekt mit template (JSON-String) und placeholders

POST /api/automations/{automationId}

Speichert AutomationDefinition (Template).

Zweck: Speichert bearbeitetes Template zurück in AutomationDefinition Input: AutomationDefinition Objekt mit aktualisiertem template (JSON-String) und placeholders Hinweis: Dies ist das Template-Objekt, nicht das ausführbare ChatWorkflow-Objekt

Validierung

Template-Validierung

  1. Struktur-Validierung:

    • Alle Tasks haben mindestens eine Action
    • Alle Task-Dependencies verweisen auf existierende Tasks
    • Keine zirkulären Dependencies
  2. Action-Validierung:

    • Alle Actions haben gültige method/action Kombinationen
    • Alle required Parameters sind gesetzt
    • Alle documentList Referenzen verweisen auf existierende resultLabels
  3. Placeholder-Validierung:

    • Alle Placeholders im Template sind in placeholders-Dict definiert
    • Placeholder-Format: {{PLACEHOLDER_NAME}}

Beispiel-Workflow

Visuelle Darstellung

┌─────────────────────────────────────┐
│  📋 Task 1: Read Emails             │
│  ─────────────────────────────────  │
│  User Input: {{userPrompt}}         │
│                                     │
│  ┌───────────────────────────────┐ │
│  │ ⚙️ outlook.readEmails         │ │
│  │ connectionRef: {{conn}}       │ │
│  │ query: "unread"                │ │
│  │ Result: emails                 │ │
│  └───────────────┬───────────────┘ │
└──────────────────┼─────────────────┘
                   │ docList:emails
                   ▼
┌─────────────────────────────────────┐
│  📋 Task 2: Process Documents        │
│  ─────────────────────────────────  │
│  Dependencies: [Task 1]             │
│                                     │
│  ┌───────────────────────────────┐ │
│  │ ⚙️ ai.process                 │ │
│  │ documentList: [docList:emails]│ │
│  │ aiPrompt: {{processPrompt}}   │ │
│  │ Result: processed             │ │
│  └───────────────────────────────┘ │
└─────────────────────────────────────┘

Generiertes Template JSON

{
  "tasks": [
    {
      "id": "task_1",
      "userInput": "{{userPrompt}}",
      "dependencies": [],
      "actions": [
        {
          "method": "outlook",
          "action": "readEmails",
          "parameters": {
            "connectionReference": "{{conn}}",
            "query": "unread",
            "limit": 50
          },
          "resultLabel": "emails"
        }
      ]
    },
    {
      "id": "task_2",
      "userInput": "Process emails",
      "dependencies": ["task_1"],
      "actions": [
        {
          "method": "ai",
          "action": "process",
          "parameters": {
            "documentList": ["docList:emails"],
            "aiPrompt": "{{processPrompt}}",
            "resultType": "json"
          },
          "resultLabel": "processed"
        }
      ]
    }
  ]
}

Nächste Schritte

  1. Graph-Library auswählen: Cytoscape.js vs. React Flow vs. JointJS
  2. Editor-Komponenten implementieren: Toolbox, Canvas, Properties Panel
  3. API-Integration: Action-Schemas laden, Templates speichern
  4. Validierung implementieren: Dependency-Checking, Parameter-Validierung
  5. Placeholder-Management: UI für Placeholder-Definition
  6. Testing: Unit-Tests für Konvertierungs-Logik

Offene Fragen

  1. Graph-Library: Welche Library bevorzugt? (Cytoscape.js ist gut für komplexe Graphen, React Flow ist moderner)
  2. Positionierung: Sollen Node-Positionen gespeichert werden oder automatisch layouted?
  3. Placeholder-UI: Separate UI für Placeholder-Management oder integriert in Editor?
  4. Versionierung: Sollen Templates versioniert werden?