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(indatamodelChat.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)
- Hat
- Ausführung:
- Kann über Route mit
startoderstopausgeführt werden - Wird automatisch (scheduled) oder dynamisch (on-demand) gestartet
- Kann über Route mit
- Lebenszyklus: Wird zur Laufzeit erstellt und ausgeführt
AutomationDefinition (Template-Objekt)
- Klasse:
AutomationDefinition(indatamodelChat.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)
- Hat
- 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.templatekonvertiert
- Wird aus
- 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:
AutomationDefinitionmittemplate: '{"tasks": [...]}'undplaceholders: {"connectionName": "MyConn"}- Template wird mit Placeholders gefüllt →
ChatWorkflowwird erstellt ChatWorkflowwird gestartet → Tasks werden ausgeführt → Actions werden ausgeführt
Zielsetzung
- Visuelle Modellierung: Workflows mit Tasks und Actions graphisch erstellen
- Parameter-Konfiguration: Action-Parameter direkt im Editor setzen
- Dependency-Management: Document-Dependencies zwischen Actions visuell darstellen
- 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:
AutomationDefinitionist das Template-Objekt, das im Workflow-Editor bearbeitet wirdtemplateFeld enthält JSON-String mit Workflow-Struktur (Tasks und Actions als Vorlage)placeholdersenthä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 ActiondocList: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
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
WorkflowActionParameterDefinitionen
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">×</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
-
Struktur-Validierung:
- Alle Tasks haben mindestens eine Action
- Alle Task-Dependencies verweisen auf existierende Tasks
- Keine zirkulären Dependencies
-
Action-Validierung:
- Alle Actions haben gültige method/action Kombinationen
- Alle required Parameters sind gesetzt
- Alle documentList Referenzen verweisen auf existierende resultLabels
-
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
- Graph-Library auswählen: Cytoscape.js vs. React Flow vs. JointJS
- Editor-Komponenten implementieren: Toolbox, Canvas, Properties Panel
- API-Integration: Action-Schemas laden, Templates speichern
- Validierung implementieren: Dependency-Checking, Parameter-Validierung
- Placeholder-Management: UI für Placeholder-Definition
- Testing: Unit-Tests für Konvertierungs-Logik
Offene Fragen
- Graph-Library: Welche Library bevorzugt? (Cytoscape.js ist gut für komplexe Graphen, React Flow ist moderner)
- Positionierung: Sollen Node-Positionen gespeichert werden oder automatisch layouted?
- Placeholder-UI: Separate UI für Placeholder-Management oder integriert in Editor?
- Versionierung: Sollen Templates versioniert werden?