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,380 @@
1
+ """ToolMacro - Expand tool calls into parallel ToolCallNodes.
2
+
3
+ This macro enables dynamic parallel tool execution:
4
+ 1. Takes a list of tool_calls from LLM
5
+ 2. Creates a ToolCallNode for each tool
6
+ 3. Executes them in parallel (via DAG waves)
7
+ 4. Merges results back into conversation
8
+
9
+ Used by ReasoningAgent and Macro Agent for dynamic tool injection.
10
+ """
11
+
12
+ from typing import Any
13
+
14
+ from hexdag.builtin.nodes import FunctionNode, ToolCallNode
15
+ from hexdag.core.configurable import ConfigurableMacro, MacroConfig
16
+ from hexdag.core.domain.dag import DirectedGraph
17
+ from hexdag.core.logging import get_logger
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ class ToolMacroConfig(MacroConfig):
23
+ """Configuration for ToolMacro.
24
+
25
+ Attributes
26
+ ----------
27
+ tool_calls : list[dict]
28
+ List of tool calls from LLM in format:
29
+ [
30
+ {"id": "call_1", "name": "search", "arguments": {"query": "AI"}},
31
+ {"id": "call_2", "name": "calc", "arguments": {"expr": "2+2"}}
32
+ ]
33
+ agent_name : str | None
34
+ Name of agent requesting tools (for access control)
35
+ allowed_tools : list[str] | None
36
+ List of allowed tool names (if None, all tools allowed)
37
+ """
38
+
39
+ tool_calls: list[dict[str, Any]] = []
40
+ agent_name: str | None = None
41
+ allowed_tools: list[str] | None = None
42
+
43
+
44
+ class ToolMacro(ConfigurableMacro):
45
+ """Expand tool calls into parallel ToolCallNodes.
46
+
47
+ This macro creates a subgraph for dynamic parallel tool execution:
48
+
49
+ Graph Structure:
50
+ ----------------
51
+ ```
52
+ [dependencies] → tool_call_1 ┐
53
+ → tool_call_2 ├─→ merger → [output]
54
+ → tool_call_3 ┘
55
+ ```
56
+
57
+ All tool nodes execute in parallel (same wave), then results merge.
58
+
59
+ Examples
60
+ --------
61
+ Basic usage::
62
+
63
+ config = ToolMacroConfig(
64
+ tool_calls=[
65
+ {"id": "1", "name": "search", "arguments": {"query": "AI"}},
66
+ {"id": "2", "name": "calc", "arguments": {"expression": "2+2"}}
67
+ ]
68
+ )
69
+
70
+ macro = ToolMacro(config)
71
+ graph = macro.expand(
72
+ instance_name="agent_tools",
73
+ inputs={},
74
+ dependencies=["llm_node"]
75
+ )
76
+
77
+ # Graph contains:
78
+ # - agent_tools_tool_0_search (depends on llm_node)
79
+ # - agent_tools_tool_1_calc (depends on llm_node)
80
+ # - agent_tools_merger (depends on both tools)
81
+
82
+ With access control::
83
+
84
+ config = ToolMacroConfig(
85
+ tool_calls=[...],
86
+ agent_name="research_agent",
87
+ allowed_tools=["search", "summarize"] # calc not allowed
88
+ )
89
+
90
+ macro = ToolMacro(config)
91
+ graph = macro.expand(...)
92
+ # Only creates nodes for allowed tools
93
+ """
94
+
95
+ Config = ToolMacroConfig
96
+
97
+ def expand(
98
+ self,
99
+ instance_name: str,
100
+ inputs: dict[str, Any],
101
+ dependencies: list[str],
102
+ ) -> DirectedGraph:
103
+ """Expand tool calls into parallel execution graph.
104
+
105
+ Parameters
106
+ ----------
107
+ instance_name : str
108
+ Unique name for this macro instance (used as prefix for nodes)
109
+ inputs : dict[str, Any]
110
+ Input values (typically empty, config has the data)
111
+ dependencies : list[str]
112
+ Nodes that tool calls depend on (typically the LLM node)
113
+
114
+ Returns
115
+ -------
116
+ DirectedGraph
117
+ Graph with parallel ToolCallNodes and merger
118
+
119
+ Examples
120
+ --------
121
+ Called during dynamic execution::
122
+
123
+ # Agent's LLM returns tool_calls
124
+ tool_calls = [
125
+ {"id": "1", "name": "search", "arguments": {"query": "AI"}},
126
+ {"id": "2", "name": "calc", "arguments": {"expr": "2+2"}}
127
+ ]
128
+
129
+ # Create macro config
130
+ config = ToolMacroConfig(tool_calls=tool_calls)
131
+ macro = ToolMacro(config)
132
+
133
+ # Expand into graph
134
+ subgraph = macro.expand(
135
+ instance_name="agent_step_1_tools",
136
+ inputs={},
137
+ dependencies=["agent_step_1_llm"]
138
+ )
139
+
140
+ # Orchestrator merges subgraph into main graph
141
+ # and executes tools in parallel
142
+ """
143
+ graph = DirectedGraph()
144
+ config: ToolMacroConfig = self.config # type: ignore[assignment]
145
+
146
+ # Get tool calls from config
147
+ tool_calls = config.tool_calls
148
+
149
+ if not tool_calls:
150
+ logger.debug(f"No tool calls for {instance_name}, returning empty graph")
151
+ return self._create_passthrough(instance_name, dependencies)
152
+
153
+ # Filter by allowed tools (access control)
154
+ allowed_tools = self._get_allowed_tools(config)
155
+ filtered_calls = self._filter_tool_calls(tool_calls, allowed_tools, config.agent_name)
156
+
157
+ if not filtered_calls:
158
+ logger.warning(f"All {len(tool_calls)} tool calls filtered out for {config.agent_name}")
159
+ return self._create_passthrough(instance_name, dependencies)
160
+
161
+ # Create ToolCallNode for each tool call
162
+ tool_call_factory = ToolCallNode()
163
+ tool_nodes = []
164
+
165
+ for i, tc in enumerate(filtered_calls):
166
+ tool_name = tc["name"]
167
+ tool_call_id = tc.get("id", f"{instance_name}_{i}")
168
+ arguments = tc.get("arguments", {})
169
+
170
+ # Create node
171
+ node = tool_call_factory(
172
+ name=f"{instance_name}_tool_{i}_{tool_name}",
173
+ tool_name=tool_name,
174
+ arguments=arguments,
175
+ tool_call_id=tool_call_id,
176
+ deps=dependencies, # All tools depend on LLM output
177
+ )
178
+
179
+ graph += node
180
+ tool_nodes.append(node.name)
181
+
182
+ logger.debug(
183
+ f"Created tool node: {node.name} for tool '{tool_name}' with args {arguments}"
184
+ )
185
+
186
+ # Create merger node that consolidates all tool results
187
+ merger = self._create_merger_node(instance_name, tool_nodes, filtered_calls)
188
+ graph += merger
189
+
190
+ logger.info(
191
+ f"ToolMacro '{instance_name}' expanded to {len(tool_nodes)} "
192
+ f"parallel tool nodes + merger"
193
+ )
194
+
195
+ return graph
196
+
197
+ def _get_allowed_tools(self, config: ToolMacroConfig) -> set[str] | None:
198
+ """Get set of allowed tools (None = all allowed).
199
+
200
+ Parameters
201
+ ----------
202
+ config : ToolMacroConfig
203
+ Macro configuration
204
+
205
+ Returns
206
+ -------
207
+ set[str] | None
208
+ Set of allowed tool names, or None if all allowed
209
+ """
210
+ if config.allowed_tools is None:
211
+ return None # All tools allowed
212
+
213
+ return set(config.allowed_tools)
214
+
215
+ def _filter_tool_calls(
216
+ self,
217
+ tool_calls: list[dict[str, Any]],
218
+ allowed_tools: set[str] | None,
219
+ agent_name: str | None,
220
+ ) -> list[dict[str, Any]]:
221
+ """Filter tool calls by allowed tools (access control).
222
+
223
+ Parameters
224
+ ----------
225
+ tool_calls : list[dict]
226
+ Raw tool calls from LLM
227
+ allowed_tools : set[str] | None
228
+ Set of allowed tool names (None = all allowed)
229
+ agent_name : str | None
230
+ Name of agent (for logging)
231
+
232
+ Returns
233
+ -------
234
+ list[dict]
235
+ Filtered tool calls
236
+ """
237
+ if allowed_tools is None:
238
+ return tool_calls # No filtering
239
+
240
+ filtered = []
241
+ for tc in tool_calls:
242
+ tool_name = tc["name"]
243
+ if tool_name in allowed_tools:
244
+ filtered.append(tc)
245
+ else:
246
+ logger.warning(
247
+ f"Tool '{tool_name}' not allowed for agent '{agent_name}' "
248
+ f"(allowed: {allowed_tools})"
249
+ )
250
+
251
+ return filtered
252
+
253
+ def _create_merger_node(
254
+ self,
255
+ instance_name: str,
256
+ tool_nodes: list[str],
257
+ tool_calls: list[dict[str, Any]],
258
+ ) -> Any: # Returns NodeSpec
259
+ """Create merger node that consolidates tool results.
260
+
261
+ The merger node:
262
+ 1. Waits for all tool nodes to complete
263
+ 2. Collects results
264
+ 3. Formats them for the agent (tool messages)
265
+
266
+ Parameters
267
+ ----------
268
+ instance_name : str
269
+ Macro instance name
270
+ tool_nodes : list[str]
271
+ Names of tool nodes to wait for
272
+ tool_calls : list[dict]
273
+ Original tool calls (for matching)
274
+
275
+ Returns
276
+ -------
277
+ NodeSpec
278
+ Merger node specification
279
+ """
280
+ fn_factory = FunctionNode()
281
+
282
+ async def merge_tool_results(input_data: dict[str, Any]) -> dict[str, Any]:
283
+ """Merge results from all tool nodes.
284
+
285
+ This function receives the outputs of all tool nodes and
286
+ consolidates them into a format suitable for continuing
287
+ the conversation with the LLM.
288
+
289
+ Parameters
290
+ ----------
291
+ input_data : dict
292
+ Contains results from all dependency nodes
293
+
294
+ Returns
295
+ -------
296
+ dict
297
+ Merged results with:
298
+ - results: List of tool results
299
+ - has_tools: True
300
+ - tool_messages: Formatted for LLM conversation
301
+ """
302
+ results = []
303
+
304
+ # Collect results from each tool node
305
+ for node_name in tool_nodes:
306
+ # Get result from this tool node
307
+ tool_result = input_data.get(node_name)
308
+
309
+ if tool_result:
310
+ # ToolCallNode returns ToolCallOutput (Pydantic model)
311
+ if hasattr(tool_result, "model_dump"):
312
+ tool_result = tool_result.model_dump()
313
+
314
+ results.append(tool_result)
315
+
316
+ # Format as tool messages for LLM
317
+ tool_messages = [
318
+ {
319
+ "role": "tool",
320
+ "tool_call_id": result.get("tool_call_id"),
321
+ "name": result.get("tool_name"),
322
+ "content": str(result.get("result") or result.get("error", "Unknown error")),
323
+ }
324
+ for result in results
325
+ ]
326
+
327
+ logger.debug(f"Merged {len(results)} tool results into {len(tool_messages)} messages")
328
+
329
+ return {
330
+ "results": results,
331
+ "has_tools": True,
332
+ "tool_messages": tool_messages,
333
+ "tool_count": len(results),
334
+ }
335
+
336
+ return fn_factory(
337
+ name=f"{instance_name}_merger",
338
+ fn=merge_tool_results,
339
+ deps=tool_nodes, # Wait for all tools
340
+ )
341
+
342
+ def _create_passthrough(self, instance_name: str, dependencies: list[str]) -> DirectedGraph:
343
+ """Create passthrough node when no tools to execute.
344
+
345
+ Used when:
346
+ - No tool calls provided
347
+ - All tool calls filtered out by access control
348
+
349
+ Parameters
350
+ ----------
351
+ instance_name : str
352
+ Macro instance name
353
+ dependencies : list[str]
354
+ Nodes to depend on
355
+
356
+ Returns
357
+ -------
358
+ DirectedGraph
359
+ Graph with single passthrough node
360
+ """
361
+ graph = DirectedGraph()
362
+ fn_factory = FunctionNode()
363
+
364
+ async def passthrough(input_data: dict[str, Any]) -> dict[str, Any]:
365
+ """Passthrough when no tools."""
366
+ return {
367
+ "results": [],
368
+ "has_tools": False,
369
+ "tool_messages": [],
370
+ "tool_count": 0,
371
+ }
372
+
373
+ passthrough_node = fn_factory(
374
+ name=f"{instance_name}_no_tools",
375
+ fn=passthrough,
376
+ deps=dependencies,
377
+ )
378
+
379
+ graph += passthrough_node
380
+ return graph
@@ -0,0 +1,38 @@
1
+ """Node factories for the hexdag framework.
2
+
3
+ All BaseNodeFactory subclasses in this package are auto-discovered
4
+ and registered for YAML pipeline validation. Adding a new node only
5
+ requires creating the node file - no manual registration needed.
6
+
7
+ See hexdag.builtin.nodes._discovery for the auto-discovery mechanism.
8
+ """
9
+
10
+ from .agent_node import ReActAgentNode
11
+ from .composite_node import CompositeNode
12
+ from .data_node import DataNode
13
+ from .expression_node import ExpressionNode
14
+ from .function_node import FunctionNode
15
+ from .llm_node import LLMNode
16
+ from .loop_node import ConditionalNode, LoopNode
17
+ from .port_call_node import PortCallNode
18
+ from .tool_call_node import ToolCallNode
19
+
20
+ __all__ = [
21
+ "CompositeNode",
22
+ "ConditionalNode",
23
+ "DataNode",
24
+ "ExpressionNode",
25
+ "FunctionNode",
26
+ "LLMNode",
27
+ "LoopNode",
28
+ "PortCallNode",
29
+ "ReActAgentNode",
30
+ "ToolCallNode",
31
+ ]
32
+
33
+ # Bootstrap: Register auto-discovered node aliases with core resolver
34
+ # This maintains hexagonal architecture - builtin calls into core, not vice versa
35
+ from hexdag.builtin.nodes._discovery import discover_node_factories
36
+ from hexdag.core.resolver import register_builtin_aliases
37
+
38
+ register_builtin_aliases(discover_node_factories())
@@ -0,0 +1,123 @@
1
+ """Auto-discovery of node factories in builtin.nodes package.
2
+
3
+ This module provides automatic discovery of all BaseNodeFactory subclasses,
4
+ eliminating the need to manually register node types in multiple places.
5
+
6
+ Usage
7
+ -----
8
+ Adding a new node only requires creating the node file:
9
+
10
+ # hexdag/builtin/nodes/my_node.py
11
+ class MyNode(BaseNodeFactory):
12
+ def __call__(self, name: str, **kwargs) -> NodeSpec:
13
+ ...
14
+
15
+ The node is automatically available in YAML as:
16
+ - my_node
17
+ - core:my_node
18
+ - core:my
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import importlib
24
+ import pkgutil
25
+ import re
26
+ from functools import lru_cache
27
+
28
+
29
+ def _to_snake_case(name: str) -> str:
30
+ """Convert CamelCase to snake_case.
31
+
32
+ Examples
33
+ --------
34
+ >>> _to_snake_case("LLMNode")
35
+ 'llm_node'
36
+ >>> _to_snake_case("ReActAgentNode")
37
+ 're_act_agent_node'
38
+ >>> _to_snake_case("DataNode")
39
+ 'data_node'
40
+ """
41
+ # Handle acronyms like LLM, ReAct, then standard CamelCase
42
+ name = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
43
+ name = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", name)
44
+ return name.lower()
45
+
46
+
47
+ @lru_cache(maxsize=1)
48
+ def discover_node_factories() -> dict[str, str]:
49
+ """Discover all BaseNodeFactory subclasses and generate aliases.
50
+
51
+ Returns mapping: {alias: full_module_path}
52
+
53
+ Examples
54
+ --------
55
+ >>> aliases = discover_node_factories()
56
+ >>> "llm_node" in aliases
57
+ True
58
+ >>> "core:llm_node" in aliases
59
+ True
60
+ >>> "core:llm" in aliases
61
+ True
62
+ """
63
+ # Import here to avoid circular imports
64
+ from hexdag.builtin.nodes.base_node_factory import BaseNodeFactory
65
+
66
+ aliases: dict[str, str] = {}
67
+ package = importlib.import_module("hexdag.builtin.nodes")
68
+
69
+ for module_info in pkgutil.iter_modules(package.__path__):
70
+ if module_info.name.startswith("_"):
71
+ continue
72
+
73
+ try:
74
+ module = importlib.import_module(f"hexdag.builtin.nodes.{module_info.name}")
75
+ except ImportError:
76
+ continue
77
+
78
+ for attr_name in dir(module):
79
+ if attr_name.startswith("_"):
80
+ continue
81
+ attr = getattr(module, attr_name)
82
+ if (
83
+ isinstance(attr, type)
84
+ and issubclass(attr, BaseNodeFactory)
85
+ and attr is not BaseNodeFactory
86
+ ):
87
+ full_path = f"hexdag.builtin.nodes.{attr_name}"
88
+ snake_name = _to_snake_case(attr_name)
89
+
90
+ # Generate all alias forms
91
+ aliases[snake_name] = full_path # llm_node
92
+ aliases[f"core:{snake_name}"] = full_path # core:llm_node
93
+
94
+ # Also add without _node suffix for convenience
95
+ if snake_name.endswith("_node"):
96
+ base = snake_name[:-5]
97
+ aliases[f"core:{base}"] = full_path # core:llm
98
+
99
+ # Add backwards compatibility aliases for legacy names
100
+ # static_node -> DataNode
101
+ if "data_node" in aliases:
102
+ aliases["static_node"] = aliases["data_node"]
103
+ aliases["core:static_node"] = aliases["data_node"]
104
+ aliases["core:static"] = aliases["data_node"]
105
+
106
+ # agent_node -> ReActAgentNode (legacy alias)
107
+ if "re_act_agent_node" in aliases:
108
+ aliases["agent_node"] = aliases["re_act_agent_node"]
109
+ aliases["core:agent_node"] = aliases["re_act_agent_node"]
110
+ aliases["core:agent"] = aliases["re_act_agent_node"]
111
+
112
+ return aliases
113
+
114
+
115
+ def get_known_node_types() -> frozenset[str]:
116
+ """Get all valid node type names for YAML validation.
117
+
118
+ Returns
119
+ -------
120
+ frozenset[str]
121
+ Set of all valid node type aliases
122
+ """
123
+ return frozenset(discover_node_factories().keys())