# 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 ```python 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 ```python 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 ```python 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 ```python 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**: ```json { "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 #### 1. Toolbox (Links) - **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) ```python 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 ```python 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 ```python 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](https://js.cytoscape.org/) oder [React Flow](https://reactflow.dev/) oder [JointJS](https://www.jointjs.com/) - **Framework**: Vanilla JavaScript (konsistent mit bestehendem Frontend) oder React (falls Migration geplant) - **Styling**: CSS mit bestehenden Styles aus `formGeneric.js` ### HTML-Struktur ```html Workflow Editor

Tasks

Task

Actions

Outlook
readEmails
sendEmail
SharePoint
readDocuments

Properties

Select a node to edit properties

Placeholders

``` ### JavaScript-Architektur ```javascript // 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 = `
${action.method}.${action.action}
Label for document output (used in documentList references)
`; // Render parameter fields based on schema Object.entries(schema.parameters || {}).forEach(([paramName, paramDef]) => { html += this.renderParameterField(paramName, paramDef, action.parameters[paramName]); }); html += `
`; 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 = `
`; html += ``; switch (paramDef.frontendType) { case 'text': case 'textarea': html += ``; break; case 'number': html += ``; break; case 'select': case 'userConnection': case 'documentReference': html += ``; break; case 'checkbox': html += ``; break; default: html += ``; } html += `${paramDef.description || ''}`; html += `
`; 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 ```javascript // 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 = ' 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 = `

Workflow Editor

`; 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: ```json { "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 ```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?