wiki/c-work/1-plan/2026-04-analysis-editor.md

398 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- status: draft -->
<!-- lastReviewed: 2026-04-07 -->
# Analyse: Typed Node Handover System für den Graphical Editor
## 1 Problemstellung
Der Graphical Editor besitzt heute **keine formale Typisierung** der Daten, die zwischen Nodes fließen. Jede Node produziert ein freiformiges Dict; die Folge-Node versucht heuristisch, daraus das Passende zu extrahieren. Das führt zu:
| Symptom | Wo sichtbar |
|---------|-------------|
| Spezialcode pro Paar (z.B. `_extractEmailContentFromUpstream`, `_getContextFromUpstream`, `_gatherAttachmentDocumentsFromUpstream`) | `actionNodeExecutor.py` (~860 Zeilen, >50 % handover Heuristik) |
| DataPicker zeigt **hartcodierte** Beispiel-Schemas (`outputPreviewRegistry.ts`) — stimmt oft nicht mit echten Laufzeit-Outputs überein | Frontend `outputPreviewRegistry.ts` |
| Kein `data.transform`, `data.filter`, `data.aggregate` implementiert — nur in `automation.md` als Placeholder aufgelistet | `nodeDefinitions/` hat keine Datei dafür |
| Kein „Aggregate" (Gegenstück zu `flow.loop`'s For-Each) | Engine sammelt Loop-Body-Resultate nicht auf |
| Frontend-Parameter haben **eigene** Typdefinitionen (node-level `type: "string"`) statt der in `MethodBase` etablierten `WorkflowActionParameter` mit `FrontendType`-Enum + Validierung | `ai.py`, `email.py` etc. vs. `methodBase.py` |
**Kern:** Die Method/Action-Schicht hat bereits eine saubere Parameter-Typisierung (`WorkflowActionParameter`, `_validateType`, `_validateParameters`, `FrontendType`). Aber der Graph-Editor nutzt sie weder für Input-Konfiguration noch für die Handover-Logik.
---
## 2 Ist-Zustand (Layer für Layer)
### 2.1 Node-Definitionen (`nodeDefinitions/*.py`)
Jede Node ist ein Dict mit:
```python
{
"id": "email.draftEmail",
"parameters": [
{"name": "subject", "type": "string", "required": True, ...},
],
"inputs": 1, "outputs": 1,
"_method": "outlook", "_action": "composeAndDraftEmailWithContext",
"_paramMap": {"connectionId": "connectionReference", ...},
}
```
**Was fehlt:**
- Kein `outputSchema` — was liefert die Node zurück?
- Kein `inputSchema` — was erwartet die Node am Eingang?
- `type: "string"` ist Node-Editor-intern, nicht identisch mit `WorkflowActionParameter.type` (`str`, `int`, `List[str]`, …).
- Kein `frontendType` (d.h. Frontend baut eigene Config-Components per Node-Typ statt generisch).
### 2.2 Execution Engine (`executionEngine.py`)
- Topologische Sortierung → iteriert Nodes → ruft Executor auf → speichert Output in `nodeOutputs[nodeId]`.
- **Kein Output-Vertrag**: der Executor liefert `Any`, das Dict wird 1:1 in `nodeOutputs` gelegt.
- Folge-Nodes holen via `inputSources[nodeId][0]` den Vorgänger-Output und _hoffen_, dass er das richtige Format hat.
### 2.3 ActionNodeExecutor (`actionNodeExecutor.py`)
- ~860 Zeilen, davon ~60 % dedizierte Merge-Logik für Paare: `email→AI`, `AI→email.draftEmail`, `file.create` content-gathering, ClickUp merge, etc.
- Aufruft am Ende `ActionExecutor.executeAction(method, action, params)` — dessen `MethodBase._validateParameters` prüft die **Action**-Parameter, nicht den Graph-Port-Kontrakt.
- Baut ein Output-Dict mit teilweise inkonsistenten Keys (`documents` vs. `documentList` vs. `data` vs. `context`).
### 2.4 DataPicker + outputPreviewRegistry (Frontend)
- `outputPreviewRegistry.ts` registriert **statische Beispiele** pro Node-Typ.
- DataPicker zeigt daraus einen Baum und erzeugt `DataRef { type: "ref", nodeId, path }`.
- `resolveParameterReferences` (Backend) löst `DataRef` gegen echte `nodeOutputs`.
- **Problem:** Die Preview-Struktur ist manuell gepflegt und **divergiert** vom echten Output. Der User wählt Pfade, die zur Laufzeit nicht existieren oder anders heißen.
### 2.5 Method/Action Parameter-System (bereits existent)
`WorkflowActionParameter` + `MethodBase._validateParameters`:
```python
class WorkflowActionParameter(BaseModel):
name: str
type: str # 'str', 'int', 'List[str]', 'Dict[str, Any]'
frontendType: FrontendType # TEXT, TEXTAREA, SELECT, USER_CONNECTION, …
frontendOptions: Optional[...]
required: bool
default: Optional[Any]
validation: Optional[Dict]
```
`_validateType` konvertiert + prüft: `str→str`, `int→int`, `List[str]→[str,…]`, usw.
**Dieses System funktioniert bereits** für Workspace-Actions. Es wird aber **nicht** für Node-Ein-/Ausgänge genutzt.
---
## 3 Ziel-Architektur: Typed Port System
### 3.1 Kern-Konzept: Port-Schema
Jede Node deklariert **typisierte Ports** für Ein- und Ausgänge — analog zu `WorkflowActionParameter`, aber für den Graphen.
```
┌──────────────────────────────────────────────────────────────────────┐
│ Node Definition (Beispiel: email.draftEmail) │
│ │
│ inputPorts: │
│ [0]: { schema: EmailDraft } ← structured type │
│ EmailDraft = { subject: str, body: str, to: List[str] } │
│ │
│ outputPorts: │
│ [0]: { schema: ActionResult } │
│ ActionResult = { success: bool, error: str?, │
│ documents: List[Document] } │
│ │
│ parameters: (config, unabhängig von Ports) │
│ connectionId: { type: str, frontendType: USER_CONNECTION } │
└──────────────────────────────────────────────────────────────────────┘
```
### 3.2 Port-Schema Definition
```python
class PortField(BaseModel):
name: str
type: str # str, int, bool, List[str], List[Document], Dict[str,Any], …
description: Dict[str, str] # {en, de, fr}
required: bool = True
class PortSchema(BaseModel):
fields: List[PortField]
class NodePortDefinition(BaseModel):
inputPorts: Dict[int, PortSchema] # port-index → schema
outputPorts: Dict[int, PortSchema] # port-index → schema
```
### 3.3 Einheitliche Output-Typen (Port-Typen-Katalog)
Statt freier Dicts gibt es benannte Schemas, die Node-übergreifend wiederverwendbar sind:
| Port-Typ | Felder | Produziert von | Konsumiert von |
|----------|--------|----------------|----------------|
| `DocumentList` | `documents: List[{name, data, mimeType, metadata}]` | sharepoint.readFile, sharepoint.downloadFile, ai.*, file.create, input.upload | ai.*, file.create, email.draftEmail, sharepoint.uploadFile |
| `FileList` | `files: List[{url, name, path, size}]` | sharepoint.listFiles, sharepoint.findFile | sharepoint.readFile, sharepoint.downloadFile, flow.loop |
| `EmailDraft` | `subject: str, body: str, to: List[str], cc?: List[str], attachments?: DocumentList` | ai.prompt (mode=email) | email.draftEmail, email.sendEmail |
| `EmailList` | `emails: List[{subject, from, to, body, date, attachments}]` | email.checkEmail, email.searchEmail | ai.prompt, flow.loop, flow.ifElse |
| `TaskList` | `tasks: List[{id, name, status, url, …}]` | clickup.searchTasks, clickup.listTasks | flow.loop, clickup.updateTask |
| `TaskResult` | `success: bool, taskId: str, task: {id, name, status}` | clickup.createTask, clickup.updateTask | flow.ifElse |
| `FormPayload` | `payload: Dict[str, Any]` (dynamisch, Keys = Feldnamen) | trigger.form, input.form | Alle (via Referenz auf Einzelfelder) |
| `AiResult` | `prompt: str, response: str, context: str, documents: List[Document]` | ai.* | email.draftEmail, file.create, sharepoint.uploadFile |
| `BoolResult` | `result: bool, reason?: str` | input.approval, input.confirmation | flow.ifElse |
| `TextResult` | `text: str` | input.comment | ai.*, file.create |
| `LoopItem` | `currentItem: Any, currentIndex: int, items: List[Any], count: int` | flow.loop (pro Iteration) | Loop-Body Nodes |
| `AggregateResult` | `items: List[Any], count: int` | **data.aggregate** (NEU) | Alle |
### 3.4 Schema auf Node-Definitions
Erweiterung der bestehenden Node-Definitions um `outputPorts` und `inputPorts`:
```python
{
"id": "ai.prompt",
"category": "ai",
"parameters": [...], # Config — bleibt wie bisher
"inputs": 1,
"outputs": 1,
# NEU ─────────────────────────────────────────
"inputPorts": {
0: {"accepts": ["DocumentList", "TextResult", "FormPayload", "EmailList", "AiResult", "Any"]},
},
"outputPorts": {
0: {"schema": "AiResult"},
},
}
```
```python
{
"id": "email.draftEmail",
"inputPorts": {
0: {"accepts": ["EmailDraft", "AiResult", "TextResult"]},
},
"outputPorts": {
0: {"schema": "ActionResult"},
},
}
```
### 3.5 Konsequenzen für bestehende Schichten
#### A) Node-Definitionen — einmalige Migration
Jede Node bekommt `inputPorts` + `outputPorts`. Bestehende `inputs`/`outputs` (Zählwerte) bleiben für Rückwärtskompatibilität, aber die Schemas werden die Quelle der Wahrheit.
#### B) Executors — Output normalisieren
Jeder Executor erhält eine `_normalizeOutput(rawResult, portSchema) → Dict` Funktion. Sie sorgt dafür, dass die Keys/Typen dem deklarierten Schema entsprechen. Für `AiResult` z.B.:
```python
def _normalizeAiResult(raw: Any) -> Dict:
return {
"prompt": raw.get("prompt", ""),
"response": raw.get("response", raw.get("context", "")),
"context": raw.get("context", ""),
"documents": _ensureDocumentList(raw.get("documents", [])),
}
```
Der Spezialcode in `actionNodeExecutor.py` (~400 Zeilen Paar-Heuristik) fällt dann weg und wird durch generische Normalizer pro Port-Typ ersetzt.
#### C) ExecutionEngine — Handover-Validierung
Nach jedem Node-Execute: `nodeOutputs[nodeId]` wird gegen `outputPorts[0].schema` geprüft (Warnung oder Fehler bei Mismatch). Vor jedem Node-Execute: Input-Port-Kompatibilitäts-Check (`accepts` enthält den Schema-Typ des Vorgänger-Outputs).
#### D) Frontend — DataPicker aus Schema generieren
`outputPreviewRegistry.ts` wird ersetzt durch eine generische Funktion, die aus dem PortSchema den Preview-Baum baut. Kein manuelles Pflegen mehr:
```typescript
function buildPreviewFromSchema(schema: PortSchema): Record<string, unknown> {
// Iteriert schema.fields → Beispielwert pro Typ
}
```
#### E) Frontend — Generisches Parameter-Rendering
Statt pro Node-Typ eine eigene Config-Component zu schreiben (aktuell 12+ Dateien), können die meisten Parameter-Felder generisch aus dem `WorkflowActionParameter`-Schema gerendert werden. Spezial-UIs (FormBuilder, ClickUp Browse, etc.) bleiben als Override.
#### F) Verbindungs-Validierung im Editor
Beim Ziehen einer Kante: prüfe `sourceNode.outputPorts[outputIdx].schema``targetNode.inputPorts[inputIdx].accepts`. Falls inkompatibel: Kante rot markieren / verhindern.
---
## 4 Fehlende Nodes / Konzepte
### 4.1 data.aggregate (Gegenstück zu flow.loop)
Loop verteilt Items → Body → aber am Ende gibt es keinen Collector. Heute liegt nur das letzte Body-Ergebnis in `nodeOutputs`.
**Lösung: `data.aggregate`-Node**
```python
{
"id": "data.aggregate",
"category": "data",
"label": {"en": "Aggregate", "de": "Sammeln", "fr": "Agréger"},
"description": {"en": "Collect results from loop body into a list", ...},
"parameters": [
{"name": "mode", "type": "string", "options": ["collect", "concat", "sum", "count"], "default": "collect"},
],
"inputs": 1, "outputs": 1,
"inputPorts": {0: {"accepts": ["Any"]}},
"outputPorts": {0: {"schema": "AggregateResult"}},
"executor": "data",
}
```
**Engine-Änderung:** Im Loop-Body erkennt die Engine eine `data.aggregate`-Node und sammelt pro Iteration das Ergebnis in einer Liste. Nach Loop-Ende: `nodeOutputs[aggregateNodeId] = { items: [...], count: N }`.
### 4.2 data.transform
Reiner Mapping-Node: Felder umbenennen, extrahieren, umstrukturieren. Konfiguration per Key-Value-Mapping oder einfachem Expression-Sprache.
```python
{
"id": "data.transform",
"category": "data",
"parameters": [
{"name": "mapping", "type": "json", "description": "Key-Value Mapping: {outputField: inputRef}"},
],
"inputPorts": {0: {"accepts": ["Any"]}},
"outputPorts": {0: {"schema": "Dict"}},
}
```
### 4.3 data.filter
Filtert Items einer Liste nach Bedingung (wie WHERE in SQL). Eingabe: List, Ausgabe: gefilterter List.
```python
{
"id": "data.filter",
"category": "data",
"parameters": [
{"name": "condition", "type": "string", "description": "Filter expression (z.B. item.status == 'open')"},
],
"inputPorts": {0: {"accepts": ["AggregateResult", "FileList", "TaskList", "EmailList"]}},
"outputPorts": {0: {"schema": "AggregateResult"}},
}
```
### 4.4 flow.merge
Heute in `automation.md` gelistet, aber nicht implementiert. Wartet auf N Eingänge und kombiniert sie.
---
## 5 Migrations-Strategie
### Phase 1: Port-Schema-Deklaration (Backend, kein Breaking Change)
1. `PortSchema` als Pydantic-Modell definieren.
2. Katalog der Port-Typen (`DocumentList`, `AiResult`, `EmailDraft`, …) anlegen.
3. Jede Node-Definition in `nodeDefinitions/*.py` um `inputPorts` / `outputPorts` erweitern.
4. API `GET /node-types` liefert die Schemas mit.
### Phase 2: Output-Normalizer (Backend)
1. Pro Port-Typ einen Normalizer schreiben (1015 Funktionen).
2. In `executionEngine.py`: nach jedem Execute `_normalizeOutput(result, schema)` aufrufen.
3. Spezialcode in `actionNodeExecutor.py` schrittweise durch Normalizer ersetzen.
4. Step-Logs mit normalisiertem Output (Debugging/Tracing sofort besser).
### Phase 3: Frontend — DataPicker aus Schema (Frontend)
1. `outputPreviewRegistry.ts` durch Schema-basierte Preview-Generierung ersetzen.
2. DataPicker zeigt korrekte Felder + Typen an.
3. Verbindungs-Validierung (rote Kanten bei Typ-Mismatch).
### Phase 4: Generisches Parameter-Rendering (Frontend)
1. `NodeConfigPanel` rendert Parameter generisch aus `WorkflowActionParameter`-Schema.
2. Spezial-Components (`FormNodeConfig`, `ClickUpNodeConfig`, …) bleiben als Override für komplexe UIs.
3. Neue Nodes brauchen **keine eigene Config-Component** mehr.
### Phase 5: Neue Data-Nodes
1. `data.aggregate` implementieren (Engine + Node-Definition + Frontend).
2. `data.transform` implementieren.
3. `data.filter` implementieren.
4. `flow.merge` implementieren.
---
## 6 Konkretes Beispiel: AI-Mail-Entwurf → Mail-Versand
### Heute (heuristisch)
```
[email.checkEmail] ──→ [ai.prompt] ──→ [email.draftEmail]
↓ ↓ ↓
freeform Dict freeform Dict ~100 Zeilen Spezialcode
(data.emails…) (context, docs) in actionNodeExecutor:
_extractEmailContentFromUpstream
_getIncomingEmailFromUpstream
_unpackIncomingEmail
_gatherAttachmentDocumentsFromUpstream
```
### Ziel (typisiert)
```
[email.checkEmail] [ai.prompt] [email.draftEmail]
outputPort[0]: outputPort[0]: inputPort[0]:
schema: EmailList schema: AiResult accepts: [EmailDraft, AiResult]
──────────────────→ ──────────────────→ Normalizer kennt AiResult und
Input: EmailList Input: EmailList mapped response → body,
Engine prüft Kompatibilität Engine prüft kein Spezialcode nötig.
```
**Entscheidend:** Der AI-Node kann so konfiguriert werden, dass sein Output-Typ `EmailDraft` statt `AiResult` ist (z.B. via Parameter `outputMode: "emailDraft"` oder via Prompt-Instruktion + structured output). Dann ist der Downstream-Merge trivial.
---
## 7 Offene Fragen / Entscheidungen
| # | Frage | Optionen |
|---|-------|----------|
| 1 | Soll Typ-Mismatch eine Kante verhindern (hard) oder nur warnen (soft)? | **Empfehlung: soft** (Warnung + gelbe Kante). Hard-Block wäre zu restriktiv bei generischen Nodes. |
| 2 | Port-Typ `Any` erlauben? | Ja, als Fallback. Nodes wie `flow.ifElse` leiten den Input transparent durch. |
| 3 | Dynamische Schemas (z.B. `input.form` Payload hängt von Felddefinition ab)? | Ja. `outputPorts` kann eine Funktion referenzieren, die das Schema aus den `parameters` ableitet (wie heute `outputPreviewRegistry`). |
| 4 | Sollen bestehende Workflows beim Upgrade automatisch migriert werden? | **Nein.** Alte Workflows laufen weiter (Normalizer fängt fehlende Keys ab). Neue Workflows profitieren von Validierung. |
| 5 | Separater `data.aggregate`-Node oder implizit in `flow.loop`? | **Empfehlung: separater Node.** Explizit ist klarer; der Loop-Node bleibt rein für Iteration. |
| 6 | Braucht es einen `data.join`-Node (merge zwei Listen)? | Später. Erst aggregate + filter + transform etablieren. |
---
## 8 Aufwand-Schätzung
| Phase | Geschätzter Aufwand | Abhängigkeiten |
|-------|-------------------|----------------|
| 1 — Port-Schema-Deklaration | 23 Tage | Keine |
| 2 — Output-Normalizer | 35 Tage | Phase 1 |
| 3 — Frontend DataPicker aus Schema | 23 Tage | Phase 1 |
| 4 — Generisches Parameter-Rendering | 34 Tage | Phase 1 |
| 5 — Data Nodes (aggregate, transform, filter) | 35 Tage | Phase 2 |
| **Gesamt** | **~1320 Tage** | Phasen 13 sind der kritische Pfad |
---
## 9 Schlüssel-Dateien
| Bereich | Pfade |
|---------|-------|
| Node-Definitionen | `gateway/modules/features/graphicalEditor/nodeDefinitions/*.py` |
| Execution Engine | `gateway/modules/workflows/automation2/executionEngine.py` |
| ActionNodeExecutor (Handover-Heuristik) | `gateway/modules/workflows/automation2/executors/actionNodeExecutor.py` |
| Graph-Utilities | `gateway/modules/workflows/automation2/graphUtils.py` |
| Method/Action Typsystem | `gateway/modules/workflows/methods/methodBase.py`, `gateway/modules/datamodels/datamodelWorkflowActions.py` |
| FrontendType Enum | `gateway/modules/shared/frontendTypes.py` |
| Output-Preview (Frontend) | `frontend_nyla/src/components/FlowEditor/nodes/shared/outputPreviewRegistry.ts` |
| DataPicker (Frontend) | `frontend_nyla/src/components/FlowEditor/nodes/shared/DataPicker.tsx` |
| DataRef/DynamicValue (Frontend) | `frontend_nyla/src/components/FlowEditor/nodes/shared/dataRef.ts` |
| Node Config Registry (Frontend) | `frontend_nyla/src/components/FlowEditor/nodes/configs/index.ts` |
| DataFlow Context (Frontend) | `frontend_nyla/src/components/FlowEditor/context/Automation2DataFlowContext.tsx` |