gateway/modules/workflows/automation2/pickNotPushMigration.py

156 lines
5.7 KiB
Python

# Copyright (c) 2025 Patrick Motsch
"""
Graph helpers for Pick-not-Push: materialize typed DataRefs before executeGraph runs.
- ``materializeConnectionRefs``: empty ``connectionReference`` from upstream connection provenance.
- ``materializePrimaryTextHandover``: parameters whose static definition includes
``graphInherit.kind == "primaryTextRef"`` (canonical paths: ``PRIMARY_TEXT_HANDOVER_REF_PATH``).
Runtime: executeGraph deep-copies the version graph and applies these passes in order.
"""
from __future__ import annotations
import copy
import logging
from typing import Any, Dict, List
from modules.features.graphicalEditor.nodeDefinitions import STATIC_NODE_TYPES
from modules.features.graphicalEditor.portTypes import (
PRIMARY_TEXT_HANDOVER_REF_PATH,
resolve_output_schema_name,
)
from modules.workflows.automation2.graphUtils import buildConnectionMap, getInputSources
logger = logging.getLogger(__name__)
_NODE_DEF_BY_ID = {n["id"]: n for n in STATIC_NODE_TYPES}
_SCHEMAS_WITH_CONNECTION = frozenset(
{"FileList", "DocumentList", "EmailList", "TaskList", "EmailDraft", "UdmDocument"},
)
def _data_ref(node_id: str, path: List[Any]) -> Dict[str, Any]:
return {"type": "ref", "nodeId": node_id, "path": list(path)}
def materializeConnectionRefs(graph: Dict[str, Any]) -> Dict[str, Any]:
"""
Deep-copy graph and set empty connectionReference (userConnection params) to
DataRef { nodeId: upstreamPort0, path: ['connection','id'] } when upstream
output schema carries connection provenance.
"""
g = copy.deepcopy(graph)
nodes: List[Dict[str, Any]] = g.get("nodes") or []
connections = g.get("connections") or []
if not nodes:
return g
conn_map = buildConnectionMap(connections)
node_by_id = {n["id"]: n for n in nodes if n.get("id")}
for node in nodes:
nid = node.get("id")
ntype = node.get("type")
if not nid or not ntype:
continue
node_def = _NODE_DEF_BY_ID.get(ntype)
if not node_def:
continue
pdefs = node_def.get("parameters") or []
has_conn = any(
p.get("name") == "connectionReference" and p.get("frontendType") == "userConnection"
for p in pdefs
)
if not has_conn:
continue
params = node.get("parameters")
if not isinstance(params, dict):
node["parameters"] = {}
params = node["parameters"]
cur = params.get("connectionReference")
if cur not in (None, "", {}):
continue
input_sources = getInputSources(nid, conn_map)
if 0 not in input_sources:
continue
src_id, _ = input_sources[0]
src_node = node_by_id.get(src_id) or {}
src_def = _NODE_DEF_BY_ID.get(src_node.get("type") or "")
if not src_def:
continue
out_port = (src_def.get("outputPorts") or {}).get(0, {}) or {}
out_schema = resolve_output_schema_name(src_node, out_port if isinstance(out_port, dict) else {})
if out_schema not in _SCHEMAS_WITH_CONNECTION:
continue
params["connectionReference"] = _data_ref(src_id, ["connection", "id"])
logger.debug("materializeConnectionRefs: %s.connectionReference -> ref %s.connection.id", nid, src_id)
return g
def _slot_empty_for_primary_text_inherit(val: Any) -> bool:
return val is None or val == "" or val == []
def materializePrimaryTextHandover(graph: Dict[str, Any]) -> Dict[str, Any]:
"""
For parameters declaring ``graphInherit.kind == "primaryTextRef"`` (optional ``port``, default 0) with an
empty value, set an explicit ``DataRef`` to the canonical text field of the producer on
that port (see ``PRIMARY_TEXT_HANDOVER_REF_PATH`` keyed by upstream output schema name).
"""
g = copy.deepcopy(graph)
nodes: List[Dict[str, Any]] = g.get("nodes") or []
connections = g.get("connections") or []
if not nodes:
return g
conn_map = buildConnectionMap(connections)
node_by_id = {n["id"]: n for n in nodes if n.get("id")}
for node in nodes:
nid = node.get("id")
ntype = node.get("type")
if not nid or not ntype:
continue
node_def = _NODE_DEF_BY_ID.get(ntype)
if not node_def:
continue
params = node.get("parameters")
if not isinstance(params, dict):
node["parameters"] = {}
params = node["parameters"]
for pdef in node_def.get("parameters") or []:
gi = pdef.get("graphInherit")
if not isinstance(gi, dict) or gi.get("kind") != "primaryTextRef":
continue
pname = pdef.get("name")
if not pname:
continue
port_ix = int(gi.get("port", 0))
if not _slot_empty_for_primary_text_inherit(params.get(pname)):
continue
input_sources = getInputSources(nid, conn_map)
if port_ix not in input_sources:
continue
src_id, _ = input_sources[port_ix]
src_node = node_by_id.get(src_id) or {}
src_def = _NODE_DEF_BY_ID.get(src_node.get("type") or "")
if not src_def:
continue
out_port = (src_def.get("outputPorts") or {}).get(0, {}) or {}
out_schema = resolve_output_schema_name(src_node, out_port if isinstance(out_port, dict) else {})
ref_path = PRIMARY_TEXT_HANDOVER_REF_PATH.get(out_schema)
if not ref_path:
continue
params[pname] = _data_ref(src_id, list(ref_path))
logger.debug(
"materializePrimaryTextHandover: %s.%s -> ref %s path=%s",
nid,
pname,
src_id,
ref_path,
)
return g