1219 lines
45 KiB
Markdown
1219 lines
45 KiB
Markdown
# 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
|
|
<!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
|
|
|
|
```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 = `
|
|
<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
|
|
|
|
```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 = '<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:
|
|
|
|
```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?
|
|
|