# Copyright (c) 2025 Patrick Motsch # Context node definitions — structural extraction without AI plus # generic key/value, merge, filter and transform helpers. from modules.shared.i18nRegistry import t _CONTEXT_INPUT_SCHEMAS = [ "Transit", "ActionResult", "AiResult", "MergeResult", "FormPayload", "DocumentList", "EmailList", "TaskList", "FileList", "LoopItem", "UdmDocument", ] _MERGE_RESULT_DATA_PICK_OPTIONS = [ { "path": ["merged"], "pickerLabel": t("Zusammengeführt"), "detail": t("Zusammengeführtes Objekt nach gewählter Strategie."), "recommended": True, "type": "Dict", }, { "path": ["first"], "pickerLabel": t("Erster Zweig"), "detail": t("Daten vom ersten verbundenen Eingang."), "recommended": False, "type": "Any", }, { "path": ["inputs"], "pickerLabel": t("Alle Eingänge"), "detail": t("Dict der Eingabeobjekte nach Port-Index."), "recommended": False, "type": "Dict[int,Any]", }, { "path": ["conflicts"], "pickerLabel": t("Konflikte"), "detail": t("Liste der Schlüssel mit Konflikt (nur bei errorOnConflict)."), "recommended": False, "type": "List[str]", }, ] CONTEXT_NODES = [ { "id": "context.extractContent", "category": "context", "label": t("Inhalt extrahieren"), "description": t( "Extrahiert Inhalt ohne KI. Ergebnis einheitlich wie KI-Schritte: `response` " "(gesammelter Klartext), strukturierte JSON-Unterlage in `documents[0]`, " "einzelne Bilder als eigene Dokumente `extract_media_*` (nur im Workflow, ohne Eintrag unter „Meine Dateien“) — " "Auswahl im Daten-Picker wie bei `ai.process`." ), "parameters": [ {"name": "documentList", "type": "str", "required": True, "frontendType": "hidden", "description": t("Dokumentenliste (via Wire oder DataRef)"), "default": "", "graphInherit": {"port": 0, "kind": "documentListWire"}}, ], "inputs": 1, "outputs": 1, "inputPorts": {0: {"accepts": ["DocumentList", "Transit", "LoopItem"]}}, "outputPorts": { 0: { "schema": "ActionResult", # Authoritative DataPicker paths (same idea as ``parameters`` for configuration). # Frontend uses only this list — no schema expansion merge for this port. "dataPickOptions": [ { "path": ["documents", 0, "documentData"], "pickerLabel": t("Gesamter Inhalt"), "detail": t( "Strukturiertes Handover als JSON inklusive aller Textteile " "und Verweisen auf ausgelagerte Bilder." ), "recommended": True, "type": "Any", }, { "path": ["response"], "pickerLabel": t("Nur Text"), "detail": t( "Verketteter Klartext aus allen erkannten Textteilen." ), "recommended": True, "type": "str", }, { "path": ["imageDocumentsOnly"], "pickerLabel": t("Nur Bilder"), "detail": t( "Nur die extrahierten Bilddokumente als Liste, ohne JSON-Handover." ), "recommended": False, "type": "List[ActionDocument]", }, { "path": ["documents"], "pickerLabel": t("Alle Dateitypen"), "detail": t( "Alle Ausgabedokumente nacheinander: JSON-Handover und Bilder." ), "recommended": False, "type": "List[ActionDocument]", }, ], } }, "meta": {"icon": "mdi-file-tree-outline", "color": "#00897B", "usesAi": False}, "_method": "context", "_action": "extractContent", }, { "id": "context.setContext", "category": "context", "label": t("Kontext setzen"), "description": t( "Schreibt in den Workflow-Kontext. Pro Zeile: Ziel-Schlüssel, dann entweder einen " "festen Wert, eine Datenquelle aus dem Graph (Kontext-Picker wie bei anderen Nodes), " "oder eine Aufgabe für einen Benutzer (Human Task) zum Setzen des Werts." ), "parameters": [ { "name": "scope", "type": "str", "required": False, "frontendType": "select", "frontendOptions": {"options": ["local", "global", "session"]}, "default": "local", "description": t("Speicherbereich"), }, { "name": "assignments", "type": "list", "required": True, "frontendType": "contextAssignments", "default": [], "description": t( "Zuweisungen: Ziel-Schlüssel, Quelle (Picker / fester Wert / Human Task), " "Modus (set, setIfEmpty, append, increment). Optionaler Experten-Pfad `sourcePath` unter der " "gewählten Datenquelle (z. B. payload.status)." ), "graphInherit": {"port": 0, "kind": "primaryTextRef"}, }, ], "inputs": 1, "outputs": 1, "inputPorts": {0: {"accepts": _CONTEXT_INPUT_SCHEMAS}}, "outputPorts": { 0: { "schema": "Transit", "dynamic": True, "deriveFrom": "assignments", "deriveNameField": "contextKey", } }, "injectUpstreamPayload": True, "injectRunContext": True, "surfaceDataAsTopLevel": True, "meta": {"icon": "mdi-database-edit-outline", "color": "#5C6BC0", "usesAi": False}, "_method": "context", "_action": "setContext", }, { "id": "context.mergeContext", "category": "context", "label": t("Kontext zusammenführen"), "description": t( "Wartet auf alle verbundenen eingehenden Branches und führt deren " "Kontext-Daten zu einem einheitlichen MergeResult zusammen. " "Strategien: 'shallow' (oberste Ebene), 'deep' (rekursiv), " "'firstWins' / 'lastWins' bei Konflikten, " "'errorOnConflict' (bricht ab und listet Konflikte). " "Der Node blockiert bis alle erwarteten Inputs eingetroffen sind." ), "parameters": [ { "name": "strategy", "type": "str", "required": False, "frontendType": "select", "frontendOptions": { "options": ["shallow", "deep", "firstWins", "lastWins", "errorOnConflict"] }, "default": "deep", "description": t("Strategie bei gleichnamigen Keys aus verschiedenen Branches"), }, { "name": "waitFor", "type": "int", "required": False, "frontendType": "number", "default": 0, "description": t( "Anzahl Inputs abwarten (0 = alle verbundenen Branches). " "Hilfreich für optionale Branches mit Timeout." ), }, { "name": "timeoutMs", "type": "int", "required": False, "frontendType": "number", "default": 30000, "description": t( "Maximale Wartezeit in ms — danach wird mit den vorhandenen Inputs fortgesetzt" ), }, ], "inputs": 5, "outputs": 1, "inputPorts": { 0: {"accepts": _CONTEXT_INPUT_SCHEMAS}, 1: {"accepts": _CONTEXT_INPUT_SCHEMAS}, 2: {"accepts": _CONTEXT_INPUT_SCHEMAS}, 3: {"accepts": _CONTEXT_INPUT_SCHEMAS}, 4: {"accepts": _CONTEXT_INPUT_SCHEMAS}, }, "outputPorts": { 0: {"schema": "MergeResult", "dataPickOptions": _MERGE_RESULT_DATA_PICK_OPTIONS} }, "waitsForAllPredecessors": True, "injectBranchInputs": True, "meta": {"icon": "mdi-call-merge", "color": "#7B1FA2", "usesAi": False}, "_method": "context", "_action": "mergeContext", }, { "id": "context.filterContext", "category": "context", "label": t("Kontext filtern"), "description": t( "Gibt nur bestimmte Felder des eingehenden Datenstroms weiter. " "Modus 'allow': nur diese Keys passieren. " "Modus 'block': diese Keys werden entfernt, alles andere bleibt. " "Unterstützt Pfadausdrücke (z.B. 'user.*', '*.id') und tiefe Pfade ('address.city'). " "Fehlende Keys werden je nach 'missingKeyBehavior' ignoriert, mit null befüllt oder als Fehler behandelt." ), "parameters": [ { "name": "mode", "type": "str", "required": False, "frontendType": "select", "frontendOptions": {"options": ["allow", "block"]}, "default": "allow", "description": t("Allowlist (nur diese durch) oder Blocklist (diese entfernen)"), }, { "name": "keys", "type": "list", "required": True, "frontendType": "stringList", "default": [], "description": t( "Key-Pfade oder Wildcard-Muster. " "Beispiele: 'response', 'user.*', '*.id', 'address.city'." ), }, { "name": "missingKeyBehavior", "type": "str", "required": False, "frontendType": "select", "frontendOptions": {"options": ["skip", "nullFill", "error"]}, "default": "skip", "description": t("Verhalten wenn ein erlaubter Key im Input fehlt"), }, { "name": "preserveMeta", "type": "bool", "required": False, "frontendType": "checkbox", "default": True, "description": t("Interne Meta-Felder (_success, _error, _transit) immer durchlassen"), }, ], "inputs": 1, "outputs": 1, "inputPorts": {0: {"accepts": _CONTEXT_INPUT_SCHEMAS}}, "outputPorts": { 0: { "schema": "Transit", "dynamic": True, "deriveFrom": "keys", } }, "injectUpstreamPayload": True, "surfaceDataAsTopLevel": True, "meta": {"icon": "mdi-filter-outline", "color": "#00838F", "usesAi": False}, "_method": "context", "_action": "filterContext", }, { "id": "context.transformContext", "category": "context", "label": t("Kontext transformieren"), "description": t( "Verändert die Struktur des eingehenden Datenstroms. " "Operationen pro Mapping: 'rename' (Key umbenennen), 'cast' (Typ konvertieren), " "'nest' (mehrere Felder unter neuem Objekt zusammenfassen), " "'flatten' (verschachteltes Objekt auf oberste Ebene heben), " "'compute' (neues Feld aus Template-/{{...}}-Ausdruck berechnen). " "Jedes Mapping definiert: 'sourceField' (Eingangspfad / Ausdruck), " "'outputField' (Ausgabe-Key), 'operation' und 'type' (Zieltyp). " "Das Ergebnis ist ein neues Objekt — der ursprüngliche Datenstrom " "wird nicht automatisch weitergegeben (ausser 'passthroughUnmapped: true')." ), "parameters": [ { "name": "mappings", "type": "list", "required": True, "frontendType": "mappingTable", "default": [], "description": t( "Liste von Mapping-Einträgen. Jeder Eintrag: " "sourceField (DataRef-Pfad oder Ausdruck), " "outputField (Ziel-Key im Output), " "operation (rename | cast | nest | flatten | compute), " "type (str | int | bool | float | object | list — für cast), " "expression (für compute: Template oder Ausdruck, z.B. '{{firstName}} {{lastName}}')." ), }, { "name": "passthroughUnmapped", "type": "bool", "required": False, "frontendType": "checkbox", "default": False, "description": t( "Alle nicht gemappten Felder des Eingangs zusätzlich in den Output übernehmen." ), }, { "name": "flattenDepth", "type": "int", "required": False, "frontendType": "number", "default": 1, "description": t("Tiefe für flatten-Operation (1 = eine Ebene, -1 = vollständig)"), }, ], "inputs": 1, "outputs": 1, "inputPorts": {0: {"accepts": _CONTEXT_INPUT_SCHEMAS}}, "outputPorts": { 0: { "schema": { "kind": "fromGraph", "parameter": "mappings", "nameField": "outputField", "schemaName": "Transform_dynamic", }, "dynamic": True, "deriveFrom": "mappings", "deriveNameField": "outputField", } }, "injectUpstreamPayload": True, "surfaceDataAsTopLevel": True, "meta": {"icon": "mdi-swap-horizontal", "color": "#EF6C00", "usesAi": False}, "_method": "context", "_action": "transformContext", }, ]