167 lines
6.3 KiB
Python
167 lines
6.3 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""
|
|
Node Type Registry for automation2 - merges static definitions with dynamic I/O nodes from methodDiscovery.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, List, Any, Optional
|
|
|
|
from modules.features.automation2.nodeDefinitions import STATIC_NODE_TYPES
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Short method names that map to I/O node category display
|
|
METHOD_LABELS = {
|
|
"outlook": {"en": "Outlook", "de": "Outlook", "fr": "Outlook"},
|
|
"sharepoint": {"en": "SharePoint", "de": "SharePoint", "fr": "SharePoint"},
|
|
"context": {"en": "Context", "de": "Kontext", "fr": "Contexte"},
|
|
"ai": {"en": "AI", "de": "KI", "fr": "IA"},
|
|
"trustee": {"en": "Trustee", "de": "Trustee", "fr": "Trustee"},
|
|
"jira": {"en": "Jira", "de": "Jira", "fr": "Jira"},
|
|
"chatbot": {"en": "Chatbot", "de": "Chatbot", "fr": "Chatbot"},
|
|
}
|
|
|
|
|
|
def _actionNameToLabel(actionName: str) -> str:
|
|
"""Convert camelCase actionName to readable label."""
|
|
import re
|
|
parts = re.sub(r"([A-Z])", r" \1", actionName).strip().split()
|
|
return " ".join(p.capitalize() for p in parts) if parts else actionName
|
|
|
|
|
|
def _buildIoNodeFromAction(
|
|
shortMethod: str,
|
|
actionName: str,
|
|
actionDef: Dict[str, Any],
|
|
language: str = "en",
|
|
) -> Dict[str, Any]:
|
|
"""Build a single I/O node definition from a method action."""
|
|
lang = language if language in ("en", "de", "fr") else "en"
|
|
methodLabel = METHOD_LABELS.get(shortMethod, {}).get(lang, shortMethod)
|
|
actionLabel = _actionNameToLabel(actionName)
|
|
nodeId = f"io.{shortMethod}.{actionName}"
|
|
nodeLabel = {l: f"{METHOD_LABELS.get(shortMethod, {}).get(l, shortMethod)} - {_actionNameToLabel(actionName)}" for l in ("en", "de", "fr")}
|
|
|
|
parameters = []
|
|
paramDefs = actionDef.get("parameters", {})
|
|
for paramName, paramInfo in paramDefs.items():
|
|
if isinstance(paramInfo, dict):
|
|
p = {
|
|
"name": paramName,
|
|
"type": paramInfo.get("type", "str"),
|
|
"required": paramInfo.get("required", False),
|
|
"description": paramInfo.get("description", ""),
|
|
}
|
|
if paramInfo.get("default") is not None:
|
|
p["default"] = paramInfo["default"]
|
|
parameters.append(p)
|
|
else:
|
|
parameters.append({
|
|
"name": paramName,
|
|
"type": "str",
|
|
"required": False,
|
|
"description": str(paramInfo),
|
|
})
|
|
|
|
return {
|
|
"id": nodeId,
|
|
"category": "io",
|
|
"label": nodeLabel,
|
|
"description": actionDef.get("description") or nodeLabel,
|
|
"parameters": parameters,
|
|
"inputs": 1,
|
|
"outputs": 1,
|
|
"executor": "io",
|
|
"meta": {"icon": "mdi-connection", "color": "#00BCD4", "method": shortMethod, "action": actionName},
|
|
}
|
|
|
|
|
|
def getIoNodesFromMethods(methods: Dict[str, Any], language: str = "en") -> List[Dict[str, Any]]:
|
|
"""
|
|
Build I/O node types from methodDiscovery.methods.
|
|
methods: { methodName: { instance, actions: { actionName: { description, parameters, method } } } }
|
|
Returns list of node definitions for io.{shortMethod}.{actionName}.
|
|
"""
|
|
ioNodes = []
|
|
processed = set()
|
|
|
|
for methodName, methodInfo in methods.items():
|
|
if not methodName.startswith("Method"):
|
|
continue
|
|
shortMethod = methodName.replace("Method", "").lower()
|
|
if shortMethod in processed:
|
|
continue
|
|
processed.add(shortMethod)
|
|
|
|
methodInstance = methodInfo.get("instance")
|
|
if not methodInstance:
|
|
continue
|
|
|
|
actions = methodInstance.actions
|
|
for actionName, actionDef in actions.items():
|
|
if not isinstance(actionDef, dict):
|
|
continue
|
|
try:
|
|
node = _buildIoNodeFromAction(shortMethod, actionName, actionDef, language)
|
|
ioNodes.append(node)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to build I/O node io.{shortMethod}.{actionName}: {e}")
|
|
continue
|
|
|
|
return ioNodes
|
|
|
|
|
|
def getNodeTypes(
|
|
services: Any,
|
|
language: str = "en",
|
|
) -> List[Dict[str, Any]]:
|
|
"""
|
|
Return merged node types: static (trigger, flow, data) + dynamic I/O nodes from methodDiscovery.
|
|
services: Hub from getAutomation2Services (needed for discoverMethods + RBAC-filtered actions).
|
|
"""
|
|
from modules.workflows.processing.shared.methodDiscovery import discoverMethods, methods
|
|
|
|
discoverMethods(services)
|
|
|
|
static = list(STATIC_NODE_TYPES)
|
|
ioNodes = getIoNodesFromMethods(methods, language)
|
|
return static + ioNodes
|
|
|
|
|
|
def _localizeNode(node: Dict[str, Any], language: str) -> Dict[str, Any]:
|
|
"""Apply language to label/description/parameters."""
|
|
lang = language if language in ("en", "de", "fr") else "en"
|
|
out = dict(node)
|
|
if isinstance(node.get("label"), dict):
|
|
out["label"] = node["label"].get(lang, node["label"].get("en", str(node["label"])))
|
|
if isinstance(node.get("description"), dict):
|
|
out["description"] = node["description"].get(lang, node["description"].get("en", str(node["description"])))
|
|
params = []
|
|
for p in node.get("parameters", []):
|
|
pc = dict(p)
|
|
if isinstance(p.get("description"), dict):
|
|
pc["description"] = p["description"].get(lang, p["description"].get("en", str(p.get("description", ""))))
|
|
params.append(pc)
|
|
out["parameters"] = params
|
|
return out
|
|
|
|
|
|
def getNodeTypesForApi(
|
|
services: Any,
|
|
language: str = "en",
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
API-ready response: nodeTypes with localized strings, plus categories list.
|
|
"""
|
|
nodes = getNodeTypes(services, language)
|
|
localized = [_localizeNode(n, language) for n in nodes]
|
|
categories = [
|
|
{"id": "trigger", "label": {"en": "Trigger", "de": "Trigger", "fr": "Déclencheur"}},
|
|
{"id": "input", "label": {"en": "Input", "de": "Eingabe", "fr": "Entrée"}},
|
|
{"id": "flow", "label": {"en": "Flow", "de": "Ablauf", "fr": "Flux"}},
|
|
{"id": "data", "label": {"en": "Data", "de": "Daten", "fr": "Données"}},
|
|
{"id": "io", "label": {"en": "I/O", "de": "E/A", "fr": "E/S"}},
|
|
{"id": "human", "label": {"en": "Human", "de": "Mensch", "fr": "Humain"}},
|
|
]
|
|
return {"nodeTypes": localized, "categories": categories}
|