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,481 @@
1
+ """BodyExecutor - Shared body execution logic for ControlFlowNode.
2
+
3
+ This module provides unified body execution for control flow nodes,
4
+ supporting four execution modes:
5
+ 1. Module path string - Resolve and call function by path
6
+ 2. !py inline Python - Compile and execute inline function
7
+ 3. Inline nodes - Build and execute sub-DAG
8
+ 4. Pipeline reference - Load and execute external pipeline
9
+
10
+ Examples
11
+ --------
12
+ Basic usage::
13
+
14
+ executor = BodyExecutor()
15
+
16
+ # Execute function by module path
17
+ result = await executor.execute(
18
+ body="myapp.process_item",
19
+ input_data={"item": data},
20
+ context=ctx,
21
+ ports={"database": db_adapter},
22
+ )
23
+
24
+ # Execute inline nodes
25
+ result = await executor.execute(
26
+ body=[
27
+ {"kind": "expression_node", "metadata": {"name": "step1"}, "spec": {...}},
28
+ {"kind": "llm_node", "metadata": {"name": "step2"}, "spec": {...}},
29
+ ],
30
+ input_data={"item": data},
31
+ context=ctx,
32
+ ports={"llm": llm_adapter},
33
+ )
34
+ """
35
+
36
+ import asyncio
37
+ from collections.abc import Callable
38
+ from pathlib import Path
39
+ from typing import Any
40
+
41
+ from hexdag.core.logging import get_logger
42
+ from hexdag.core.orchestration.models import NodeExecutionContext
43
+ from hexdag.core.resolver import resolve_function
44
+
45
+ logger = get_logger(__name__)
46
+
47
+
48
+ class BodyExecutorError(Exception):
49
+ """Error during body execution."""
50
+
51
+ pass
52
+
53
+
54
+ class BodyExecutor:
55
+ """Execute node body in function, inline-nodes, !py, or pipeline mode.
56
+
57
+ This class provides a unified interface for executing different body types
58
+ within control flow nodes (while, for-each, times, if-else, switch).
59
+
60
+ The BodyExecutor can be configured with orchestrator settings to ensure
61
+ sub-DAGs and pipelines execute with consistent configuration.
62
+
63
+ Attributes
64
+ ----------
65
+ base_path : Path
66
+ Base directory for resolving pipeline references
67
+ max_concurrent_nodes : int
68
+ Maximum concurrent nodes for sub-orchestrators
69
+ strict_validation : bool
70
+ Whether to use strict validation in sub-orchestrators
71
+ default_node_timeout : float | None
72
+ Default timeout for nodes in sub-orchestrators
73
+
74
+ Examples
75
+ --------
76
+ Execute a function by module path::
77
+
78
+ executor = BodyExecutor()
79
+ result = await executor.execute(
80
+ body="myapp.process",
81
+ input_data={"x": 1},
82
+ context=ctx,
83
+ ports={},
84
+ )
85
+
86
+ Execute inline Python function (from !py tag)::
87
+
88
+ async def my_func(item, index, state, **ports):
89
+ return item * 2
90
+
91
+ result = await executor.execute(
92
+ body=my_func, # Compiled from !py tag
93
+ input_data={"item": 5, "index": 0, "state": {}},
94
+ context=ctx,
95
+ ports={},
96
+ )
97
+
98
+ With orchestrator configuration::
99
+
100
+ executor = BodyExecutor(
101
+ base_path=Path("./pipelines"),
102
+ max_concurrent_nodes=5,
103
+ strict_validation=True,
104
+ )
105
+ """
106
+
107
+ def __init__(
108
+ self,
109
+ base_path: Path | None = None,
110
+ max_concurrent_nodes: int = 10,
111
+ strict_validation: bool = False,
112
+ default_node_timeout: float | None = None,
113
+ ) -> None:
114
+ """Initialize BodyExecutor.
115
+
116
+ Parameters
117
+ ----------
118
+ base_path : Path | None
119
+ Base directory for resolving pipeline references.
120
+ Defaults to current working directory.
121
+ max_concurrent_nodes : int
122
+ Maximum concurrent nodes for sub-orchestrators (default: 10)
123
+ strict_validation : bool
124
+ Whether to use strict validation in sub-orchestrators (default: False)
125
+ default_node_timeout : float | None
126
+ Default timeout for nodes in sub-orchestrators (default: None)
127
+ """
128
+ self.base_path = base_path or Path.cwd()
129
+ self.max_concurrent_nodes = max_concurrent_nodes
130
+ self.strict_validation = strict_validation
131
+ self.default_node_timeout = default_node_timeout
132
+
133
+ async def execute(
134
+ self,
135
+ body: str | list[dict[str, Any]] | Callable[..., Any] | None,
136
+ body_pipeline: str | None,
137
+ input_data: dict[str, Any],
138
+ context: NodeExecutionContext,
139
+ ports: dict[str, Any],
140
+ iteration_context: dict[str, Any] | None = None,
141
+ ) -> Any:
142
+ """Execute body and return result.
143
+
144
+ Dispatches to the appropriate execution method based on body type:
145
+ - str: Module path string → _execute_function
146
+ - Callable: !py compiled function → _execute_py_function
147
+ - list[dict]: Inline nodes → _execute_inline_nodes
148
+ - body_pipeline: Pipeline reference → _execute_pipeline
149
+
150
+ Parameters
151
+ ----------
152
+ body : str | list[dict] | Callable | None
153
+ The body specification. Can be:
154
+ - Module path string (e.g., "myapp.process")
155
+ - Callable (compiled from !py tag)
156
+ - List of node configs (inline nodes/sub-DAG)
157
+ body_pipeline : str | None
158
+ Path to external pipeline YAML file
159
+ input_data : dict[str, Any]
160
+ Input data for the body execution
161
+ context : NodeExecutionContext
162
+ Execution context with metadata
163
+ ports : dict[str, Any]
164
+ Injected ports (llm, database, memory, etc.)
165
+ iteration_context : dict[str, Any] | None
166
+ Additional context for iterations ($item, $index, $total, state)
167
+
168
+ Returns
169
+ -------
170
+ Any
171
+ Result from body execution
172
+
173
+ Raises
174
+ ------
175
+ BodyExecutorError
176
+ If body execution fails or no body is specified
177
+ """
178
+ # Merge iteration context with input data
179
+ exec_context = dict(input_data)
180
+ if iteration_context:
181
+ exec_context.update(iteration_context)
182
+
183
+ if body_pipeline:
184
+ return await self._execute_pipeline(body_pipeline, exec_context, context, ports)
185
+ if isinstance(body, list):
186
+ return await self._execute_inline_nodes(body, exec_context, context, ports)
187
+ if isinstance(body, str):
188
+ return await self._execute_function(body, exec_context, context, ports)
189
+ if callable(body):
190
+ # Callable passed directly (either !py compiled function or regular callable)
191
+ # We call with (input_data, **ports) to match function_node convention
192
+ return await self._execute_callable(body, exec_context, context, ports)
193
+ raise BodyExecutorError(
194
+ "No body specified. Provide 'body' (function path, callable, or inline nodes) "
195
+ "or 'body_pipeline' (pipeline reference)."
196
+ )
197
+
198
+ async def _execute_function(
199
+ self,
200
+ body: str,
201
+ input_data: dict[str, Any],
202
+ context: NodeExecutionContext,
203
+ ports: dict[str, Any],
204
+ ) -> Any:
205
+ """Execute body as module path function.
206
+
207
+ Parameters
208
+ ----------
209
+ body : str
210
+ Module path to function (e.g., "myapp.process")
211
+ input_data : dict[str, Any]
212
+ Input data to pass to function
213
+ context : NodeExecutionContext
214
+ Execution context
215
+ ports : dict[str, Any]
216
+ Injected ports
217
+
218
+ Returns
219
+ -------
220
+ Any
221
+ Function result
222
+ """
223
+ try:
224
+ func = resolve_function(body)
225
+ except Exception as e:
226
+ raise BodyExecutorError(f"Failed to resolve function '{body}': {e}") from e
227
+
228
+ logger.debug(
229
+ "Executing function body",
230
+ function=body,
231
+ node_id=context.node_id,
232
+ )
233
+
234
+ try:
235
+ # Call function with input_data and ports
236
+ # Support both sync and async functions
237
+ result = func(input_data, **ports)
238
+ if asyncio.iscoroutine(result):
239
+ result = await result
240
+ return result
241
+ except Exception as e:
242
+ raise BodyExecutorError(f"Function '{body}' execution failed: {e}") from e
243
+
244
+ async def _execute_py_function(
245
+ self,
246
+ body: Callable[..., Any],
247
+ input_data: dict[str, Any],
248
+ context: NodeExecutionContext,
249
+ ports: dict[str, Any],
250
+ ) -> Any:
251
+ """Execute !py compiled inline Python function.
252
+
253
+ The function is expected to have signature:
254
+ async def process(item, index, state, **ports) -> Any
255
+
256
+ But we support flexible signatures - pass what's available.
257
+
258
+ Parameters
259
+ ----------
260
+ body : Callable
261
+ Compiled Python function from !py tag
262
+ input_data : dict[str, Any]
263
+ Input data containing item, index, state, etc.
264
+ context : NodeExecutionContext
265
+ Execution context
266
+ ports : dict[str, Any]
267
+ Injected ports
268
+
269
+ Returns
270
+ -------
271
+ Any
272
+ Function result
273
+ """
274
+ logger.debug(
275
+ "Executing !py inline function",
276
+ function_name=getattr(body, "__name__", "<anonymous>"),
277
+ node_id=context.node_id,
278
+ )
279
+
280
+ try:
281
+ # Extract standard parameters from input_data
282
+ item = input_data.get("$item", input_data.get("item"))
283
+ index = input_data.get("$index", input_data.get("index", 0))
284
+ state = input_data.get("state", {})
285
+
286
+ # Call with positional args (item, index, state) and keyword args (ports)
287
+ result = body(item, index, state, **ports)
288
+ if asyncio.iscoroutine(result):
289
+ result = await result
290
+ return result
291
+ except Exception as e:
292
+ func_name = getattr(body, "__name__", "<anonymous>")
293
+ raise BodyExecutorError(f"!py function '{func_name}' execution failed: {e}") from e
294
+
295
+ async def _execute_callable(
296
+ self,
297
+ body: Callable[..., Any],
298
+ input_data: dict[str, Any],
299
+ context: NodeExecutionContext,
300
+ ports: dict[str, Any],
301
+ ) -> Any:
302
+ """Execute a callable body with function_node-style signature.
303
+
304
+ This method handles regular callable bodies passed directly to
305
+ ControlFlowNode. It uses the (input_data, **ports) convention
306
+ matching function_node behavior.
307
+
308
+ For !py compiled functions that need (item, index, state, **ports),
309
+ use _execute_py_function instead.
310
+
311
+ Parameters
312
+ ----------
313
+ body : Callable
314
+ A callable function
315
+ input_data : dict[str, Any]
316
+ Input data to pass as first argument
317
+ context : NodeExecutionContext
318
+ Execution context
319
+ ports : dict[str, Any]
320
+ Injected ports
321
+
322
+ Returns
323
+ -------
324
+ Any
325
+ Function result
326
+ """
327
+ logger.debug(
328
+ "Executing callable body",
329
+ function_name=getattr(body, "__name__", "<anonymous>"),
330
+ node_id=context.node_id,
331
+ )
332
+
333
+ try:
334
+ # Call with (input_data, **ports) - matching function_node convention
335
+ result = body(input_data, **ports)
336
+ if asyncio.iscoroutine(result):
337
+ result = await result
338
+ return result
339
+ except Exception as e:
340
+ func_name = getattr(body, "__name__", "<anonymous>")
341
+ raise BodyExecutorError(f"Callable '{func_name}' execution failed: {e}") from e
342
+
343
+ async def _execute_inline_nodes(
344
+ self,
345
+ body: list[dict[str, Any]],
346
+ input_data: dict[str, Any],
347
+ context: NodeExecutionContext,
348
+ ports: dict[str, Any],
349
+ ) -> Any:
350
+ """Execute inline nodes as sub-DAG.
351
+
352
+ Builds a temporary DirectedGraph from the inline node configs,
353
+ creates a sub-orchestrator, and executes it.
354
+
355
+ Parameters
356
+ ----------
357
+ body : list[dict[str, Any]]
358
+ List of node configuration dicts
359
+ input_data : dict[str, Any]
360
+ Input data for the sub-DAG
361
+ context : NodeExecutionContext
362
+ Execution context
363
+ ports : dict[str, Any]
364
+ Injected ports
365
+
366
+ Returns
367
+ -------
368
+ Any
369
+ Result from the last non-skipped node in the sub-DAG
370
+ """
371
+ # Import here to avoid circular dependency
372
+ from hexdag.core.domain.dag import DirectedGraph
373
+ from hexdag.core.orchestration.orchestrator import Orchestrator
374
+ from hexdag.core.pipeline_builder import YamlPipelineBuilder
375
+
376
+ logger.debug(
377
+ "Executing inline nodes",
378
+ node_count=len(body),
379
+ node_id=context.node_id,
380
+ )
381
+
382
+ # Build sub-graph from inline nodes
383
+ builder = YamlPipelineBuilder(base_path=self.base_path)
384
+ sub_graph = DirectedGraph()
385
+
386
+ for node_config in body:
387
+ # Find appropriate plugin to build node
388
+ for plugin in builder.entity_plugins:
389
+ if plugin.can_handle(node_config):
390
+ result = plugin.build(node_config, builder, sub_graph)
391
+ if result is not None:
392
+ sub_graph += result
393
+ break
394
+
395
+ if not sub_graph.nodes:
396
+ logger.warning("Inline body has no nodes", node_id=context.node_id)
397
+ return None
398
+
399
+ # Create sub-orchestrator with inherited configuration
400
+ sub_orchestrator = Orchestrator(
401
+ max_concurrent_nodes=self.max_concurrent_nodes,
402
+ strict_validation=self.strict_validation,
403
+ default_node_timeout=self.default_node_timeout,
404
+ ports=ports,
405
+ )
406
+
407
+ try:
408
+ run_result = await sub_orchestrator.run(sub_graph, input_data, validate=False)
409
+ except Exception as e:
410
+ raise BodyExecutorError(f"Inline nodes execution failed: {e}") from e
411
+
412
+ # Return the result from the last non-skipped node
413
+ # Find nodes in topological order and get last result
414
+ # Note: orchestrator.run() returns dict[str, Any]
415
+ for node_name in reversed(list(sub_graph.nodes.keys())):
416
+ if node_name in run_result:
417
+ node_result = run_result[node_name]
418
+ if isinstance(node_result, dict) and node_result.get("_skipped"):
419
+ continue
420
+ return node_result
421
+
422
+ return run_result
423
+
424
+ async def _execute_pipeline(
425
+ self,
426
+ body_pipeline: str,
427
+ input_data: dict[str, Any],
428
+ context: NodeExecutionContext,
429
+ ports: dict[str, Any],
430
+ ) -> Any:
431
+ """Execute external pipeline from YAML file.
432
+
433
+ Parameters
434
+ ----------
435
+ body_pipeline : str
436
+ Path to pipeline YAML file (relative to base_path)
437
+ input_data : dict[str, Any]
438
+ Input data for the pipeline
439
+ context : NodeExecutionContext
440
+ Execution context
441
+ ports : dict[str, Any]
442
+ Injected ports
443
+
444
+ Returns
445
+ -------
446
+ Any
447
+ Pipeline execution result
448
+ """
449
+ # Import here to avoid circular dependency
450
+ from hexdag.core.orchestration.orchestrator import Orchestrator
451
+ from hexdag.core.pipeline_builder import YamlPipelineBuilder
452
+
453
+ # Resolve pipeline path
454
+ pipeline_path = self.base_path / body_pipeline
455
+ if not pipeline_path.exists():
456
+ raise BodyExecutorError(f"Pipeline file not found: {pipeline_path}")
457
+
458
+ logger.debug(
459
+ "Executing pipeline body",
460
+ pipeline=body_pipeline,
461
+ node_id=context.node_id,
462
+ )
463
+
464
+ try:
465
+ # Build pipeline
466
+ builder = YamlPipelineBuilder(base_path=pipeline_path.parent)
467
+ graph, config = builder.build_from_yaml_file(str(pipeline_path))
468
+
469
+ # Create orchestrator with inherited configuration
470
+ orchestrator = Orchestrator(
471
+ max_concurrent_nodes=self.max_concurrent_nodes,
472
+ strict_validation=self.strict_validation,
473
+ default_node_timeout=self.default_node_timeout,
474
+ ports=ports,
475
+ )
476
+
477
+ # Execute pipeline
478
+ return await orchestrator.run(graph, input_data, validate=False)
479
+
480
+ except Exception as e:
481
+ raise BodyExecutorError(f"Pipeline '{body_pipeline}' execution failed: {e}") from e
@@ -0,0 +1,97 @@
1
+ """Components used by the orchestrator.
2
+
3
+ This package contains reusable components that implement specific
4
+ responsibilities in the orchestration pipeline:
5
+
6
+ Primary Components (Recommended):
7
+ - ExecutionCoordinator: Unified policy evaluation, input mapping, event notification
8
+ - LifecycleManager: Unified pre/post execution lifecycle management
9
+
10
+ Execution Components (Keep Unchanged):
11
+ - NodeExecutor: Executes individual nodes with validation and timeout
12
+ - WaveExecutor: Executes waves of parallel nodes with concurrency control
13
+ - CheckpointManager: Manages checkpoint save/restore and graph filtering
14
+
15
+ Deprecated Components (Use Unified Managers Instead):
16
+ - PolicyCoordinator: -> Use ExecutionCoordinator
17
+ - InputMapper: -> Use ExecutionCoordinator
18
+ - HealthCheckManager: -> Use LifecycleManager
19
+ - SecretManager: -> Use LifecycleManager
20
+ - AdapterLifecycleManager: -> Use LifecycleManager
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import importlib
26
+ import warnings
27
+ from typing import Any
28
+
29
+ from hexdag.core.exceptions import OrchestratorError
30
+ from hexdag.core.orchestration.components.checkpoint_manager import CheckpointManager
31
+ from hexdag.core.orchestration.components.execution_coordinator import ExecutionCoordinator
32
+ from hexdag.core.orchestration.components.lifecycle_manager import LifecycleManager
33
+ from hexdag.core.orchestration.components.node_executor import (
34
+ NodeExecutionError,
35
+ NodeExecutor,
36
+ NodeTimeoutError,
37
+ )
38
+ from hexdag.core.orchestration.components.wave_executor import WaveExecutor
39
+
40
+ # Deprecated classes - lazy loaded via __getattr__
41
+ _DEPRECATED_MAPPING: dict[str, tuple[str, str, str]] = {
42
+ "PolicyCoordinator": (
43
+ "hexdag.core.orchestration.components.policy_coordinator",
44
+ "PolicyCoordinator",
45
+ "ExecutionCoordinator",
46
+ ),
47
+ "InputMapper": (
48
+ "hexdag.core.orchestration.components.input_mapper",
49
+ "InputMapper",
50
+ "ExecutionCoordinator",
51
+ ),
52
+ "HealthCheckManager": (
53
+ "hexdag.core.orchestration.components.health_check_manager",
54
+ "HealthCheckManager",
55
+ "LifecycleManager",
56
+ ),
57
+ "SecretManager": (
58
+ "hexdag.core.orchestration.components.secret_manager",
59
+ "SecretManager",
60
+ "LifecycleManager",
61
+ ),
62
+ "AdapterLifecycleManager": (
63
+ "hexdag.core.orchestration.components.adapter_lifecycle_manager",
64
+ "AdapterLifecycleManager",
65
+ "LifecycleManager",
66
+ ),
67
+ }
68
+
69
+
70
+ def __getattr__(name: str) -> Any:
71
+ """Provide deprecated imports with warnings."""
72
+ if name in _DEPRECATED_MAPPING:
73
+ module_path, class_name, replacement = _DEPRECATED_MAPPING[name]
74
+ warnings.warn(
75
+ f"{class_name} is deprecated and will be removed in a future version. "
76
+ f"Use {replacement} instead.",
77
+ DeprecationWarning,
78
+ stacklevel=2,
79
+ )
80
+ module = importlib.import_module(module_path)
81
+ return getattr(module, class_name)
82
+
83
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
84
+
85
+
86
+ __all__ = [
87
+ # Primary components (recommended)
88
+ "ExecutionCoordinator",
89
+ "LifecycleManager",
90
+ # Execution components (keep unchanged)
91
+ "CheckpointManager",
92
+ "NodeExecutionError",
93
+ "NodeExecutor",
94
+ "NodeTimeoutError",
95
+ "OrchestratorError",
96
+ "WaveExecutor",
97
+ ]
@@ -0,0 +1,113 @@
1
+ """Adapter lifecycle manager for cleanup of adapter resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from hexdag.core.logging import get_logger
8
+
9
+ if TYPE_CHECKING:
10
+ from hexdag.core.ports.observer_manager import ObserverManagerPort
11
+ else:
12
+ ObserverManagerPort = Any
13
+
14
+ logger = get_logger(__name__)
15
+
16
+ # Port names to skip during cleanup
17
+ MANAGER_PORT_NAMES = frozenset({"observer_manager"})
18
+
19
+
20
+ class AdapterLifecycleManager:
21
+ """Manages adapter lifecycle including connection cleanup.
22
+
23
+ Responsibilities:
24
+ - Close adapter connections (aclose, ashutdown, cleanup methods)
25
+ - Release resources after DAG execution
26
+ - Track which adapters were cleaned up
27
+
28
+ Examples
29
+ --------
30
+ Example usage::
31
+
32
+ manager = AdapterLifecycleManager()
33
+ result = await manager.cleanup_all_adapters(
34
+ ports={"llm": openai, "database": postgres},
35
+ observer_manager=observer
36
+ )
37
+ # {"cleaned_adapters": ["llm", "database"], "count": 2}
38
+ """
39
+
40
+ # Methods to try for cleanup, in order of preference
41
+ CLEANUP_METHODS = ["aclose", "ashutdown", "cleanup"]
42
+
43
+ async def cleanup_all_adapters(
44
+ self,
45
+ ports: dict[str, Any],
46
+ observer_manager: ObserverManagerPort | None,
47
+ ) -> dict[str, Any]:
48
+ """Close adapter connections and release resources.
49
+
50
+ Parameters
51
+ ----------
52
+ ports : dict[str, Any]
53
+ All available ports
54
+ observer_manager : ObserverManagerPort | None
55
+ Optional observer for event emission
56
+
57
+ Returns
58
+ -------
59
+ dict[str, Any]
60
+ Cleanup results with cleaned_adapters list and count
61
+
62
+ Examples
63
+ --------
64
+ Example usage::
65
+
66
+ result = await manager.cleanup_all_adapters(
67
+ ports={"llm": openai, "database": postgres},
68
+ observer_manager=observer
69
+ )
70
+ # {"cleaned_adapters": ["llm", "database"], "count": 2}
71
+ """
72
+ cleaned_adapters = []
73
+
74
+ for port_name, adapter in ports.items():
75
+ # Skip manager ports
76
+ if port_name in MANAGER_PORT_NAMES:
77
+ continue
78
+
79
+ # Try each cleanup method in order
80
+ if await self._cleanup_single_adapter(port_name, adapter):
81
+ cleaned_adapters.append(port_name)
82
+
83
+ return {"cleaned_adapters": cleaned_adapters, "count": len(cleaned_adapters)}
84
+
85
+ async def _cleanup_single_adapter(self, port_name: str, adapter: Any) -> bool:
86
+ """Attempt to clean up a single adapter.
87
+
88
+ Parameters
89
+ ----------
90
+ port_name : str
91
+ Name of the port
92
+ adapter : Any
93
+ Adapter instance
94
+
95
+ Returns
96
+ -------
97
+ bool
98
+ True if cleanup succeeded, False otherwise
99
+ """
100
+ for method_name in self.CLEANUP_METHODS:
101
+ if hasattr(adapter, method_name) and callable(getattr(adapter, method_name)):
102
+ cleanup_method = getattr(adapter, method_name)
103
+ try:
104
+ logger.debug(f"Cleaning up adapter '{port_name}' via {method_name}()")
105
+ await cleanup_method()
106
+ logger.info(f"✅ Cleaned up adapter: {port_name}")
107
+ return True # Only call first matching cleanup method
108
+ except (RuntimeError, ValueError, TypeError, ConnectionError, OSError) as e:
109
+ # Expected cleanup errors - log but don't crash
110
+ logger.warning(f"Cleanup failed for {port_name}: {e}")
111
+ return False
112
+
113
+ return False