179 lines
6.4 KiB
Python
179 lines
6.4 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""
|
|
Node Type Registry for graphicalEditor - static node definitions (ai, email, sharepoint, trigger, flow, data, input).
|
|
Nodes are defined first; IO/method actions are used at execution time.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, List, Any, Optional
|
|
|
|
from modules.features.graphicalEditor.nodeDefinitions import STATIC_NODE_TYPES
|
|
from modules.features.graphicalEditor.nodeAdapter import _bindsActionFromLegacy
|
|
from modules.features.graphicalEditor.portTypes import PORT_TYPE_CATALOG, SYSTEM_VARIABLES
|
|
from modules.shared.i18nRegistry import normalizePrimaryLanguageTag, resolveText
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def getNodeTypes(
|
|
services: Any = None,
|
|
language: str = "de",
|
|
) -> List[Dict[str, Any]]:
|
|
"""
|
|
Return static node types. No dynamic I/O derivation from methodDiscovery.
|
|
services: Optional (kept for API compatibility, not used).
|
|
"""
|
|
return list(STATIC_NODE_TYPES)
|
|
|
|
|
|
def _pickFromLangMap(d: Any, lang: str) -> Any:
|
|
"""Resolve multilingual dict: ``lang`` → ``xx`` → ``de`` → ``en`` → first non-empty value."""
|
|
if not isinstance(d, dict) or not d:
|
|
return None
|
|
for k in (lang, "xx", "de", "en"):
|
|
v = d.get(k)
|
|
if v is not None and v != "":
|
|
return v
|
|
for v in d.values():
|
|
if v is not None and v != "":
|
|
return v
|
|
return None
|
|
|
|
|
|
def _localizeNode(node: Dict[str, Any], language: str) -> Dict[str, Any]:
|
|
"""Apply request language via resolveText (t() keys + multilingual dicts).
|
|
|
|
Also exposes Schicht-3 metadata (`bindsAction`) derived from the legacy
|
|
`_method`/`_action` pair, so frontend consumers can resolve back to the
|
|
Schicht-2 Action signature without parsing internal underscore-prefixed
|
|
fields.
|
|
"""
|
|
lang = normalizePrimaryLanguageTag(language, "en")
|
|
bindsAction = _bindsActionFromLegacy(node)
|
|
out = dict(node)
|
|
for key in list(out.keys()):
|
|
if key.startswith("_"):
|
|
del out[key]
|
|
if bindsAction:
|
|
out["bindsAction"] = bindsAction
|
|
lbl = node.get("label")
|
|
if lbl is not None:
|
|
out["label"] = resolveText(lbl, lang) or node.get("id", "")
|
|
desc = node.get("description")
|
|
if desc is not None:
|
|
out["description"] = resolveText(desc, lang)
|
|
ol = node.get("outputLabels")
|
|
if ol is not None:
|
|
if isinstance(ol, list):
|
|
out["outputLabels"] = [resolveText(x, lang) for x in ol]
|
|
elif isinstance(ol, dict) and ol:
|
|
first = next(iter(ol.values()), None)
|
|
if isinstance(first, (list, tuple)):
|
|
picked = _pickFromLangMap(ol, lang)
|
|
raw = list(picked) if picked is not None else list(first)
|
|
out["outputLabels"] = [resolveText(x, lang) for x in raw]
|
|
params = []
|
|
for p in node.get("parameters", []):
|
|
pc = dict(p)
|
|
pd = p.get("description")
|
|
if pd is not None:
|
|
pc["description"] = resolveText(pd, lang)
|
|
params.append(pc)
|
|
out["parameters"] = params
|
|
return out
|
|
|
|
|
|
def getNodeTypesForApi(
|
|
services: Any,
|
|
language: str = "de",
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
API-ready response: nodeTypes with localized strings, plus categories, portTypeCatalog, systemVariables.
|
|
"""
|
|
nodes = getNodeTypes(services, language)
|
|
localized = [_localizeNode(n, language) for n in nodes]
|
|
categories = [
|
|
{"id": "trigger", "label": "Trigger"},
|
|
{"id": "input", "label": "Eingabe/Mensch"},
|
|
{"id": "flow", "label": "Ablauf"},
|
|
{"id": "data", "label": "Daten"},
|
|
{"id": "context", "label": "Kontext"},
|
|
{"id": "ai", "label": "KI"},
|
|
{"id": "file", "label": "Datei"},
|
|
{"id": "email", "label": "E-Mail"},
|
|
{"id": "sharepoint", "label": "SharePoint"},
|
|
{"id": "clickup", "label": "ClickUp"},
|
|
{"id": "trustee", "label": "Treuhand"},
|
|
]
|
|
|
|
catalogSerialized = {}
|
|
for name, schema in PORT_TYPE_CATALOG.items():
|
|
catalogSerialized[name] = {
|
|
"name": schema.name,
|
|
"fields": [f.model_dump() for f in schema.fields],
|
|
}
|
|
|
|
return {
|
|
"nodeTypes": localized,
|
|
"categories": categories,
|
|
"portTypeCatalog": catalogSerialized,
|
|
"systemVariables": SYSTEM_VARIABLES,
|
|
}
|
|
|
|
|
|
def getNodeTypeToMethodAction() -> Dict[str, tuple]:
|
|
"""
|
|
Mapping from node type id to (method, action) for execution.
|
|
Used by ActionNodeExecutor.
|
|
"""
|
|
mapping = {}
|
|
for node in STATIC_NODE_TYPES:
|
|
method = node.get("_method")
|
|
action = node.get("_action")
|
|
if method and action:
|
|
mapping[node["id"]] = (method, action)
|
|
return mapping
|
|
|
|
|
|
def validateAdaptersAgainstMethods(methodInstances: Optional[Dict[str, Any]] = None) -> Optional[str]:
|
|
"""Run the Schicht-3 Adapter validator (5 drift rules) against the live methods.
|
|
|
|
Intended to be called once at startup after methodDiscovery has populated
|
|
the methods registry. Returns a human-readable report (None when healthy)
|
|
so the caller decides whether to log, raise, or surface to operators.
|
|
|
|
Pass `methodInstances` directly for testability; defaults to importing
|
|
the live registry from `methodDiscovery.methods`.
|
|
"""
|
|
from modules.features.graphicalEditor.adapterValidator import (
|
|
_buildActionsRegistryFromMethods,
|
|
_formatAdapterReport,
|
|
_validateAllAdapters,
|
|
)
|
|
|
|
if methodInstances is None:
|
|
try:
|
|
from modules.workflows.processing.shared.methodDiscovery import methods
|
|
except Exception as exc:
|
|
logger.warning("Adapter validator skipped: cannot import methodDiscovery (%s)", exc)
|
|
return None
|
|
|
|
methodInstances = {}
|
|
for fullName, info in (methods or {}).items():
|
|
shortName = fullName.replace("Method", "").lower() if fullName[:1].isupper() else fullName
|
|
instance = info.get("instance") if isinstance(info, dict) else None
|
|
if instance is not None:
|
|
methodInstances[shortName] = instance
|
|
|
|
if not methodInstances:
|
|
return None
|
|
|
|
actionsRegistry = _buildActionsRegistryFromMethods(methodInstances)
|
|
report = _validateAllAdapters(list(STATIC_NODE_TYPES), actionsRegistry)
|
|
formatted = _formatAdapterReport(report)
|
|
if not report.isHealthy:
|
|
logger.warning("[adapterValidator] %s", formatted)
|
|
elif report.warnings:
|
|
logger.info("[adapterValidator] %s", formatted)
|
|
return formatted
|