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.
Files changed (261) hide show
  1. hexdag/__init__.py +116 -0
  2. hexdag/__main__.py +30 -0
  3. hexdag/adapters/executors/__init__.py +5 -0
  4. hexdag/adapters/executors/local_executor.py +316 -0
  5. hexdag/builtin/__init__.py +6 -0
  6. hexdag/builtin/adapters/__init__.py +51 -0
  7. hexdag/builtin/adapters/anthropic/__init__.py +5 -0
  8. hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
  9. hexdag/builtin/adapters/database/__init__.py +6 -0
  10. hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
  11. hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
  12. hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
  13. hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
  14. hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
  15. hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
  16. hexdag/builtin/adapters/local/README.md +59 -0
  17. hexdag/builtin/adapters/local/__init__.py +7 -0
  18. hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
  19. hexdag/builtin/adapters/memory/__init__.py +47 -0
  20. hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
  21. hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
  22. hexdag/builtin/adapters/memory/schemas.py +57 -0
  23. hexdag/builtin/adapters/memory/session_memory.py +178 -0
  24. hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
  25. hexdag/builtin/adapters/memory/state_memory.py +280 -0
  26. hexdag/builtin/adapters/mock/README.md +89 -0
  27. hexdag/builtin/adapters/mock/__init__.py +15 -0
  28. hexdag/builtin/adapters/mock/hexdag.toml +50 -0
  29. hexdag/builtin/adapters/mock/mock_database.py +225 -0
  30. hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
  31. hexdag/builtin/adapters/mock/mock_llm.py +177 -0
  32. hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
  33. hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
  34. hexdag/builtin/adapters/openai/__init__.py +5 -0
  35. hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
  36. hexdag/builtin/adapters/secret/__init__.py +7 -0
  37. hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
  38. hexdag/builtin/adapters/unified_tool_router.py +280 -0
  39. hexdag/builtin/macros/__init__.py +17 -0
  40. hexdag/builtin/macros/conversation_agent.py +390 -0
  41. hexdag/builtin/macros/llm_macro.py +151 -0
  42. hexdag/builtin/macros/reasoning_agent.py +423 -0
  43. hexdag/builtin/macros/tool_macro.py +380 -0
  44. hexdag/builtin/nodes/__init__.py +38 -0
  45. hexdag/builtin/nodes/_discovery.py +123 -0
  46. hexdag/builtin/nodes/agent_node.py +696 -0
  47. hexdag/builtin/nodes/base_node_factory.py +242 -0
  48. hexdag/builtin/nodes/composite_node.py +926 -0
  49. hexdag/builtin/nodes/data_node.py +201 -0
  50. hexdag/builtin/nodes/expression_node.py +487 -0
  51. hexdag/builtin/nodes/function_node.py +454 -0
  52. hexdag/builtin/nodes/llm_node.py +491 -0
  53. hexdag/builtin/nodes/loop_node.py +920 -0
  54. hexdag/builtin/nodes/mapped_input.py +518 -0
  55. hexdag/builtin/nodes/port_call_node.py +269 -0
  56. hexdag/builtin/nodes/tool_call_node.py +195 -0
  57. hexdag/builtin/nodes/tool_utils.py +390 -0
  58. hexdag/builtin/prompts/__init__.py +68 -0
  59. hexdag/builtin/prompts/base.py +422 -0
  60. hexdag/builtin/prompts/chat_prompts.py +303 -0
  61. hexdag/builtin/prompts/error_correction_prompts.py +320 -0
  62. hexdag/builtin/prompts/tool_prompts.py +160 -0
  63. hexdag/builtin/tools/builtin_tools.py +84 -0
  64. hexdag/builtin/tools/database_tools.py +164 -0
  65. hexdag/cli/__init__.py +17 -0
  66. hexdag/cli/__main__.py +7 -0
  67. hexdag/cli/commands/__init__.py +27 -0
  68. hexdag/cli/commands/build_cmd.py +812 -0
  69. hexdag/cli/commands/create_cmd.py +208 -0
  70. hexdag/cli/commands/docs_cmd.py +293 -0
  71. hexdag/cli/commands/generate_types_cmd.py +252 -0
  72. hexdag/cli/commands/init_cmd.py +188 -0
  73. hexdag/cli/commands/pipeline_cmd.py +494 -0
  74. hexdag/cli/commands/plugin_dev_cmd.py +529 -0
  75. hexdag/cli/commands/plugins_cmd.py +441 -0
  76. hexdag/cli/commands/studio_cmd.py +101 -0
  77. hexdag/cli/commands/validate_cmd.py +221 -0
  78. hexdag/cli/main.py +84 -0
  79. hexdag/core/__init__.py +83 -0
  80. hexdag/core/config/__init__.py +20 -0
  81. hexdag/core/config/loader.py +479 -0
  82. hexdag/core/config/models.py +150 -0
  83. hexdag/core/configurable.py +294 -0
  84. hexdag/core/context/__init__.py +37 -0
  85. hexdag/core/context/execution_context.py +378 -0
  86. hexdag/core/docs/__init__.py +26 -0
  87. hexdag/core/docs/extractors.py +678 -0
  88. hexdag/core/docs/generators.py +890 -0
  89. hexdag/core/docs/models.py +120 -0
  90. hexdag/core/domain/__init__.py +10 -0
  91. hexdag/core/domain/dag.py +1225 -0
  92. hexdag/core/exceptions.py +234 -0
  93. hexdag/core/expression_parser.py +569 -0
  94. hexdag/core/logging.py +449 -0
  95. hexdag/core/models/__init__.py +17 -0
  96. hexdag/core/models/base.py +138 -0
  97. hexdag/core/orchestration/__init__.py +46 -0
  98. hexdag/core/orchestration/body_executor.py +481 -0
  99. hexdag/core/orchestration/components/__init__.py +97 -0
  100. hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
  101. hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
  102. hexdag/core/orchestration/components/execution_coordinator.py +360 -0
  103. hexdag/core/orchestration/components/health_check_manager.py +176 -0
  104. hexdag/core/orchestration/components/input_mapper.py +143 -0
  105. hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
  106. hexdag/core/orchestration/components/node_executor.py +377 -0
  107. hexdag/core/orchestration/components/secret_manager.py +202 -0
  108. hexdag/core/orchestration/components/wave_executor.py +158 -0
  109. hexdag/core/orchestration/constants.py +17 -0
  110. hexdag/core/orchestration/events/README.md +312 -0
  111. hexdag/core/orchestration/events/__init__.py +104 -0
  112. hexdag/core/orchestration/events/batching.py +330 -0
  113. hexdag/core/orchestration/events/decorators.py +139 -0
  114. hexdag/core/orchestration/events/events.py +573 -0
  115. hexdag/core/orchestration/events/observers/__init__.py +30 -0
  116. hexdag/core/orchestration/events/observers/core_observers.py +690 -0
  117. hexdag/core/orchestration/events/observers/models.py +111 -0
  118. hexdag/core/orchestration/events/taxonomy.py +269 -0
  119. hexdag/core/orchestration/hook_context.py +237 -0
  120. hexdag/core/orchestration/hooks.py +437 -0
  121. hexdag/core/orchestration/models.py +418 -0
  122. hexdag/core/orchestration/orchestrator.py +910 -0
  123. hexdag/core/orchestration/orchestrator_factory.py +275 -0
  124. hexdag/core/orchestration/port_wrappers.py +327 -0
  125. hexdag/core/orchestration/prompt/__init__.py +32 -0
  126. hexdag/core/orchestration/prompt/template.py +332 -0
  127. hexdag/core/pipeline_builder/__init__.py +21 -0
  128. hexdag/core/pipeline_builder/component_instantiator.py +386 -0
  129. hexdag/core/pipeline_builder/include_tag.py +265 -0
  130. hexdag/core/pipeline_builder/pipeline_config.py +133 -0
  131. hexdag/core/pipeline_builder/py_tag.py +223 -0
  132. hexdag/core/pipeline_builder/tag_discovery.py +268 -0
  133. hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
  134. hexdag/core/pipeline_builder/yaml_validator.py +569 -0
  135. hexdag/core/ports/__init__.py +65 -0
  136. hexdag/core/ports/api_call.py +133 -0
  137. hexdag/core/ports/database.py +489 -0
  138. hexdag/core/ports/embedding.py +215 -0
  139. hexdag/core/ports/executor.py +237 -0
  140. hexdag/core/ports/file_storage.py +117 -0
  141. hexdag/core/ports/healthcheck.py +87 -0
  142. hexdag/core/ports/llm.py +551 -0
  143. hexdag/core/ports/memory.py +70 -0
  144. hexdag/core/ports/observer_manager.py +130 -0
  145. hexdag/core/ports/secret.py +145 -0
  146. hexdag/core/ports/tool_router.py +94 -0
  147. hexdag/core/ports_builder.py +623 -0
  148. hexdag/core/protocols.py +273 -0
  149. hexdag/core/resolver.py +304 -0
  150. hexdag/core/schema/__init__.py +9 -0
  151. hexdag/core/schema/generator.py +742 -0
  152. hexdag/core/secrets.py +242 -0
  153. hexdag/core/types.py +413 -0
  154. hexdag/core/utils/async_warnings.py +206 -0
  155. hexdag/core/utils/schema_conversion.py +78 -0
  156. hexdag/core/utils/sql_validation.py +86 -0
  157. hexdag/core/validation/secure_json.py +148 -0
  158. hexdag/core/yaml_macro.py +517 -0
  159. hexdag/mcp_server.py +3120 -0
  160. hexdag/studio/__init__.py +10 -0
  161. hexdag/studio/build_ui.py +92 -0
  162. hexdag/studio/server/__init__.py +1 -0
  163. hexdag/studio/server/main.py +100 -0
  164. hexdag/studio/server/routes/__init__.py +9 -0
  165. hexdag/studio/server/routes/execute.py +208 -0
  166. hexdag/studio/server/routes/export.py +558 -0
  167. hexdag/studio/server/routes/files.py +207 -0
  168. hexdag/studio/server/routes/plugins.py +419 -0
  169. hexdag/studio/server/routes/validate.py +220 -0
  170. hexdag/studio/ui/index.html +13 -0
  171. hexdag/studio/ui/package-lock.json +2992 -0
  172. hexdag/studio/ui/package.json +31 -0
  173. hexdag/studio/ui/postcss.config.js +6 -0
  174. hexdag/studio/ui/public/hexdag.svg +5 -0
  175. hexdag/studio/ui/src/App.tsx +251 -0
  176. hexdag/studio/ui/src/components/Canvas.tsx +408 -0
  177. hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
  178. hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
  179. hexdag/studio/ui/src/components/Header.tsx +181 -0
  180. hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
  181. hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
  182. hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
  183. hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
  184. hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
  185. hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
  186. hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
  187. hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
  188. hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
  189. hexdag/studio/ui/src/components/index.ts +8 -0
  190. hexdag/studio/ui/src/index.css +92 -0
  191. hexdag/studio/ui/src/main.tsx +10 -0
  192. hexdag/studio/ui/src/types/index.ts +123 -0
  193. hexdag/studio/ui/src/vite-env.d.ts +1 -0
  194. hexdag/studio/ui/tailwind.config.js +29 -0
  195. hexdag/studio/ui/tsconfig.json +37 -0
  196. hexdag/studio/ui/tsconfig.node.json +13 -0
  197. hexdag/studio/ui/vite.config.ts +35 -0
  198. hexdag/visualization/__init__.py +69 -0
  199. hexdag/visualization/dag_visualizer.py +1020 -0
  200. hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
  201. hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
  202. hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
  203. hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
  204. hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
  205. hexdag_plugins/.gitignore +43 -0
  206. hexdag_plugins/README.md +73 -0
  207. hexdag_plugins/__init__.py +1 -0
  208. hexdag_plugins/azure/LICENSE +21 -0
  209. hexdag_plugins/azure/README.md +414 -0
  210. hexdag_plugins/azure/__init__.py +21 -0
  211. hexdag_plugins/azure/azure_blob_adapter.py +450 -0
  212. hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
  213. hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
  214. hexdag_plugins/azure/azure_openai_adapter.py +415 -0
  215. hexdag_plugins/azure/pyproject.toml +107 -0
  216. hexdag_plugins/azure/tests/__init__.py +1 -0
  217. hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
  218. hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
  219. hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
  220. hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
  221. hexdag_plugins/hexdag_etl/README.md +168 -0
  222. hexdag_plugins/hexdag_etl/__init__.py +53 -0
  223. hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
  224. hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
  225. hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
  226. hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
  227. hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
  228. hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
  229. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
  230. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
  231. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
  232. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
  233. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
  234. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
  235. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
  236. hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
  237. hexdag_plugins/hexdag_etl/test_transform.py +54 -0
  238. hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
  239. hexdag_plugins/mysql_adapter/LICENSE +21 -0
  240. hexdag_plugins/mysql_adapter/README.md +224 -0
  241. hexdag_plugins/mysql_adapter/__init__.py +6 -0
  242. hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
  243. hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
  244. hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
  245. hexdag_plugins/storage/README.md +184 -0
  246. hexdag_plugins/storage/__init__.py +19 -0
  247. hexdag_plugins/storage/file/__init__.py +5 -0
  248. hexdag_plugins/storage/file/local.py +325 -0
  249. hexdag_plugins/storage/ports/__init__.py +5 -0
  250. hexdag_plugins/storage/ports/vector_store.py +236 -0
  251. hexdag_plugins/storage/sql/__init__.py +7 -0
  252. hexdag_plugins/storage/sql/base.py +187 -0
  253. hexdag_plugins/storage/sql/mysql.py +27 -0
  254. hexdag_plugins/storage/sql/postgresql.py +27 -0
  255. hexdag_plugins/storage/tests/__init__.py +1 -0
  256. hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
  257. hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
  258. hexdag_plugins/storage/vector/__init__.py +7 -0
  259. hexdag_plugins/storage/vector/chromadb.py +223 -0
  260. hexdag_plugins/storage/vector/in_memory.py +285 -0
  261. hexdag_plugins/storage/vector/pgvector.py +502 -0
@@ -0,0 +1,269 @@
1
+ """Port call node for direct adapter method invocation.
2
+
3
+ This node allows calling any method on a configured port directly from YAML,
4
+ eliminating Python wrapper functions for simple adapter operations.
5
+
6
+ Examples
7
+ --------
8
+ Basic usage in Python::
9
+
10
+ from hexdag.builtin.nodes import PortCallNode
11
+
12
+ node_factory = PortCallNode()
13
+ node = node_factory(
14
+ name="save_to_db",
15
+ port="database",
16
+ method="aexecute_query",
17
+ input_mapping={"query": "$input.sql_query"}
18
+ )
19
+
20
+ YAML pipeline usage::
21
+
22
+ - kind: port_call_node
23
+ metadata:
24
+ name: execute_accept
25
+ spec:
26
+ port: database
27
+ method: record_acceptance
28
+ input_mapping:
29
+ load_id: $input.load_id
30
+ negotiation_id: get_context.negotiation.id
31
+ carrier_id: get_context.carrier.id
32
+ dependencies: [get_context]
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import asyncio
38
+ import time
39
+ from typing import TYPE_CHECKING, Any
40
+
41
+ from hexdag.core.context import get_port
42
+ from hexdag.core.logging import get_logger
43
+
44
+ from .base_node_factory import BaseNodeFactory
45
+
46
+ if TYPE_CHECKING:
47
+ from pydantic import BaseModel
48
+
49
+ from hexdag.core.domain.dag import NodeSpec
50
+
51
+ logger = get_logger(__name__)
52
+
53
+
54
+ class PortCallNode(BaseNodeFactory):
55
+ """Execute a method on a configured port/adapter.
56
+
57
+ This node type eliminates the need for Python wrapper functions
58
+ that simply extract fields from input and call adapter methods.
59
+ The port, method, and parameter mapping are defined declaratively
60
+ in the YAML configuration.
61
+
62
+ The node:
63
+ 1. Resolves the port from the execution context
64
+ 2. Uses input_mapping to prepare method arguments (handled by orchestrator)
65
+ 3. Calls the method (supports both sync and async)
66
+ 4. Returns the result with metadata
67
+
68
+ Parameters (in YAML spec)
69
+ -------------------------
70
+ port : str
71
+ Name of the port to use (e.g., "database", "llm", "cache")
72
+ method : str
73
+ Method name to call on the port (e.g., "aexecute_query", "aget")
74
+ input_mapping : dict[str, str], optional
75
+ Mapping of method parameter names to data sources.
76
+ Supports:
77
+ - ``$input.field`` - Extract from initial pipeline input
78
+ - ``dependency_name.field`` - Extract from a dependency's output
79
+ fallback : Any, optional
80
+ Value to return if the port is not available
81
+ has_fallback : bool, optional
82
+ Set to True to enable fallback behavior (allows None as fallback value)
83
+
84
+ Examples
85
+ --------
86
+ >>> factory = PortCallNode()
87
+ >>> node = factory(
88
+ ... name="call_db",
89
+ ... port="database",
90
+ ... method="aexecute_query",
91
+ ... )
92
+ >>> node.name
93
+ 'call_db'
94
+
95
+ With input mapping::
96
+
97
+ >>> node = factory(
98
+ ... name="record_data",
99
+ ... port="database",
100
+ ... method="record_acceptance",
101
+ ... input_mapping={
102
+ ... "load_id": "$input.load_id",
103
+ ... "carrier_id": "context.carrier.id",
104
+ ... },
105
+ ... deps=["context"],
106
+ ... )
107
+ >>> "context" in node.deps
108
+ True
109
+ """
110
+
111
+ def __call__(
112
+ self,
113
+ name: str,
114
+ port: str,
115
+ method: str,
116
+ input_mapping: dict[str, str] | None = None,
117
+ fallback: Any = None,
118
+ has_fallback: bool = False,
119
+ output_schema: dict[str, Any] | type[BaseModel] | None = None,
120
+ deps: list[str] | None = None,
121
+ **kwargs: Any,
122
+ ) -> NodeSpec:
123
+ """Create a NodeSpec for a port method invocation node.
124
+
125
+ Parameters
126
+ ----------
127
+ name : str
128
+ Node name (must be unique within the pipeline)
129
+ port : str
130
+ Name of the port to call (e.g., "database", "llm", "tool_router")
131
+ method : str
132
+ Method name to invoke on the port
133
+ input_mapping : dict[str, str] | None, optional
134
+ Mapping of method parameter names to data sources.
135
+ Supports ``$input.field`` and ``dependency_name.field`` syntax.
136
+ fallback : Any, optional
137
+ Value to return if the port is not available
138
+ has_fallback : bool, optional
139
+ Set to True to enable fallback behavior (allows None as fallback)
140
+ output_schema : dict[str, Any] | type[BaseModel] | None, optional
141
+ Optional schema for validating/structuring the output
142
+ deps : list[str] | None, optional
143
+ List of dependency node names for execution ordering
144
+ **kwargs : Any
145
+ Additional parameters stored in NodeSpec.params
146
+
147
+ Returns
148
+ -------
149
+ NodeSpec
150
+ Complete node specification ready for execution
151
+
152
+ Examples
153
+ --------
154
+ >>> factory = PortCallNode()
155
+ >>> node = factory(
156
+ ... name="save_to_db",
157
+ ... port="database",
158
+ ... method="aexecute_query",
159
+ ... )
160
+ >>> node.name
161
+ 'save_to_db'
162
+ """
163
+ # Capture configuration in closure
164
+ port_name = port
165
+ method_name = method
166
+ _fallback = fallback
167
+ _has_fallback = has_fallback
168
+
169
+ async def port_call_fn(input_data: dict[str, Any]) -> dict[str, Any]:
170
+ """Execute port method call."""
171
+ node_logger = logger.bind(
172
+ node=name,
173
+ node_type="port_call_node",
174
+ port=port_name,
175
+ method=method_name,
176
+ )
177
+
178
+ start_time = time.perf_counter()
179
+
180
+ # Get the port from context
181
+ port_adapter = get_port(port_name)
182
+
183
+ if port_adapter is None:
184
+ if _has_fallback:
185
+ node_logger.warning(f"Port '{port_name}' not available, using fallback")
186
+ return {
187
+ "result": _fallback,
188
+ "port": port_name,
189
+ "method": method_name,
190
+ "error": f"Port '{port_name}' not available",
191
+ }
192
+ raise RuntimeError(
193
+ f"Port '{port_name}' not available in execution context. "
194
+ f"Ensure the port is configured in the orchestrator."
195
+ )
196
+
197
+ # Verify method exists
198
+ if not hasattr(port_adapter, method_name):
199
+ available = [m for m in dir(port_adapter) if not m.startswith("_")]
200
+ raise AttributeError(
201
+ f"Port '{port_name}' has no method '{method_name}'. "
202
+ f"Available methods: {', '.join(available[:10])}"
203
+ )
204
+
205
+ method_fn = getattr(port_adapter, method_name)
206
+
207
+ # Prepare method arguments from input_data
208
+ # input_data is already processed by ExecutionCoordinator._apply_input_mapping
209
+ # if input_mapping was specified in the node params
210
+ method_kwargs = dict(input_data) if isinstance(input_data, dict) else {}
211
+
212
+ node_logger.info(
213
+ "Calling port method",
214
+ args=list(method_kwargs.keys()),
215
+ )
216
+
217
+ try:
218
+ # Call method (handle both sync and async)
219
+ if asyncio.iscoroutinefunction(method_fn):
220
+ result = await method_fn(**method_kwargs)
221
+ else:
222
+ result = method_fn(**method_kwargs)
223
+
224
+ duration_ms = (time.perf_counter() - start_time) * 1000
225
+ node_logger.debug(
226
+ "Port method completed",
227
+ result_type=type(result).__name__,
228
+ duration_ms=f"{duration_ms:.2f}",
229
+ )
230
+
231
+ return {
232
+ "result": result,
233
+ "port": port_name,
234
+ "method": method_name,
235
+ "error": None,
236
+ }
237
+
238
+ except Exception as e:
239
+ node_logger.error(
240
+ "Port method failed",
241
+ error=str(e),
242
+ error_type=type(e).__name__,
243
+ )
244
+
245
+ if _has_fallback:
246
+ return {
247
+ "result": _fallback,
248
+ "port": port_name,
249
+ "method": method_name,
250
+ "error": str(e),
251
+ }
252
+ raise
253
+
254
+ # Preserve function metadata for debugging
255
+ port_call_fn.__name__ = f"port_call_{name}"
256
+ port_call_fn.__doc__ = f"Port call: {port_name}.{method_name}"
257
+
258
+ # Build input schema from input_mapping if provided
259
+ input_schema = dict.fromkeys(input_mapping, Any) if input_mapping else None
260
+
261
+ return self.create_node_with_mapping(
262
+ name=name,
263
+ wrapped_fn=port_call_fn,
264
+ input_schema=input_schema,
265
+ output_schema=output_schema,
266
+ deps=deps,
267
+ input_mapping=input_mapping, # Pass to params for ExecutionCoordinator
268
+ **kwargs,
269
+ )
@@ -0,0 +1,195 @@
1
+ """ToolCallNode - Execute a single tool call as a FunctionNode.
2
+
3
+ This node wraps a tool function and executes it as a node.
4
+ Used by ToolMacro to create parallel tool execution nodes.
5
+ """
6
+
7
+ import contextlib
8
+ import inspect
9
+ import time
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from pydantic import BaseModel
13
+
14
+ from hexdag.core.context import get_port
15
+ from hexdag.core.domain.dag import NodeSpec
16
+ from hexdag.core.logging import get_logger
17
+ from hexdag.core.orchestration.events import ToolCalled, ToolCompleted
18
+ from hexdag.core.resolver import resolve_function
19
+
20
+ from .base_node_factory import BaseNodeFactory
21
+
22
+ if TYPE_CHECKING:
23
+ from collections.abc import Callable
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ class ToolCallInput(BaseModel):
29
+ """Input for a tool call."""
30
+
31
+ tool_name: str
32
+ arguments: dict[str, Any] = {}
33
+ tool_call_id: str | None = None
34
+
35
+
36
+ class ToolCallOutput(BaseModel):
37
+ """Output from a tool call."""
38
+
39
+ result: Any
40
+ tool_call_id: str | None = None
41
+ tool_name: str
42
+ error: str | None = None
43
+
44
+
45
+ class ToolCallNode(BaseNodeFactory):
46
+ """Execute a single tool call as a FunctionNode.
47
+
48
+ This node is a simple wrapper that:
49
+ 1. Takes a tool name and arguments
50
+ 2. Resolves the tool function using the module path resolver
51
+ 3. Executes it and returns the result with metadata
52
+ 4. Emits ToolCalled and ToolCompleted events
53
+ Used by ToolMacro to create parallel tool execution nodes.
54
+
55
+ Examples
56
+ --------
57
+ Direct usage::
58
+
59
+ tool_node = ToolCallNode()(
60
+ name="search_tool",
61
+ tool_name="search",
62
+ arguments={"query": "python async"}
63
+ )
64
+
65
+ result = await orchestrator.run(
66
+ graph,
67
+ {"tool_call_id": "1"}
68
+ )
69
+ # Returns: {"result": [...], "tool_call_id": "1", "tool_name": "search"}
70
+
71
+ Tool requiring ports::
72
+
73
+ # Tool definition
74
+ @tool(name="db_query", required_ports=["database"])
75
+ async def query_db(sql: str, database_port=None):
76
+ return await database_port.aexecute_query(sql)
77
+
78
+ # ToolCallNode automatically injects database port
79
+ db_tool = ToolCallNode()(
80
+ name="db_query_node",
81
+ tool_name="db_query",
82
+ arguments={"sql": "SELECT * FROM users"}
83
+ )
84
+
85
+ # When executed, database_port is injected from context
86
+
87
+ In a macro (automatic parallel execution)::
88
+
89
+ # ToolMacro creates multiple ToolCallNodes
90
+ tool1 = ToolCallNode()(name="tool_1", tool_name="search", ...)
91
+ tool2 = ToolCallNode()(name="tool_2", tool_name="calc", ...)
92
+ # Orchestrator executes them in parallel automatically
93
+ # Ports injected automatically for tools that need them
94
+ """
95
+
96
+ def __init__(self, **kwargs: Any) -> None:
97
+ """Initialize ToolCallNode."""
98
+ super().__init__()
99
+
100
+ def __call__(
101
+ self,
102
+ name: str,
103
+ tool_name: str,
104
+ arguments: dict[str, Any] | None = None,
105
+ tool_call_id: str | None = None,
106
+ deps: list[str] | None = None,
107
+ **kwargs: Any,
108
+ ) -> NodeSpec:
109
+ """Create a tool call execution node.
110
+
111
+ Args
112
+ ----
113
+ name: Node name (should be unique)
114
+ tool_name: Full module path to the tool function (e.g., 'mymodule.my_tool')
115
+ arguments: Arguments to pass to the tool (default: {})
116
+ tool_call_id: Optional ID for tracking (from LLM tool calls)
117
+ deps: Dependencies (typically the LLM node that requested the tool)
118
+ **kwargs: Additional parameters
119
+
120
+ Returns
121
+ -------
122
+ NodeSpec
123
+ Configured node specification for tool execution
124
+ """
125
+ arguments = arguments or {}
126
+
127
+ async def execute_tool(input_data: dict[str, Any]) -> dict[str, Any]:
128
+ """Execute the tool call with event emission."""
129
+
130
+ # Get observer for event emission (optional)
131
+ observer_manager = None
132
+ with contextlib.suppress(Exception):
133
+ observer_manager = get_port("observer_manager")
134
+
135
+ # Emit ToolCalled event
136
+ if observer_manager:
137
+ await observer_manager.notify(
138
+ ToolCalled(node_name=name, tool_name=tool_name, params=arguments)
139
+ )
140
+
141
+ logger.debug(f"🔧 Executing tool '{tool_name}' with args: {arguments}")
142
+
143
+ start_time = time.time()
144
+ try:
145
+ # Resolve tool function using module path
146
+ tool_fn: Callable[..., Any] = resolve_function(tool_name)
147
+
148
+ # Prepare tool arguments
149
+ tool_kwargs = dict(arguments)
150
+
151
+ # Execute tool (handle both sync and async)
152
+ if inspect.iscoroutinefunction(tool_fn):
153
+ result = await tool_fn(**tool_kwargs)
154
+ else:
155
+ result = tool_fn(**tool_kwargs)
156
+
157
+ duration_ms = (time.time() - start_time) * 1000
158
+
159
+ # Emit ToolCompleted event
160
+ if observer_manager:
161
+ await observer_manager.notify(
162
+ ToolCompleted(
163
+ node_name=name,
164
+ tool_name=tool_name,
165
+ result=result,
166
+ duration_ms=duration_ms,
167
+ )
168
+ )
169
+
170
+ logger.debug(f"✅ Tool '{tool_name}' completed in {duration_ms:.2f}ms")
171
+
172
+ return {
173
+ "result": result,
174
+ "tool_call_id": tool_call_id,
175
+ "tool_name": tool_name,
176
+ "error": None,
177
+ }
178
+ except Exception as e:
179
+ duration_ms = (time.time() - start_time) * 1000
180
+ logger.warning(f"❌ Tool '{tool_name}' failed after {duration_ms:.2f}ms: {e}")
181
+ return {
182
+ "result": None,
183
+ "tool_call_id": tool_call_id,
184
+ "tool_name": tool_name,
185
+ "error": str(e),
186
+ }
187
+
188
+ return self.create_node_with_mapping(
189
+ name=name,
190
+ wrapped_fn=execute_tool,
191
+ input_schema={}, # No specific input schema (uses context)
192
+ output_schema=ToolCallOutput,
193
+ deps=deps,
194
+ **kwargs,
195
+ )