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,134 @@
1
+ """CheckpointManager component for orchestrator state persistence.
2
+
3
+ Storage-agnostic checkpoint manager using Memory Port for maximum flexibility.
4
+ Supports any backend: SQL databases, files (JSON/YAML), Redis, S3, etc.
5
+ """
6
+
7
+ from hexdag.core.domain.dag import DirectedGraph, NodeSpec
8
+ from hexdag.core.orchestration.models import CheckpointState
9
+ from hexdag.core.ports.memory import Memory
10
+
11
+
12
+ class CheckpointManager:
13
+ """Manages orchestrator checkpoints using Memory Port abstraction.
14
+
15
+ This implementation is storage-agnostic and works with any Memory backend:
16
+ - SQL databases (via SQLiteMemoryAdapter)
17
+ - File storage (JSON, YAML, pickle via FileMemoryAdapter)
18
+ - In-memory storage (for testing)
19
+ - Redis, S3, etc.
20
+
21
+ Responsibilities:
22
+ - Save/restore execution state
23
+ - Filter graphs for resume
24
+ - Automatic serialization via Pydantic
25
+
26
+ Parameters
27
+ ----------
28
+ storage : Memory
29
+ Memory port implementation for storage backend
30
+ key_prefix : str, default="checkpoint:"
31
+ Prefix for checkpoint keys (useful for namespacing)
32
+ auto_checkpoint : bool, default=True
33
+ Auto-save after nodes complete
34
+
35
+ Examples
36
+ --------
37
+ In-memory storage (testing)::
38
+
39
+ storage = InMemoryMemory()
40
+ mgr = CheckpointManager(storage=storage)
41
+ await mgr.save(state)
42
+ restored = await mgr.load("run-123")
43
+
44
+ File-based storage (production)::
45
+
46
+ storage = FileMemoryAdapter(base_path="./checkpoints", format="json")
47
+ mgr = CheckpointManager(storage=storage)
48
+
49
+ Database storage (enterprise)::
50
+
51
+ db = SQLiteAdapter(db_path="hexdag.db")
52
+ storage = SQLiteMemoryAdapter(database=db)
53
+ mgr = CheckpointManager(storage=storage)
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ storage: Memory,
59
+ key_prefix: str = "checkpoint:",
60
+ auto_checkpoint: bool = True,
61
+ ):
62
+ self.storage = storage
63
+ self.key_prefix = key_prefix
64
+ self.auto_checkpoint = auto_checkpoint
65
+
66
+ def _make_key(self, run_id: str) -> str:
67
+ """Generate storage key for a run_id."""
68
+ return f"{self.key_prefix}{run_id}"
69
+
70
+ async def save(self, state: CheckpointState) -> None:
71
+ """Save checkpoint state.
72
+
73
+ Uses Pydantic's model_dump_json() for automatic serialization.
74
+ All complex types (datetime, nested models) are handled automatically.
75
+
76
+ Parameters
77
+ ----------
78
+ state : CheckpointState
79
+ Complete checkpoint state to persist
80
+ """
81
+ key = self._make_key(state.run_id)
82
+ # Pydantic handles all serialization including datetime, nested models, etc.
83
+ serialized = state.model_dump_json()
84
+ await self.storage.aset(key, serialized)
85
+
86
+ async def load(self, run_id: str) -> CheckpointState | None:
87
+ """Load checkpoint state by run_id.
88
+
89
+ Uses Pydantic's model_validate_json() for automatic deserialization.
90
+
91
+ Parameters
92
+ ----------
93
+ run_id : str
94
+ Run identifier to load
95
+
96
+ Returns
97
+ -------
98
+ CheckpointState | None
99
+ Restored checkpoint state, or None if not found
100
+ """
101
+ key = self._make_key(run_id)
102
+ serialized = await self.storage.aget(key)
103
+
104
+ if serialized is None:
105
+ return None
106
+
107
+ # Pydantic handles all deserialization and validation
108
+ return CheckpointState.model_validate_json(serialized)
109
+
110
+ def filter_completed(self, graph: DirectedGraph, completed: set[str]) -> DirectedGraph:
111
+ """Create graph with only pending nodes.
112
+
113
+ Parameters
114
+ ----------
115
+ graph : DirectedGraph
116
+ Original DAG
117
+ completed : set[str]
118
+ Set of completed node names
119
+
120
+ Returns
121
+ -------
122
+ DirectedGraph
123
+ New graph with only pending nodes and updated dependencies
124
+ """
125
+ pending = DirectedGraph()
126
+ for spec in graph: # Using iterator instead of .nodes.items()
127
+ if spec.name not in completed:
128
+ pending += NodeSpec( # Using += operator instead of .add()
129
+ name=spec.name,
130
+ fn=spec.fn,
131
+ deps=frozenset(d for d in spec.deps if d not in completed),
132
+ timeout=spec.timeout,
133
+ )
134
+ return pending
@@ -0,0 +1,360 @@
1
+ """Execution coordinator for observer notifications and input mapping.
2
+
3
+ This module provides execution coordination functionality:
4
+
5
+ - Observer notifications during execution
6
+ - Input preparation and dependency mapping
7
+ - Input mapping transformation (including $input syntax)
8
+ """
9
+
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ if TYPE_CHECKING:
13
+ from hexdag.core.ports.observer_manager import ObserverManagerPort
14
+ else:
15
+ ObserverManagerPort = Any
16
+
17
+ from hexdag.core.domain.dag import NodeSpec
18
+ from hexdag.core.logging import get_logger
19
+
20
+ __all__ = ["ExecutionCoordinator"]
21
+
22
+ logger = get_logger(__name__)
23
+
24
+
25
+ class ExecutionCoordinator:
26
+ """Coordinates execution context: observer notifications and input mapping.
27
+
28
+ This component handles two responsibilities:
29
+
30
+ 1. **Observer Notifications**: Notifying observers of events during DAG execution.
31
+
32
+ 2. **Input Mapping**: Preparing input data for nodes based on their dependencies.
33
+ Uses a smart mapping strategy:
34
+ - No dependencies → initial input
35
+ - Single dependency → pass through that result
36
+ - Multiple dependencies → dict of results
37
+
38
+ Examples
39
+ --------
40
+ Basic usage::
41
+
42
+ coordinator = ExecutionCoordinator()
43
+
44
+ # Notify observer of an event
45
+ await coordinator.notify_observer(observer_manager, NodeStarted(...))
46
+
47
+ # Prepare input for a node
48
+ input_data = coordinator.prepare_node_input(
49
+ node_spec, node_results, initial_input
50
+ )
51
+ """
52
+
53
+ # ========================================================================
54
+ # Observer Notifications (from PolicyCoordinator)
55
+ # ========================================================================
56
+
57
+ async def notify_observer(
58
+ self, observer_manager: ObserverManagerPort | None, event: Any
59
+ ) -> None:
60
+ """Notify observer manager of an event if it exists.
61
+
62
+ Parameters
63
+ ----------
64
+ observer_manager : ObserverManagerPort | None
65
+ Observer manager to notify (None if no observer configured)
66
+ event : Any
67
+ Event to send (typically NodeStarted, NodeCompleted, etc.)
68
+
69
+ Examples
70
+ --------
71
+ >>> from hexdag.core.orchestration.events import NodeStarted
72
+ >>> event = NodeStarted(name="my_node", wave_index=0)
73
+ >>> await coordinator.notify_observer(observer_manager, event) # doctest: +SKIP
74
+ """
75
+ if observer_manager:
76
+ await observer_manager.notify(event)
77
+
78
+ # ========================================================================
79
+ # Input Mapping
80
+ # ========================================================================
81
+
82
+ def prepare_node_input(
83
+ self, node_spec: NodeSpec, node_results: dict[str, Any], initial_input: Any
84
+ ) -> Any:
85
+ """Prepare input data for node execution with simplified data mapping.
86
+
87
+ The mapping strategy is:
88
+ 1. **No dependencies** → initial_input (entry point)
89
+ 2. **Single dependency** → results[dependency_name] (pass-through)
90
+ 3. **Multiple dependencies** → {dep1: result1, dep2: result2, ...} (namespace)
91
+
92
+ This approach balances simplicity (pass-through for single deps) with
93
+ clarity (named dict for multiple deps).
94
+
95
+ Parameters
96
+ ----------
97
+ node_spec : NodeSpec
98
+ Node specification containing dependencies
99
+ node_results : dict[str, Any]
100
+ Results from previously executed nodes
101
+ initial_input : Any
102
+ Initial input data for the pipeline
103
+
104
+ Returns
105
+ -------
106
+ Any
107
+ Prepared input data for the node:
108
+ - initial_input if no dependencies
109
+ - dependency result if single dependency
110
+ - dict of dependency results if multiple dependencies
111
+
112
+ Examples
113
+ --------
114
+ >>> coordinator = ExecutionCoordinator()
115
+ >>>
116
+ >>> # No dependencies - gets initial input
117
+ >>> # start_input = coordinator.prepare_node_input(
118
+ >>> # NodeSpec("start", lambda x: x.upper()),
119
+ >>> # node_results={},
120
+ >>> # initial_input="hello"
121
+ >>> # )
122
+ >>> # start_input == "hello"
123
+ >>>
124
+ >>> # Single dependency - gets that result directly
125
+ >>> # process_input = coordinator.prepare_node_input(
126
+ >>> # NodeSpec("process", lambda x: x + "!", deps={"start"}),
127
+ >>> # node_results={"start": "HELLO"},
128
+ >>> # initial_input="hello"
129
+ >>> # )
130
+ >>> # process_input == "HELLO"
131
+
132
+ Notes
133
+ -----
134
+ The multi-dependency dict preserves node names as keys, making it clear
135
+ where each piece of data came from. This is especially useful for
136
+ debugging and for nodes that need to treat different dependencies
137
+ differently.
138
+
139
+ If the node has an ``input_mapping`` in its params, the prepared input
140
+ will be transformed according to the mapping. This supports:
141
+ - ``$input.field`` - Reference the initial pipeline input
142
+ - ``node_name.field`` - Reference a specific dependency's output
143
+ """
144
+ # Prepare base input from dependencies
145
+ if not node_spec.deps:
146
+ base_input = initial_input
147
+ elif len(node_spec.deps) == 1:
148
+ dep_name = next(iter(node_spec.deps))
149
+ base_input = node_results.get(dep_name, initial_input)
150
+ else:
151
+ # Multiple dependencies - preserve namespace structure
152
+ base_input = {}
153
+ for dep_name in node_spec.deps:
154
+ if dep_name in node_results:
155
+ base_input[dep_name] = node_results[dep_name]
156
+
157
+ # Apply input_mapping if present in node params
158
+ input_mapping = node_spec.params.get("input_mapping") if node_spec.params else None
159
+ if input_mapping:
160
+ return self._apply_input_mapping(base_input, input_mapping, initial_input, node_results)
161
+
162
+ return base_input
163
+
164
+ def _is_expression(self, source: str) -> bool:
165
+ """Check if a source string is an expression (contains function calls or operators).
166
+
167
+ Parameters
168
+ ----------
169
+ source : str
170
+ The source string to check
171
+
172
+ Returns
173
+ -------
174
+ bool
175
+ True if the source appears to be an expression
176
+ """
177
+ from hexdag.core.expression_parser import ALLOWED_FUNCTIONS
178
+
179
+ # Check for function call patterns (function_name followed by parenthesis)
180
+ for func_name in ALLOWED_FUNCTIONS:
181
+ if f"{func_name}(" in source:
182
+ return True
183
+
184
+ # Check for arithmetic/comparison operators (but not dots which are field paths)
185
+ # Be careful not to match operators in simple field paths
186
+ expression_indicators = [
187
+ "==",
188
+ "!=",
189
+ "<=",
190
+ ">=",
191
+ " < ",
192
+ " > ",
193
+ " + ",
194
+ " - ",
195
+ " * ",
196
+ " / ",
197
+ " % ",
198
+ " and ",
199
+ " or ",
200
+ " not ",
201
+ " in ",
202
+ ]
203
+ return any(op in source for op in expression_indicators)
204
+
205
+ def _apply_input_mapping(
206
+ self,
207
+ base_input: Any,
208
+ input_mapping: dict[str, str],
209
+ initial_input: Any,
210
+ node_results: dict[str, Any],
211
+ ) -> dict[str, Any]:
212
+ """Apply field mapping to transform input data.
213
+
214
+ Supports multiple syntaxes:
215
+ - ``$input.field`` - Extract from the initial pipeline input
216
+ - ``node_name.field`` - Extract from a specific node's output
217
+ - Expression syntax - Use allowed functions and operators
218
+
219
+ Parameters
220
+ ----------
221
+ base_input : Any
222
+ The prepared input from dependencies (may be single value or dict)
223
+ input_mapping : dict[str, str]
224
+ Mapping of {target_field: "source_path"} or {target_field: "expression"}
225
+ initial_input : Any
226
+ The original pipeline input (for $input references)
227
+ node_results : dict[str, Any]
228
+ Results from all previously executed nodes
229
+
230
+ Returns
231
+ -------
232
+ dict[str, Any]
233
+ Transformed input with mapped fields
234
+
235
+ Examples
236
+ --------
237
+ >>> coordinator = ExecutionCoordinator()
238
+ >>> mapping = {"load_id": "$input.load_id", "result": "analyzer.output"}
239
+ >>> # This would extract load_id from initial input and result from analyzer node
240
+
241
+ Expression examples::
242
+
243
+ mapping = {
244
+ "is_valid": "len(items) > 0",
245
+ "name_upper": "upper(user.name)",
246
+ "total": "price * quantity",
247
+ }
248
+ """
249
+ from hexdag.builtin.nodes.mapped_input import FieldExtractor
250
+
251
+ result: dict[str, Any] = {}
252
+
253
+ for target_field, source_path in input_mapping.items():
254
+ # Check if this is an expression that needs evaluation
255
+ if self._is_expression(source_path):
256
+ value = self._evaluate_expression(
257
+ source_path, base_input, initial_input, node_results
258
+ )
259
+ elif source_path.startswith("$input."):
260
+ # Extract from initial pipeline input
261
+ actual_path = source_path[7:] # Remove "$input." prefix
262
+ if actual_path:
263
+ # Has a field path like "$input.my_field"
264
+ if isinstance(initial_input, dict):
265
+ value = FieldExtractor.extract(initial_input, actual_path)
266
+ else:
267
+ # Non-dict input - wrap and extract
268
+ value = FieldExtractor.extract({"_root": initial_input}, "_root")
269
+ else:
270
+ # Just "$input." with no field - return entire initial input
271
+ value = initial_input
272
+ elif source_path == "$input":
273
+ # Reference the entire initial input
274
+ value = initial_input
275
+ elif "." in source_path:
276
+ # Check if it's a node_name.field pattern
277
+ parts = source_path.split(".", 1)
278
+ node_name, field_path = parts[0], parts[1]
279
+ if node_name in node_results:
280
+ # Extract from specific node's result
281
+ value = FieldExtractor.extract(node_results[node_name], field_path)
282
+ else:
283
+ # Fall back to extracting from base_input
284
+ value = FieldExtractor.extract(
285
+ base_input if isinstance(base_input, dict) else {}, source_path
286
+ )
287
+ else:
288
+ # Simple field name - extract from base_input
289
+ value = FieldExtractor.extract(
290
+ base_input if isinstance(base_input, dict) else {}, source_path
291
+ )
292
+
293
+ if value is None:
294
+ logger.warning(
295
+ f"input_mapping: '{source_path}' resolved to None for target '{target_field}'"
296
+ )
297
+
298
+ result[target_field] = value
299
+
300
+ return result
301
+
302
+ def _evaluate_expression(
303
+ self,
304
+ expression: str,
305
+ base_input: Any,
306
+ initial_input: Any,
307
+ node_results: dict[str, Any],
308
+ ) -> Any:
309
+ """Evaluate an expression against available data.
310
+
311
+ Parameters
312
+ ----------
313
+ expression : str
314
+ The expression to evaluate (e.g., "len(items) > 0")
315
+ base_input : Any
316
+ The prepared input from dependencies
317
+ initial_input : Any
318
+ The original pipeline input
319
+ node_results : dict[str, Any]
320
+ Results from all previously executed nodes
321
+
322
+ Returns
323
+ -------
324
+ Any
325
+ The result of evaluating the expression
326
+ """
327
+ from hexdag.core.expression_parser import ExpressionError, evaluate_expression
328
+
329
+ # Build the data context for expression evaluation
330
+ # Merge all available data sources into a single dict
331
+ data_context: dict[str, Any] = {}
332
+
333
+ # Add node results
334
+ data_context.update(node_results)
335
+
336
+ # Add base_input (either as-is if dict, or wrapped)
337
+ if isinstance(base_input, dict):
338
+ data_context.update(base_input)
339
+ elif base_input is not None:
340
+ data_context["_input"] = base_input
341
+
342
+ # Add initial input with $input prefix removed (accessible as 'input')
343
+ if isinstance(initial_input, dict):
344
+ data_context["input"] = initial_input
345
+ # Also add initial_input fields at top level for convenience
346
+ for key, val in initial_input.items():
347
+ if key not in data_context:
348
+ data_context[key] = val
349
+ elif initial_input is not None:
350
+ data_context["input"] = initial_input
351
+
352
+ try:
353
+ # Use evaluate_expression to get the actual value, not a boolean
354
+ return evaluate_expression(expression, data_context, {})
355
+ except ExpressionError as e:
356
+ logger.error(f"Expression evaluation failed for '{expression}': {e}")
357
+ return None
358
+ except Exception as e:
359
+ logger.error(f"Unexpected error evaluating expression '{expression}': {e}")
360
+ return None
@@ -0,0 +1,176 @@
1
+ """Health check manager for pre-DAG adapter validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from hexdag.core.ports.observer_manager import ObserverManagerPort
9
+ else:
10
+ ObserverManagerPort = Any
11
+
12
+ from hexdag.core.logging import get_logger
13
+ from hexdag.core.orchestration.events import HealthCheckCompleted
14
+ from hexdag.core.ports.healthcheck import HealthStatus
15
+ from hexdag.core.protocols import HealthCheckable
16
+
17
+ logger = get_logger(__name__)
18
+
19
+ # Constants
20
+ MANAGER_PORT_NAMES = frozenset({"observer_manager"})
21
+ LATENCY_PRECISION = 1 # Decimal places for latency display
22
+
23
+
24
+ class HealthCheckManager:
25
+ """Manages health checks on adapters before DAG execution.
26
+
27
+ Responsibilities:
28
+ - Check adapter health via ahealth_check() method
29
+ - Emit HealthCheckCompleted events
30
+ - Determine if unhealthy adapters should block execution
31
+
32
+ Examples
33
+ --------
34
+ Example usage::
35
+
36
+ manager = HealthCheckManager(fail_fast=True, warn_only=False)
37
+ health_results = await manager.check_all_adapters(
38
+ ports={"llm": openai, "database": postgres},
39
+ observer_manager=observer,
40
+ pipeline_name="my_pipeline"
41
+ )
42
+ """
43
+
44
+ def __init__(self, fail_fast: bool = False, warn_only: bool = True):
45
+ """Initialize health check manager.
46
+
47
+ Parameters
48
+ ----------
49
+ fail_fast : bool, default=False
50
+ If True, unhealthy adapters block pipeline execution
51
+ warn_only : bool, default=True
52
+ If True, log warnings for unhealthy adapters but don't block
53
+ """
54
+ self.fail_fast = fail_fast
55
+ self.warn_only = warn_only
56
+
57
+ async def check_all_adapters(
58
+ self,
59
+ ports: dict[str, Any],
60
+ observer_manager: ObserverManagerPort | None,
61
+ pipeline_name: str,
62
+ ) -> list[HealthStatus]:
63
+ """Run health checks on all adapters that implement ahealth_check().
64
+
65
+ Parameters
66
+ ----------
67
+ ports : dict[str, Any]
68
+ All available ports
69
+ observer_manager : ObserverManagerPort | None
70
+ Optional observer for event emission
71
+ pipeline_name : str
72
+ Name of the pipeline
73
+
74
+ Returns
75
+ -------
76
+ list[HealthStatus]
77
+ Health status results from all adapters
78
+ """
79
+ health_results = []
80
+
81
+ for port_name, adapter in ports.items():
82
+ # Skip non-adapter ports
83
+ if port_name in MANAGER_PORT_NAMES:
84
+ continue
85
+
86
+ if isinstance(adapter, HealthCheckable):
87
+ status = await self._check_single_adapter(port_name, adapter, observer_manager)
88
+ health_results.append(status)
89
+
90
+ return health_results
91
+
92
+ async def _check_single_adapter(
93
+ self,
94
+ port_name: str,
95
+ adapter: Any,
96
+ observer_manager: ObserverManagerPort | None,
97
+ ) -> HealthStatus:
98
+ """Check health of a single adapter.
99
+
100
+ Parameters
101
+ ----------
102
+ port_name : str
103
+ Name of the port
104
+ adapter : Any
105
+ Adapter instance
106
+ observer_manager : ObserverManagerPort | None
107
+ Optional observer for event emission
108
+
109
+ Returns
110
+ -------
111
+ HealthStatus
112
+ Health status of the adapter
113
+ """
114
+ try:
115
+ logger.debug(f"Running health check for {port_name}")
116
+ health_check = adapter.ahealth_check
117
+ status: HealthStatus = await health_check() # pyright: ignore[reportGeneralTypeIssues]
118
+ status.port_name = port_name # Ensure port name is set
119
+
120
+ # Emit event
121
+ if observer_manager:
122
+ event = HealthCheckCompleted(
123
+ adapter_name=status.adapter_name,
124
+ port_name=port_name,
125
+ status=status,
126
+ )
127
+ await observer_manager.notify(event)
128
+
129
+ # Log result
130
+ self._log_health_result(port_name, status)
131
+
132
+ return status
133
+
134
+ except (RuntimeError, ConnectionError, TimeoutError, ValueError) as e:
135
+ # Health check errors - mark adapter as unhealthy
136
+ logger.error(f"Health check failed for {port_name}: {e}", exc_info=True)
137
+ adapter_name = getattr(adapter, "_hexdag_name", port_name)
138
+ return HealthStatus(
139
+ status="unhealthy",
140
+ adapter_name=adapter_name,
141
+ port_name=port_name,
142
+ error=e,
143
+ )
144
+
145
+ def _log_health_result(self, port_name: str, status: HealthStatus) -> None:
146
+ """Log health check result.
147
+
148
+ Parameters
149
+ ----------
150
+ port_name : str
151
+ Name of the port
152
+ status : HealthStatus
153
+ Health status result
154
+ """
155
+ if status.status == "healthy":
156
+ latency_info = (
157
+ f" ({status.latency_ms:.{LATENCY_PRECISION}f}ms)" if status.latency_ms else ""
158
+ )
159
+ logger.info(f"✅ {port_name} health check: {status.status}{latency_info}")
160
+ else:
161
+ logger.warning(f"⚠️ {port_name} health check: {status.status} - {status.error}")
162
+
163
+ def get_unhealthy_adapters(self, health_results: list[HealthStatus]) -> list[HealthStatus]:
164
+ """Filter health results to only unhealthy adapters.
165
+
166
+ Parameters
167
+ ----------
168
+ health_results : list[HealthStatus]
169
+ All health check results
170
+
171
+ Returns
172
+ -------
173
+ list[HealthStatus]
174
+ Only the unhealthy adapters
175
+ """
176
+ return [h for h in health_results if h.status == "unhealthy"]