From af061b1a076d5e6cc11935ea190f091efa1ab539 Mon Sep 17 00:00:00 2001 From: Claude AI Date: Thu, 29 Jan 2026 11:13:03 +0000 Subject: [PATCH] feat(fase4): add JavaScript and HTTP request nodes Co-Authored-By: Claude Opus 4.5 --- services/flow-engine/app/nodes/__init__.py | 1 + services/flow-engine/app/nodes/script.py | 100 +++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 services/flow-engine/app/nodes/script.py diff --git a/services/flow-engine/app/nodes/__init__.py b/services/flow-engine/app/nodes/__init__.py index b9ab663..43e097f 100644 --- a/services/flow-engine/app/nodes/__init__.py +++ b/services/flow-engine/app/nodes/__init__.py @@ -3,3 +3,4 @@ from app.nodes.basic import ( TriggerExecutor, MessageExecutor, ButtonsExecutor, WaitInputExecutor, SetVariableExecutor, ConditionExecutor ) +from app.nodes.script import JavaScriptExecutor, HttpRequestExecutor diff --git a/services/flow-engine/app/nodes/script.py b/services/flow-engine/app/nodes/script.py new file mode 100644 index 0000000..0f322c1 --- /dev/null +++ b/services/flow-engine/app/nodes/script.py @@ -0,0 +1,100 @@ +import json +from typing import Optional, Any +import httpx +from app.nodes.base import NodeExecutor +from app.context import FlowContext + + +class JavaScriptExecutor(NodeExecutor): + """Execute Python expressions with restricted globals""" + + ALLOWED_BUILTINS = { + 'abs': abs, 'all': all, 'any': any, 'bool': bool, + 'dict': dict, 'float': float, 'int': int, 'len': len, + 'list': list, 'max': max, 'min': min, 'round': round, + 'str': str, 'sum': sum, 'sorted': sorted, + } + + async def execute(self, config: dict, context: FlowContext, session: Any) -> Optional[str]: + code = config.get("code", "") + output_variable = config.get("output_variable", "_result") + + if not code: + return "default" + + exec_globals = { + '__builtins__': self.ALLOWED_BUILTINS, + 'context': { + 'contact': context.contact, + 'conversation': context.conversation, + 'message': context.message, + 'variables': context.variables.copy(), + }, + 'variables': context.variables.copy(), + } + + try: + result = eval(code, exec_globals, {}) + if result is not None: + context.set(output_variable, result) + return "success" + except Exception as e: + context.set("_script_error", str(e)) + return "error" + + +class HttpRequestExecutor(NodeExecutor): + """Make HTTP requests to external APIs""" + + async def execute(self, config: dict, context: FlowContext, session: Any) -> Optional[str]: + url = context.interpolate(config.get("url", "")) + method = config.get("method", "GET").upper() + headers = config.get("headers", {}) + body = config.get("body") + output_variable = config.get("output_variable", "_http_response") + timeout = config.get("timeout", 10) + + if not url: + return "error" + + headers = {k: context.interpolate(str(v)) for k, v in headers.items()} + if body and isinstance(body, str): + body = context.interpolate(body) + + try: + async with httpx.AsyncClient() as client: + json_body = None + if body: + try: + json_body = json.loads(body) if isinstance(body, str) else body + except json.JSONDecodeError: + json_body = None + + response = await client.request( + method=method, + url=url, + headers=headers, + json=json_body, + timeout=timeout + ) + + response_json = None + if response.headers.get("content-type", "").startswith("application/json"): + try: + response_json = response.json() + except json.JSONDecodeError: + pass + + context.set(output_variable, { + "status": response.status_code, + "body": response.text, + "json": response_json + }) + + if 200 <= response.status_code < 300: + return "success" + return "error" + + except Exception as e: + context.set("_http_error", str(e)) + return "error"