376 lines
14 KiB
Python
376 lines
14 KiB
Python
# 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",
|
|
},
|
|
]
|