# 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/Human", "de": "Eingabe/Mensch", "fr": "Entrée/Humain"}}, {"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"}}, ] return {"nodeTypes": localized, "categories": categories}