hexdag 0.5.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hexdag/__init__.py +116 -0
- hexdag/__main__.py +30 -0
- hexdag/adapters/executors/__init__.py +5 -0
- hexdag/adapters/executors/local_executor.py +316 -0
- hexdag/builtin/__init__.py +6 -0
- hexdag/builtin/adapters/__init__.py +51 -0
- hexdag/builtin/adapters/anthropic/__init__.py +5 -0
- hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
- hexdag/builtin/adapters/database/__init__.py +6 -0
- hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
- hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
- hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
- hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
- hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
- hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
- hexdag/builtin/adapters/local/README.md +59 -0
- hexdag/builtin/adapters/local/__init__.py +7 -0
- hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
- hexdag/builtin/adapters/memory/__init__.py +47 -0
- hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
- hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
- hexdag/builtin/adapters/memory/schemas.py +57 -0
- hexdag/builtin/adapters/memory/session_memory.py +178 -0
- hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
- hexdag/builtin/adapters/memory/state_memory.py +280 -0
- hexdag/builtin/adapters/mock/README.md +89 -0
- hexdag/builtin/adapters/mock/__init__.py +15 -0
- hexdag/builtin/adapters/mock/hexdag.toml +50 -0
- hexdag/builtin/adapters/mock/mock_database.py +225 -0
- hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
- hexdag/builtin/adapters/mock/mock_llm.py +177 -0
- hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
- hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
- hexdag/builtin/adapters/openai/__init__.py +5 -0
- hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
- hexdag/builtin/adapters/secret/__init__.py +7 -0
- hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
- hexdag/builtin/adapters/unified_tool_router.py +280 -0
- hexdag/builtin/macros/__init__.py +17 -0
- hexdag/builtin/macros/conversation_agent.py +390 -0
- hexdag/builtin/macros/llm_macro.py +151 -0
- hexdag/builtin/macros/reasoning_agent.py +423 -0
- hexdag/builtin/macros/tool_macro.py +380 -0
- hexdag/builtin/nodes/__init__.py +38 -0
- hexdag/builtin/nodes/_discovery.py +123 -0
- hexdag/builtin/nodes/agent_node.py +696 -0
- hexdag/builtin/nodes/base_node_factory.py +242 -0
- hexdag/builtin/nodes/composite_node.py +926 -0
- hexdag/builtin/nodes/data_node.py +201 -0
- hexdag/builtin/nodes/expression_node.py +487 -0
- hexdag/builtin/nodes/function_node.py +454 -0
- hexdag/builtin/nodes/llm_node.py +491 -0
- hexdag/builtin/nodes/loop_node.py +920 -0
- hexdag/builtin/nodes/mapped_input.py +518 -0
- hexdag/builtin/nodes/port_call_node.py +269 -0
- hexdag/builtin/nodes/tool_call_node.py +195 -0
- hexdag/builtin/nodes/tool_utils.py +390 -0
- hexdag/builtin/prompts/__init__.py +68 -0
- hexdag/builtin/prompts/base.py +422 -0
- hexdag/builtin/prompts/chat_prompts.py +303 -0
- hexdag/builtin/prompts/error_correction_prompts.py +320 -0
- hexdag/builtin/prompts/tool_prompts.py +160 -0
- hexdag/builtin/tools/builtin_tools.py +84 -0
- hexdag/builtin/tools/database_tools.py +164 -0
- hexdag/cli/__init__.py +17 -0
- hexdag/cli/__main__.py +7 -0
- hexdag/cli/commands/__init__.py +27 -0
- hexdag/cli/commands/build_cmd.py +812 -0
- hexdag/cli/commands/create_cmd.py +208 -0
- hexdag/cli/commands/docs_cmd.py +293 -0
- hexdag/cli/commands/generate_types_cmd.py +252 -0
- hexdag/cli/commands/init_cmd.py +188 -0
- hexdag/cli/commands/pipeline_cmd.py +494 -0
- hexdag/cli/commands/plugin_dev_cmd.py +529 -0
- hexdag/cli/commands/plugins_cmd.py +441 -0
- hexdag/cli/commands/studio_cmd.py +101 -0
- hexdag/cli/commands/validate_cmd.py +221 -0
- hexdag/cli/main.py +84 -0
- hexdag/core/__init__.py +83 -0
- hexdag/core/config/__init__.py +20 -0
- hexdag/core/config/loader.py +479 -0
- hexdag/core/config/models.py +150 -0
- hexdag/core/configurable.py +294 -0
- hexdag/core/context/__init__.py +37 -0
- hexdag/core/context/execution_context.py +378 -0
- hexdag/core/docs/__init__.py +26 -0
- hexdag/core/docs/extractors.py +678 -0
- hexdag/core/docs/generators.py +890 -0
- hexdag/core/docs/models.py +120 -0
- hexdag/core/domain/__init__.py +10 -0
- hexdag/core/domain/dag.py +1225 -0
- hexdag/core/exceptions.py +234 -0
- hexdag/core/expression_parser.py +569 -0
- hexdag/core/logging.py +449 -0
- hexdag/core/models/__init__.py +17 -0
- hexdag/core/models/base.py +138 -0
- hexdag/core/orchestration/__init__.py +46 -0
- hexdag/core/orchestration/body_executor.py +481 -0
- hexdag/core/orchestration/components/__init__.py +97 -0
- hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
- hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
- hexdag/core/orchestration/components/execution_coordinator.py +360 -0
- hexdag/core/orchestration/components/health_check_manager.py +176 -0
- hexdag/core/orchestration/components/input_mapper.py +143 -0
- hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
- hexdag/core/orchestration/components/node_executor.py +377 -0
- hexdag/core/orchestration/components/secret_manager.py +202 -0
- hexdag/core/orchestration/components/wave_executor.py +158 -0
- hexdag/core/orchestration/constants.py +17 -0
- hexdag/core/orchestration/events/README.md +312 -0
- hexdag/core/orchestration/events/__init__.py +104 -0
- hexdag/core/orchestration/events/batching.py +330 -0
- hexdag/core/orchestration/events/decorators.py +139 -0
- hexdag/core/orchestration/events/events.py +573 -0
- hexdag/core/orchestration/events/observers/__init__.py +30 -0
- hexdag/core/orchestration/events/observers/core_observers.py +690 -0
- hexdag/core/orchestration/events/observers/models.py +111 -0
- hexdag/core/orchestration/events/taxonomy.py +269 -0
- hexdag/core/orchestration/hook_context.py +237 -0
- hexdag/core/orchestration/hooks.py +437 -0
- hexdag/core/orchestration/models.py +418 -0
- hexdag/core/orchestration/orchestrator.py +910 -0
- hexdag/core/orchestration/orchestrator_factory.py +275 -0
- hexdag/core/orchestration/port_wrappers.py +327 -0
- hexdag/core/orchestration/prompt/__init__.py +32 -0
- hexdag/core/orchestration/prompt/template.py +332 -0
- hexdag/core/pipeline_builder/__init__.py +21 -0
- hexdag/core/pipeline_builder/component_instantiator.py +386 -0
- hexdag/core/pipeline_builder/include_tag.py +265 -0
- hexdag/core/pipeline_builder/pipeline_config.py +133 -0
- hexdag/core/pipeline_builder/py_tag.py +223 -0
- hexdag/core/pipeline_builder/tag_discovery.py +268 -0
- hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
- hexdag/core/pipeline_builder/yaml_validator.py +569 -0
- hexdag/core/ports/__init__.py +65 -0
- hexdag/core/ports/api_call.py +133 -0
- hexdag/core/ports/database.py +489 -0
- hexdag/core/ports/embedding.py +215 -0
- hexdag/core/ports/executor.py +237 -0
- hexdag/core/ports/file_storage.py +117 -0
- hexdag/core/ports/healthcheck.py +87 -0
- hexdag/core/ports/llm.py +551 -0
- hexdag/core/ports/memory.py +70 -0
- hexdag/core/ports/observer_manager.py +130 -0
- hexdag/core/ports/secret.py +145 -0
- hexdag/core/ports/tool_router.py +94 -0
- hexdag/core/ports_builder.py +623 -0
- hexdag/core/protocols.py +273 -0
- hexdag/core/resolver.py +304 -0
- hexdag/core/schema/__init__.py +9 -0
- hexdag/core/schema/generator.py +742 -0
- hexdag/core/secrets.py +242 -0
- hexdag/core/types.py +413 -0
- hexdag/core/utils/async_warnings.py +206 -0
- hexdag/core/utils/schema_conversion.py +78 -0
- hexdag/core/utils/sql_validation.py +86 -0
- hexdag/core/validation/secure_json.py +148 -0
- hexdag/core/yaml_macro.py +517 -0
- hexdag/mcp_server.py +3120 -0
- hexdag/studio/__init__.py +10 -0
- hexdag/studio/build_ui.py +92 -0
- hexdag/studio/server/__init__.py +1 -0
- hexdag/studio/server/main.py +100 -0
- hexdag/studio/server/routes/__init__.py +9 -0
- hexdag/studio/server/routes/execute.py +208 -0
- hexdag/studio/server/routes/export.py +558 -0
- hexdag/studio/server/routes/files.py +207 -0
- hexdag/studio/server/routes/plugins.py +419 -0
- hexdag/studio/server/routes/validate.py +220 -0
- hexdag/studio/ui/index.html +13 -0
- hexdag/studio/ui/package-lock.json +2992 -0
- hexdag/studio/ui/package.json +31 -0
- hexdag/studio/ui/postcss.config.js +6 -0
- hexdag/studio/ui/public/hexdag.svg +5 -0
- hexdag/studio/ui/src/App.tsx +251 -0
- hexdag/studio/ui/src/components/Canvas.tsx +408 -0
- hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
- hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
- hexdag/studio/ui/src/components/Header.tsx +181 -0
- hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
- hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
- hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
- hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
- hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
- hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
- hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
- hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
- hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
- hexdag/studio/ui/src/components/index.ts +8 -0
- hexdag/studio/ui/src/index.css +92 -0
- hexdag/studio/ui/src/main.tsx +10 -0
- hexdag/studio/ui/src/types/index.ts +123 -0
- hexdag/studio/ui/src/vite-env.d.ts +1 -0
- hexdag/studio/ui/tailwind.config.js +29 -0
- hexdag/studio/ui/tsconfig.json +37 -0
- hexdag/studio/ui/tsconfig.node.json +13 -0
- hexdag/studio/ui/vite.config.ts +35 -0
- hexdag/visualization/__init__.py +69 -0
- hexdag/visualization/dag_visualizer.py +1020 -0
- hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
- hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
- hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
- hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
- hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
- hexdag_plugins/.gitignore +43 -0
- hexdag_plugins/README.md +73 -0
- hexdag_plugins/__init__.py +1 -0
- hexdag_plugins/azure/LICENSE +21 -0
- hexdag_plugins/azure/README.md +414 -0
- hexdag_plugins/azure/__init__.py +21 -0
- hexdag_plugins/azure/azure_blob_adapter.py +450 -0
- hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
- hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
- hexdag_plugins/azure/azure_openai_adapter.py +415 -0
- hexdag_plugins/azure/pyproject.toml +107 -0
- hexdag_plugins/azure/tests/__init__.py +1 -0
- hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
- hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
- hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
- hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
- hexdag_plugins/hexdag_etl/README.md +168 -0
- hexdag_plugins/hexdag_etl/__init__.py +53 -0
- hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
- hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
- hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
- hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
- hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
- hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
- hexdag_plugins/hexdag_etl/test_transform.py +54 -0
- hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
- hexdag_plugins/mysql_adapter/LICENSE +21 -0
- hexdag_plugins/mysql_adapter/README.md +224 -0
- hexdag_plugins/mysql_adapter/__init__.py +6 -0
- hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
- hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
- hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
- hexdag_plugins/storage/README.md +184 -0
- hexdag_plugins/storage/__init__.py +19 -0
- hexdag_plugins/storage/file/__init__.py +5 -0
- hexdag_plugins/storage/file/local.py +325 -0
- hexdag_plugins/storage/ports/__init__.py +5 -0
- hexdag_plugins/storage/ports/vector_store.py +236 -0
- hexdag_plugins/storage/sql/__init__.py +7 -0
- hexdag_plugins/storage/sql/base.py +187 -0
- hexdag_plugins/storage/sql/mysql.py +27 -0
- hexdag_plugins/storage/sql/postgresql.py +27 -0
- hexdag_plugins/storage/tests/__init__.py +1 -0
- hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
- hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
- hexdag_plugins/storage/vector/__init__.py +7 -0
- hexdag_plugins/storage/vector/chromadb.py +223 -0
- hexdag_plugins/storage/vector/in_memory.py +285 -0
- hexdag_plugins/storage/vector/pgvector.py +502 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Static data node for returning constant output.
|
|
2
|
+
|
|
3
|
+
.. deprecated::
|
|
4
|
+
DataNode is deprecated. Use ExpressionNode directly instead.
|
|
5
|
+
DataNode already delegates to ExpressionNode internally.
|
|
6
|
+
|
|
7
|
+
This module provides a DataNode factory for creating nodes that return
|
|
8
|
+
static data without requiring Python functions. Useful for terminal
|
|
9
|
+
nodes like rejection actions or static configuration.
|
|
10
|
+
|
|
11
|
+
DataNode now delegates to ExpressionNode internally, supporting both
|
|
12
|
+
static output and template syntax ({{variable}}).
|
|
13
|
+
|
|
14
|
+
Examples
|
|
15
|
+
--------
|
|
16
|
+
Basic usage in Python::
|
|
17
|
+
|
|
18
|
+
from hexdag.builtin.nodes import DataNode
|
|
19
|
+
|
|
20
|
+
node_factory = DataNode()
|
|
21
|
+
node = node_factory(
|
|
22
|
+
name="reject_locked",
|
|
23
|
+
output={"action": "REJECTED", "reason": "Load already has winner locked"}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
YAML pipeline usage::
|
|
27
|
+
|
|
28
|
+
- kind: data_node
|
|
29
|
+
metadata:
|
|
30
|
+
name: reject_locked
|
|
31
|
+
spec:
|
|
32
|
+
output:
|
|
33
|
+
action: "REJECTED"
|
|
34
|
+
reason: "Load already has winner locked"
|
|
35
|
+
|
|
36
|
+
With template syntax::
|
|
37
|
+
|
|
38
|
+
- kind: data_node
|
|
39
|
+
metadata:
|
|
40
|
+
name: welcome_message
|
|
41
|
+
spec:
|
|
42
|
+
output:
|
|
43
|
+
message: "Welcome {{user.name}}!"
|
|
44
|
+
type: "greeting"
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
import warnings
|
|
48
|
+
from typing import Any
|
|
49
|
+
|
|
50
|
+
from hexdag.core.domain.dag import NodeSpec
|
|
51
|
+
from hexdag.core.logging import get_logger
|
|
52
|
+
|
|
53
|
+
from .base_node_factory import BaseNodeFactory
|
|
54
|
+
from .expression_node import ExpressionNode, _is_template
|
|
55
|
+
|
|
56
|
+
logger = get_logger(__name__)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _value_to_expression(value: Any) -> str:
|
|
60
|
+
"""Convert a value to an expression string.
|
|
61
|
+
|
|
62
|
+
For strings without templates, wraps in quotes.
|
|
63
|
+
For strings with templates, passes through.
|
|
64
|
+
For other types, uses repr().
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
value : Any
|
|
69
|
+
The value to convert
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
str
|
|
74
|
+
An expression string that evaluates to the original value
|
|
75
|
+
"""
|
|
76
|
+
if isinstance(value, str):
|
|
77
|
+
if _is_template(value):
|
|
78
|
+
# Template syntax - pass through for PromptTemplate rendering
|
|
79
|
+
return value
|
|
80
|
+
# Static string - wrap as literal expression
|
|
81
|
+
return repr(value)
|
|
82
|
+
if isinstance(value, bool):
|
|
83
|
+
# Bool must come before int since bool is subclass of int
|
|
84
|
+
return repr(value)
|
|
85
|
+
if isinstance(value, (int, float)):
|
|
86
|
+
return repr(value)
|
|
87
|
+
if value is None:
|
|
88
|
+
return "None"
|
|
89
|
+
# For complex types (dict, list), use repr
|
|
90
|
+
# Note: This has limitations for nested dicts with templates
|
|
91
|
+
return repr(value)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DataNode(BaseNodeFactory):
|
|
95
|
+
"""Static data node factory that returns constant output.
|
|
96
|
+
|
|
97
|
+
.. deprecated::
|
|
98
|
+
DataNode is deprecated. Use ExpressionNode directly instead.
|
|
99
|
+
This node already delegates to ExpressionNode internally.
|
|
100
|
+
|
|
101
|
+
This node type eliminates the need for trivial Python functions
|
|
102
|
+
that simply return static dictionaries. The output is defined
|
|
103
|
+
declaratively in the YAML configuration.
|
|
104
|
+
|
|
105
|
+
Internally delegates to ExpressionNode for unified template/expression
|
|
106
|
+
handling. Supports {{variable}} template syntax for dynamic values.
|
|
107
|
+
|
|
108
|
+
The node ignores any input data and always returns the configured
|
|
109
|
+
output. Dependencies can still be specified to control execution
|
|
110
|
+
order in the DAG.
|
|
111
|
+
|
|
112
|
+
The YAML schema for this node is auto-generated from the ``__call__`` signature
|
|
113
|
+
and docstrings using ``SchemaGenerator``.
|
|
114
|
+
|
|
115
|
+
Examples
|
|
116
|
+
--------
|
|
117
|
+
>>> factory = DataNode()
|
|
118
|
+
>>> node = factory(
|
|
119
|
+
... name="static_response",
|
|
120
|
+
... output={"status": "OK", "code": 200}
|
|
121
|
+
... )
|
|
122
|
+
>>> node.name
|
|
123
|
+
'static_response'
|
|
124
|
+
|
|
125
|
+
With dependencies::
|
|
126
|
+
|
|
127
|
+
>>> node = factory(
|
|
128
|
+
... name="after_validation",
|
|
129
|
+
... output={"result": "validated"},
|
|
130
|
+
... deps=["validator"]
|
|
131
|
+
... )
|
|
132
|
+
>>> "validator" in node.deps
|
|
133
|
+
True
|
|
134
|
+
|
|
135
|
+
With templates::
|
|
136
|
+
|
|
137
|
+
>>> node = factory(
|
|
138
|
+
... name="greeting",
|
|
139
|
+
... output={"message": "Hello {{name}}!"},
|
|
140
|
+
... deps=["user_lookup"]
|
|
141
|
+
... )
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
# Schema is auto-generated from __call__ signature by SchemaGenerator
|
|
145
|
+
|
|
146
|
+
def __call__(
|
|
147
|
+
self,
|
|
148
|
+
name: str,
|
|
149
|
+
output: dict[str, Any],
|
|
150
|
+
deps: list[str] | None = None,
|
|
151
|
+
**kwargs: Any,
|
|
152
|
+
) -> NodeSpec:
|
|
153
|
+
"""Create a NodeSpec for a data node.
|
|
154
|
+
|
|
155
|
+
Parameters
|
|
156
|
+
----------
|
|
157
|
+
name : str
|
|
158
|
+
Node name (must be unique within the pipeline)
|
|
159
|
+
output : dict[str, Any]
|
|
160
|
+
Output data to return. Values can be:
|
|
161
|
+
- Static values (strings, numbers, bools, etc.)
|
|
162
|
+
- Template strings using {{variable}} syntax
|
|
163
|
+
deps : list[str] | None, optional
|
|
164
|
+
List of dependency node names for execution ordering
|
|
165
|
+
**kwargs : Any
|
|
166
|
+
Additional parameters (when, timeout, etc.)
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
NodeSpec
|
|
171
|
+
Complete node specification ready for execution
|
|
172
|
+
|
|
173
|
+
Examples
|
|
174
|
+
--------
|
|
175
|
+
>>> factory = DataNode()
|
|
176
|
+
>>> node = factory(
|
|
177
|
+
... name="reject_locked",
|
|
178
|
+
... output={"action": "REJECTED", "reason": "Load locked"}
|
|
179
|
+
... )
|
|
180
|
+
>>> node.name
|
|
181
|
+
'reject_locked'
|
|
182
|
+
"""
|
|
183
|
+
warnings.warn(
|
|
184
|
+
"DataNode is deprecated. Use ExpressionNode directly instead.",
|
|
185
|
+
DeprecationWarning,
|
|
186
|
+
stacklevel=2,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Convert output values to expressions
|
|
190
|
+
expressions: dict[str, str] = {}
|
|
191
|
+
for key, value in output.items():
|
|
192
|
+
expressions[key] = _value_to_expression(value)
|
|
193
|
+
|
|
194
|
+
# Delegate to ExpressionNode
|
|
195
|
+
return ExpressionNode()(
|
|
196
|
+
name=name,
|
|
197
|
+
expressions=expressions,
|
|
198
|
+
output_fields=list(output.keys()),
|
|
199
|
+
deps=deps,
|
|
200
|
+
**kwargs,
|
|
201
|
+
)
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
"""ExpressionNode factory for creating expression-based computation nodes.
|
|
2
|
+
|
|
3
|
+
This module provides a node type that computes values using safe AST-based
|
|
4
|
+
expressions, eliminating the need for boilerplate Python code when performing
|
|
5
|
+
calculations and transformations in YAML pipelines.
|
|
6
|
+
|
|
7
|
+
Similar to n8n's "Set Node" (Edit Fields), ExpressionNode is designed for
|
|
8
|
+
data transformation and computation. It also supports merge strategies for
|
|
9
|
+
aggregating outputs from multiple upstream dependency nodes.
|
|
10
|
+
|
|
11
|
+
Examples
|
|
12
|
+
--------
|
|
13
|
+
Basic usage in YAML::
|
|
14
|
+
|
|
15
|
+
- kind: expression_node
|
|
16
|
+
metadata:
|
|
17
|
+
name: calculate_discount
|
|
18
|
+
spec:
|
|
19
|
+
input_mapping:
|
|
20
|
+
price: "product.price"
|
|
21
|
+
quantity: "order.quantity"
|
|
22
|
+
expressions:
|
|
23
|
+
subtotal: "price * quantity"
|
|
24
|
+
discount: "0.1 if quantity > 10 else 0"
|
|
25
|
+
total: "subtotal * (1 - discount)"
|
|
26
|
+
output_fields: [total, discount]
|
|
27
|
+
|
|
28
|
+
Financial calculations with Decimal::
|
|
29
|
+
|
|
30
|
+
- kind: expression_node
|
|
31
|
+
metadata:
|
|
32
|
+
name: calculate_counter
|
|
33
|
+
spec:
|
|
34
|
+
input_mapping:
|
|
35
|
+
offered_rate: "extract_offer.rate"
|
|
36
|
+
rate_floor: "get_context.load.rate_floor"
|
|
37
|
+
counter_count: "get_context.negotiation.counter_count"
|
|
38
|
+
expressions:
|
|
39
|
+
discount: "Decimal('0.10') if counter_count == 0 else Decimal('0.03')"
|
|
40
|
+
floor_val: "Decimal(str(rate_floor or 0))"
|
|
41
|
+
counter_amount: "float(max(Decimal(str(offered_rate)) * (1 - discount), floor_val))"
|
|
42
|
+
output_fields: [counter_amount]
|
|
43
|
+
|
|
44
|
+
Template syntax for string formatting::
|
|
45
|
+
|
|
46
|
+
- kind: expression_node
|
|
47
|
+
metadata:
|
|
48
|
+
name: format_response
|
|
49
|
+
spec:
|
|
50
|
+
input_mapping:
|
|
51
|
+
name: "user.name"
|
|
52
|
+
quantity: "order.quantity"
|
|
53
|
+
expressions:
|
|
54
|
+
# Template syntax for strings (detected by {{ }})
|
|
55
|
+
message: "{{name}} ordered {{quantity}} items"
|
|
56
|
+
greeting: "Hello {{name}}!"
|
|
57
|
+
# Expression syntax for computation
|
|
58
|
+
total: "price * quantity"
|
|
59
|
+
output_fields: [message, greeting, total]
|
|
60
|
+
|
|
61
|
+
Merge strategies for multi-dependency aggregation::
|
|
62
|
+
|
|
63
|
+
# Collect scores from multiple nodes into a list
|
|
64
|
+
- kind: expression_node
|
|
65
|
+
metadata:
|
|
66
|
+
name: collect_scores
|
|
67
|
+
spec:
|
|
68
|
+
merge_strategy: list
|
|
69
|
+
extract_field: score
|
|
70
|
+
dependencies: [scorer_1, scorer_2, scorer_3]
|
|
71
|
+
|
|
72
|
+
# Calculate average score using reduce
|
|
73
|
+
- kind: expression_node
|
|
74
|
+
metadata:
|
|
75
|
+
name: average_score
|
|
76
|
+
spec:
|
|
77
|
+
merge_strategy: reduce
|
|
78
|
+
extract_field: score
|
|
79
|
+
reducer: "statistics.mean"
|
|
80
|
+
dependencies: [scorer_1, scorer_2, scorer_3]
|
|
81
|
+
|
|
82
|
+
# Get first successful result (fallback pattern)
|
|
83
|
+
- kind: expression_node
|
|
84
|
+
metadata:
|
|
85
|
+
name: get_result
|
|
86
|
+
spec:
|
|
87
|
+
merge_strategy: first
|
|
88
|
+
dependencies: [primary, fallback, cache]
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
from collections.abc import Callable
|
|
92
|
+
from typing import Any, Literal
|
|
93
|
+
|
|
94
|
+
from hexdag.builtin.nodes.base_node_factory import BaseNodeFactory
|
|
95
|
+
from hexdag.core.domain.dag import NodeSpec
|
|
96
|
+
from hexdag.core.expression_parser import evaluate_expression
|
|
97
|
+
from hexdag.core.logging import get_logger
|
|
98
|
+
from hexdag.core.orchestration.prompt.template import PromptTemplate
|
|
99
|
+
from hexdag.core.resolver import resolve_function
|
|
100
|
+
|
|
101
|
+
logger = get_logger(__name__)
|
|
102
|
+
|
|
103
|
+
# Type alias for merge strategies
|
|
104
|
+
MergeStrategy = Literal["dict", "list", "first", "last", "reduce"]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _is_template(expr: str) -> bool:
|
|
108
|
+
"""Check if expression uses template syntax (contains {{ }})."""
|
|
109
|
+
return "{{" in expr and "}}" in expr
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _extract_field(value: Any, field: str | None) -> Any:
|
|
113
|
+
"""Extract a field from a value if specified.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
value : Any
|
|
118
|
+
The value to extract from
|
|
119
|
+
field : str | None
|
|
120
|
+
Dot-notation field path to extract (e.g., "result.score")
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
Any
|
|
125
|
+
The extracted field value, or the original value if no field specified
|
|
126
|
+
"""
|
|
127
|
+
if field is None:
|
|
128
|
+
return value
|
|
129
|
+
|
|
130
|
+
result = value
|
|
131
|
+
for part in field.split("."):
|
|
132
|
+
if isinstance(result, dict):
|
|
133
|
+
result = result.get(part)
|
|
134
|
+
elif hasattr(result, part):
|
|
135
|
+
result = getattr(result, part)
|
|
136
|
+
else:
|
|
137
|
+
return None
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _apply_merge_strategy(
|
|
142
|
+
input_data: dict[str, Any],
|
|
143
|
+
strategy: MergeStrategy,
|
|
144
|
+
field_path: str | None,
|
|
145
|
+
reducer: Callable[[list[Any]], Any] | None,
|
|
146
|
+
dep_order: list[str],
|
|
147
|
+
) -> Any:
|
|
148
|
+
"""Apply merge strategy to multi-dependency input.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
input_data : dict[str, Any]
|
|
153
|
+
Dict of {node_name: result} from dependencies
|
|
154
|
+
strategy : MergeStrategy
|
|
155
|
+
The merge strategy to apply
|
|
156
|
+
field_path : str | None
|
|
157
|
+
Field to extract from each result before merging (dot notation)
|
|
158
|
+
reducer : Callable | None
|
|
159
|
+
Reducer function for 'reduce' strategy
|
|
160
|
+
dep_order : list[str]
|
|
161
|
+
Ordered list of dependency names for consistent ordering
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
Any
|
|
166
|
+
The merged result
|
|
167
|
+
"""
|
|
168
|
+
# Get values in dependency order, extracting field if specified
|
|
169
|
+
values = []
|
|
170
|
+
for dep in dep_order:
|
|
171
|
+
if dep in input_data:
|
|
172
|
+
val = _extract_field(input_data[dep], field_path)
|
|
173
|
+
values.append(val)
|
|
174
|
+
|
|
175
|
+
match strategy:
|
|
176
|
+
case "dict":
|
|
177
|
+
# Return as-is (passthrough) or with field extraction
|
|
178
|
+
if field_path:
|
|
179
|
+
return {
|
|
180
|
+
dep: _extract_field(input_data[dep], field_path)
|
|
181
|
+
for dep in dep_order
|
|
182
|
+
if dep in input_data
|
|
183
|
+
}
|
|
184
|
+
return dict(input_data)
|
|
185
|
+
|
|
186
|
+
case "list":
|
|
187
|
+
return values
|
|
188
|
+
|
|
189
|
+
case "first":
|
|
190
|
+
for val in values:
|
|
191
|
+
if val is not None:
|
|
192
|
+
return val
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
case "last":
|
|
196
|
+
for val in reversed(values):
|
|
197
|
+
if val is not None:
|
|
198
|
+
return val
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
case "reduce":
|
|
202
|
+
if reducer is None:
|
|
203
|
+
raise ValueError("reducer is required for 'reduce' strategy")
|
|
204
|
+
# Filter out None values
|
|
205
|
+
non_none_values = [v for v in values if v is not None]
|
|
206
|
+
if not non_none_values:
|
|
207
|
+
return None
|
|
208
|
+
return reducer(non_none_values)
|
|
209
|
+
|
|
210
|
+
case _:
|
|
211
|
+
raise ValueError(f"Unknown merge strategy: {strategy}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class ExpressionNode(BaseNodeFactory):
|
|
215
|
+
"""Node factory for computing values using safe AST-based expressions.
|
|
216
|
+
|
|
217
|
+
ExpressionNode eliminates dict packing/unpacking boilerplate by:
|
|
218
|
+
1. Auto-extracting input fields via input_mapping (handled by orchestrator)
|
|
219
|
+
2. Evaluating chained expressions in definition order
|
|
220
|
+
3. Filtering output to specified fields
|
|
221
|
+
|
|
222
|
+
It also supports merge strategies for aggregating outputs from multiple
|
|
223
|
+
upstream dependency nodes:
|
|
224
|
+
- dict: Return {node_name: result, ...} (default passthrough)
|
|
225
|
+
- list: Return [result1, result2, ...] in dependency order
|
|
226
|
+
- first: Return first non-None result
|
|
227
|
+
- last: Return last non-None result
|
|
228
|
+
- reduce: Apply reducer function (e.g., statistics.mean)
|
|
229
|
+
|
|
230
|
+
This node uses the same safe expression parser as CompositeNode, but
|
|
231
|
+
returns computed values instead of routing decisions.
|
|
232
|
+
|
|
233
|
+
See Also
|
|
234
|
+
--------
|
|
235
|
+
CompositeNode : For control flow (loops, conditionals)
|
|
236
|
+
FunctionNode : For complex logic requiring full Python functions
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
# Schema is auto-generated from __call__ signature by SchemaGenerator
|
|
240
|
+
|
|
241
|
+
def __call__(
|
|
242
|
+
self,
|
|
243
|
+
name: str,
|
|
244
|
+
expressions: dict[str, str] | None = None,
|
|
245
|
+
input_mapping: dict[str, str] | None = None,
|
|
246
|
+
output_fields: list[str] | None = None,
|
|
247
|
+
deps: list[str] | None = None,
|
|
248
|
+
# Merge strategy parameters
|
|
249
|
+
merge_strategy: MergeStrategy | None = None,
|
|
250
|
+
reducer: str | Callable[[list[Any]], Any] | None = None,
|
|
251
|
+
extract_field: str | None = None,
|
|
252
|
+
**kwargs: Any,
|
|
253
|
+
) -> NodeSpec:
|
|
254
|
+
"""Create an ExpressionNode for computing values or merging dependencies.
|
|
255
|
+
|
|
256
|
+
Parameters
|
|
257
|
+
----------
|
|
258
|
+
name : str
|
|
259
|
+
Node name (unique identifier in the pipeline)
|
|
260
|
+
expressions : dict[str, str] | None
|
|
261
|
+
Mapping of {variable_name: expression_string}.
|
|
262
|
+
Expressions are evaluated in definition order and can reference:
|
|
263
|
+
- Input fields from input_mapping
|
|
264
|
+
- Earlier computed variables
|
|
265
|
+
- Whitelisted functions (len, max, min, Decimal, etc.)
|
|
266
|
+
Optional when using merge_strategy.
|
|
267
|
+
input_mapping : dict[str, str] | None
|
|
268
|
+
Field extraction mapping {local_name: "source_node.field_path"}.
|
|
269
|
+
Handled by the orchestrator's ExecutionCoordinator before node runs.
|
|
270
|
+
output_fields : list[str] | None
|
|
271
|
+
Fields to include in output dict. If None, all computed expressions
|
|
272
|
+
are returned.
|
|
273
|
+
deps : list[str] | None
|
|
274
|
+
Dependency node names (for DAG ordering)
|
|
275
|
+
merge_strategy : MergeStrategy | None
|
|
276
|
+
Strategy for merging multiple dependency outputs:
|
|
277
|
+
- "dict": Return {node_name: result} passthrough (default for multi-dep)
|
|
278
|
+
- "list": Return [result1, result2, ...] in dependency order
|
|
279
|
+
- "first": Return first non-None result
|
|
280
|
+
- "last": Return last non-None result
|
|
281
|
+
- "reduce": Apply reducer function to values
|
|
282
|
+
reducer : str | Callable | None
|
|
283
|
+
Module path (e.g., "statistics.mean") or callable for 'reduce' strategy.
|
|
284
|
+
The function receives a list of values and returns a single result.
|
|
285
|
+
extract_field : str | None
|
|
286
|
+
Field to extract from each dependency result before merging.
|
|
287
|
+
Uses dot notation (e.g., "result.score").
|
|
288
|
+
**kwargs : Any
|
|
289
|
+
Additional parameters passed to NodeSpec
|
|
290
|
+
|
|
291
|
+
Returns
|
|
292
|
+
-------
|
|
293
|
+
NodeSpec
|
|
294
|
+
Configured node specification ready for execution
|
|
295
|
+
|
|
296
|
+
Examples
|
|
297
|
+
--------
|
|
298
|
+
Programmatic usage for expressions::
|
|
299
|
+
|
|
300
|
+
node = ExpressionNode()(
|
|
301
|
+
name="calculate_total",
|
|
302
|
+
expressions={
|
|
303
|
+
"subtotal": "price * quantity",
|
|
304
|
+
"tax": "subtotal * 0.08",
|
|
305
|
+
"total": "subtotal + tax",
|
|
306
|
+
},
|
|
307
|
+
input_mapping={
|
|
308
|
+
"price": "product.price",
|
|
309
|
+
"quantity": "order.quantity",
|
|
310
|
+
},
|
|
311
|
+
output_fields=["total"],
|
|
312
|
+
deps=["product", "order"],
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
Programmatic usage for merging::
|
|
316
|
+
|
|
317
|
+
node = ExpressionNode()(
|
|
318
|
+
name="average_score",
|
|
319
|
+
merge_strategy="reduce",
|
|
320
|
+
extract_field="score",
|
|
321
|
+
reducer="statistics.mean",
|
|
322
|
+
deps=["scorer_1", "scorer_2", "scorer_3"],
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
Notes
|
|
326
|
+
-----
|
|
327
|
+
ValueError may be raised at runtime if an expression fails to evaluate
|
|
328
|
+
or references undefined variables.
|
|
329
|
+
"""
|
|
330
|
+
# Validate: either expressions or merge_strategy must be provided
|
|
331
|
+
if expressions is None and merge_strategy is None:
|
|
332
|
+
raise ValueError("Either 'expressions' or 'merge_strategy' must be provided")
|
|
333
|
+
|
|
334
|
+
# Validate: reducer is required for reduce strategy
|
|
335
|
+
if merge_strategy == "reduce" and reducer is None:
|
|
336
|
+
raise ValueError("'reducer' is required when merge_strategy='reduce'")
|
|
337
|
+
|
|
338
|
+
# Store input_mapping in params for orchestrator to handle
|
|
339
|
+
if input_mapping is not None:
|
|
340
|
+
kwargs["input_mapping"] = input_mapping
|
|
341
|
+
|
|
342
|
+
# Resolve reducer if it's a string module path (e.g., "statistics.mean")
|
|
343
|
+
resolved_reducer: Callable[[list[Any]], Any] | None = None
|
|
344
|
+
if reducer is not None:
|
|
345
|
+
resolved_reducer = resolve_function(reducer) if isinstance(reducer, str) else reducer
|
|
346
|
+
|
|
347
|
+
# Capture for closure
|
|
348
|
+
_expressions = expressions or {}
|
|
349
|
+
_output_fields = output_fields or list(_expressions.keys()) if _expressions else None
|
|
350
|
+
_merge_strategy = merge_strategy
|
|
351
|
+
_reducer = resolved_reducer
|
|
352
|
+
_extract_field = extract_field
|
|
353
|
+
_dep_order = list(deps or [])
|
|
354
|
+
|
|
355
|
+
async def expression_fn(input_data: Any, **ports: Any) -> dict[str, Any] | Any:
|
|
356
|
+
"""Evaluate expressions and/or apply merge strategy.
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
input_data : Any
|
|
361
|
+
Input data (typically dict after input_mapping is applied)
|
|
362
|
+
**ports : Any
|
|
363
|
+
Injected ports (memory, llm, etc.) - usually unused for expressions
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
dict[str, Any] | Any
|
|
368
|
+
Computed values filtered to output_fields, or merged result
|
|
369
|
+
"""
|
|
370
|
+
node_logger = logger.bind(node=name, node_type="expression_node")
|
|
371
|
+
|
|
372
|
+
# Apply merge strategy first if specified
|
|
373
|
+
if _merge_strategy is not None and isinstance(input_data, dict):
|
|
374
|
+
node_logger.info(
|
|
375
|
+
"Applying merge strategy",
|
|
376
|
+
strategy=_merge_strategy,
|
|
377
|
+
extract_field=_extract_field,
|
|
378
|
+
dep_count=len(_dep_order),
|
|
379
|
+
)
|
|
380
|
+
merged = _apply_merge_strategy(
|
|
381
|
+
input_data=input_data,
|
|
382
|
+
strategy=_merge_strategy,
|
|
383
|
+
field_path=_extract_field,
|
|
384
|
+
reducer=_reducer,
|
|
385
|
+
dep_order=_dep_order,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# If no expressions, return merged result directly
|
|
389
|
+
if not _expressions:
|
|
390
|
+
node_logger.info(
|
|
391
|
+
"Merge complete (no expressions)",
|
|
392
|
+
result_type=type(merged).__name__,
|
|
393
|
+
)
|
|
394
|
+
return {"result": merged}
|
|
395
|
+
|
|
396
|
+
# Otherwise, make merged result available for expressions
|
|
397
|
+
input_data = merged if isinstance(merged, dict) else {"merged": merged}
|
|
398
|
+
|
|
399
|
+
node_logger.info(
|
|
400
|
+
"Evaluating expressions",
|
|
401
|
+
expression_count=len(_expressions),
|
|
402
|
+
output_fields=_output_fields,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Build context from input data
|
|
406
|
+
context: dict[str, Any] = {}
|
|
407
|
+
if isinstance(input_data, dict):
|
|
408
|
+
context.update(input_data)
|
|
409
|
+
elif input_data is not None:
|
|
410
|
+
# Non-dict input - make it available as '_input'
|
|
411
|
+
context["_input"] = input_data
|
|
412
|
+
|
|
413
|
+
# Evaluate expressions in definition order (supports chaining)
|
|
414
|
+
for var_name, expr in _expressions.items():
|
|
415
|
+
try:
|
|
416
|
+
# Check if expression uses template syntax ({{ }})
|
|
417
|
+
if _is_template(expr):
|
|
418
|
+
# Use PromptTemplate for string rendering
|
|
419
|
+
template = PromptTemplate(expr)
|
|
420
|
+
value = template.render(**context)
|
|
421
|
+
node_logger.debug(
|
|
422
|
+
"Template rendered",
|
|
423
|
+
variable=var_name,
|
|
424
|
+
template=expr,
|
|
425
|
+
result_type=type(value).__name__,
|
|
426
|
+
)
|
|
427
|
+
else:
|
|
428
|
+
# Use expression parser for computation
|
|
429
|
+
value = evaluate_expression(expr, context, state={})
|
|
430
|
+
node_logger.debug(
|
|
431
|
+
"Expression evaluated",
|
|
432
|
+
variable=var_name,
|
|
433
|
+
expression=expr,
|
|
434
|
+
result_type=type(value).__name__,
|
|
435
|
+
)
|
|
436
|
+
context[var_name] = value
|
|
437
|
+
except Exception as e:
|
|
438
|
+
node_logger.error(
|
|
439
|
+
"Expression/template evaluation failed",
|
|
440
|
+
variable=var_name,
|
|
441
|
+
expression=expr,
|
|
442
|
+
error=str(e),
|
|
443
|
+
error_type=type(e).__name__,
|
|
444
|
+
)
|
|
445
|
+
raise ValueError(
|
|
446
|
+
f"Expression '{var_name}' failed: {e}\n"
|
|
447
|
+
f" Expression: {expr}\n"
|
|
448
|
+
f" Available context: {list(context.keys())}"
|
|
449
|
+
) from e
|
|
450
|
+
|
|
451
|
+
# Filter to output_fields only
|
|
452
|
+
result: dict[str, Any] = {}
|
|
453
|
+
fields_to_output = _output_fields or list(_expressions.keys())
|
|
454
|
+
for field in fields_to_output:
|
|
455
|
+
if field in context:
|
|
456
|
+
result[field] = context[field]
|
|
457
|
+
else:
|
|
458
|
+
node_logger.warning(
|
|
459
|
+
"Output field not found in context",
|
|
460
|
+
field=field,
|
|
461
|
+
available=list(context.keys()),
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
node_logger.info(
|
|
465
|
+
"Expression evaluation complete",
|
|
466
|
+
output_keys=list(result.keys()),
|
|
467
|
+
)
|
|
468
|
+
return result
|
|
469
|
+
|
|
470
|
+
# Preserve function metadata for debugging
|
|
471
|
+
expression_fn.__name__ = f"expression_{name}"
|
|
472
|
+
expression_fn.__doc__ = f"Expression node: {name}"
|
|
473
|
+
|
|
474
|
+
# Extract framework-level parameters from kwargs
|
|
475
|
+
framework = self.extract_framework_params(kwargs)
|
|
476
|
+
|
|
477
|
+
return NodeSpec(
|
|
478
|
+
name=name,
|
|
479
|
+
fn=expression_fn,
|
|
480
|
+
in_model=None, # Accepts any dict input
|
|
481
|
+
out_model=None, # Returns dict output
|
|
482
|
+
deps=frozenset(deps or []),
|
|
483
|
+
params=kwargs,
|
|
484
|
+
timeout=framework["timeout"],
|
|
485
|
+
max_retries=framework["max_retries"],
|
|
486
|
+
when=framework["when"],
|
|
487
|
+
)
|