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,386 @@
1
+ """Component instantiator for creating adapters and policies from YAML specs.
2
+
3
+ This module handles:
4
+ - Parsing native YAML dict component specifications
5
+ - Resolving components via module paths
6
+ - Instantiating adapters with configuration
7
+ - Instantiating policies with configuration
8
+ - Runtime resolution of deferred environment variables
9
+ """
10
+
11
+ import os
12
+ import re
13
+ from collections import namedtuple
14
+ from typing import Any
15
+
16
+ from hexdag.core.logging import get_logger
17
+ from hexdag.core.resolver import resolve
18
+
19
+ logger = get_logger(__name__)
20
+
21
+ # Pattern for deferred environment variables: ${VAR} or ${VAR:default}
22
+ _DEFERRED_ENV_VAR_PATTERN = re.compile(r"\$\{([A-Z_][A-Z0-9_]*)(?::([^}]*))?\}")
23
+
24
+ # Simple namedtuple for component specification
25
+ ComponentSpec = namedtuple("ComponentSpec", ["module_path", "params"])
26
+
27
+
28
+ class ComponentInstantiationError(Exception):
29
+ """Error instantiating component from specification."""
30
+
31
+ pass
32
+
33
+
34
+ def _resolve_deferred_env_vars(params: dict[str, Any]) -> dict[str, Any]:
35
+ """Resolve deferred ${VAR} syntax in parameters at runtime.
36
+
37
+ This completes the deferred secret resolution workflow where secrets like
38
+ ${OPENAI_API_KEY} are preserved at YAML build-time and resolved here
39
+ at adapter instantiation time.
40
+
41
+ Parameters
42
+ ----------
43
+ params : dict[str, Any]
44
+ Parameters potentially containing ${VAR} or ${VAR:default} syntax
45
+
46
+ Returns
47
+ -------
48
+ dict[str, Any]
49
+ Parameters with environment variables resolved
50
+
51
+ Note
52
+ ----
53
+ Raises ComponentInstantiationError (via _resolve_string_value) if a
54
+ required environment variable is not set and has no default.
55
+
56
+ Examples
57
+ --------
58
+ >>> import os
59
+ >>> os.environ["TEST_KEY"] = "secret123"
60
+ >>> _resolve_deferred_env_vars({"api_key": "${TEST_KEY}"})
61
+ {'api_key': 'secret123'}
62
+ >>> _resolve_deferred_env_vars({"model": "${MODEL:gpt-4}"})
63
+ {'model': 'gpt-4'}
64
+ """
65
+ resolved: dict[str, Any] = {}
66
+ for key, value in params.items():
67
+ if isinstance(value, str):
68
+ resolved[key] = _resolve_string_value(value)
69
+ elif isinstance(value, dict):
70
+ resolved[key] = _resolve_deferred_env_vars(value)
71
+ elif isinstance(value, list):
72
+ resolved[key] = [
73
+ _resolve_string_value(v)
74
+ if isinstance(v, str)
75
+ else _resolve_deferred_env_vars(v)
76
+ if isinstance(v, dict)
77
+ else v
78
+ for v in value
79
+ ]
80
+ else:
81
+ resolved[key] = value
82
+ return resolved
83
+
84
+
85
+ def _resolve_string_value(value: str) -> str:
86
+ """Resolve ${VAR} or ${VAR:default} patterns in a string value.
87
+
88
+ Parameters
89
+ ----------
90
+ value : str
91
+ String potentially containing ${VAR} patterns
92
+
93
+ Returns
94
+ -------
95
+ str
96
+ String with all ${VAR} patterns resolved
97
+
98
+ Note
99
+ ----
100
+ Raises ComponentInstantiationError (via nested replacer) if a required
101
+ environment variable is not set and has no default.
102
+ """
103
+
104
+ def replacer(match: re.Match[str]) -> str:
105
+ var_name = match.group(1)
106
+ default = match.group(2)
107
+ env_value = os.environ.get(var_name)
108
+
109
+ if env_value is None:
110
+ if default is not None:
111
+ logger.debug(f"Using default value for ${{{var_name}}}")
112
+ return default
113
+ raise ComponentInstantiationError(
114
+ f"Environment variable '{var_name}' is not set and no default provided. "
115
+ f"Set the environment variable or use ${{​{var_name}:default}} syntax."
116
+ )
117
+
118
+ logger.debug(f"Resolved ${{{var_name}}} from environment")
119
+ return env_value
120
+
121
+ return _DEFERRED_ENV_VAR_PATTERN.sub(replacer, value)
122
+
123
+
124
+ class ComponentInstantiator:
125
+ """Instantiates adapters and policies from native YAML dict specifications."""
126
+
127
+ def __init__(self) -> None:
128
+ """Initialize component instantiator."""
129
+ pass # No bootstrap needed anymore
130
+
131
+ def _parse_component_spec(self, spec: dict[str, Any]) -> ComponentSpec:
132
+ """Parse component specification from native YAML dict format.
133
+
134
+ Parameters
135
+ ----------
136
+ spec : dict[str, Any]
137
+ Component specification with either:
138
+ - "adapter": "module.path.ClassName" and optional "config": {...}
139
+ - "name": "module.path.ClassName" and optional "params": {...}
140
+
141
+ Returns
142
+ -------
143
+ ComponentSpec
144
+ Parsed component specification with module_path and params
145
+
146
+ Raises
147
+ ------
148
+ ComponentInstantiationError
149
+ If specification format is invalid
150
+ """
151
+ if not isinstance(spec, dict):
152
+ raise ComponentInstantiationError(
153
+ f"Component specification must be a dict, got {type(spec).__name__}. "
154
+ f"Use format: {{adapter: 'module.path.Class', config: {{...}}}}"
155
+ )
156
+
157
+ # Support both "adapter" and "name" keys for module path
158
+ module_path = spec.get("adapter") or spec.get("name")
159
+ # Support both "config" and "params" keys for parameters
160
+ params = spec.get("config") or spec.get("params", {})
161
+
162
+ if not module_path:
163
+ raise ComponentInstantiationError(
164
+ f"Component specification requires 'adapter' or 'name' field. Got: {spec}"
165
+ )
166
+
167
+ return ComponentSpec(module_path=module_path, params=params)
168
+
169
+ def instantiate_adapter(self, spec: dict[str, Any], port_name: str | None = None) -> Any:
170
+ """Instantiate an adapter from native YAML dict specification.
171
+
172
+ Parameters
173
+ ----------
174
+ spec : dict[str, Any]
175
+ Adapter specification: {"adapter": "hexdag.builtin.adapters.mock.MockLLM",
176
+ "config": {"model": "gpt-4"}}
177
+ port_name : str | None
178
+ Optional port name for context in error messages
179
+
180
+ Returns
181
+ -------
182
+ Any
183
+ Instantiated adapter instance
184
+
185
+ Raises
186
+ ------
187
+ ComponentInstantiationError
188
+ If adapter cannot be instantiated
189
+
190
+ Examples
191
+ --------
192
+ >>> instantiator = ComponentInstantiator() # doctest: +SKIP
193
+ >>> adapter = instantiator.instantiate_adapter({ # doctest: +SKIP
194
+ ... "adapter": "hexdag.builtin.adapters.mock.MockLLM",
195
+ ... "config": {"model": "gpt-4"}
196
+ ... })
197
+ """
198
+ try:
199
+ # Parse the specification
200
+ component_spec = self._parse_component_spec(spec)
201
+
202
+ try:
203
+ adapter_class = resolve(component_spec.module_path)
204
+ except Exception as e:
205
+ raise ComponentInstantiationError(
206
+ f"Adapter '{component_spec.module_path}' "
207
+ f"could not be resolved. "
208
+ f"Make sure the module path is correct. Error: {e}"
209
+ ) from e
210
+
211
+ # Instantiate the adapter class with parameters
212
+ if isinstance(adapter_class, type):
213
+ try:
214
+ # Resolve any deferred environment variables at instantiation time
215
+ resolved_params = _resolve_deferred_env_vars(component_spec.params)
216
+ adapter_instance = adapter_class(**resolved_params)
217
+ logger.info(
218
+ f"Instantiated adapter '{component_spec.module_path}' "
219
+ f"for port '{port_name}'"
220
+ )
221
+ return adapter_instance
222
+ except Exception as e:
223
+ raise ComponentInstantiationError(
224
+ f"Failed to instantiate adapter "
225
+ f"'{component_spec.module_path}' "
226
+ f"with params {component_spec.params}. Error: {e}"
227
+ ) from e
228
+ else:
229
+ # It's already an instance (runtime-registered non-class component)
230
+ if component_spec.params: # type: ignore[unreachable]
231
+ logger.warning(
232
+ f"Adapter '{component_spec.module_path}' "
233
+ f"resolved to an instance. Parameters {component_spec.params} "
234
+ f"will be ignored."
235
+ )
236
+ logger.info(
237
+ f"Using resolved adapter instance "
238
+ f"'{component_spec.module_path}' "
239
+ f"for port '{port_name}'"
240
+ )
241
+ return adapter_class
242
+
243
+ except ComponentInstantiationError:
244
+ raise
245
+ except Exception as e:
246
+ raise ComponentInstantiationError(
247
+ f"Failed to instantiate adapter for port '{port_name}': {e}"
248
+ ) from e
249
+
250
+ def instantiate_policy(self, spec: dict[str, Any], policy_name: str | None = None) -> Any:
251
+ """Instantiate a policy from native YAML dict specification.
252
+
253
+ Parameters
254
+ ----------
255
+ spec : dict[str, Any]
256
+ Policy specification: {"name": "hexdag.builtin.policies.RetryPolicy",
257
+ "params": {"max_retries": 3}}
258
+ policy_name : str | None
259
+ Optional policy name for context in error messages
260
+
261
+ Returns
262
+ -------
263
+ Any
264
+ Instantiated policy instance
265
+
266
+ Raises
267
+ ------
268
+ ComponentInstantiationError
269
+ If policy cannot be instantiated
270
+
271
+ Examples
272
+ --------
273
+ >>> instantiator = ComponentInstantiator() # doctest: +SKIP
274
+ >>> policy = instantiator.instantiate_policy({ # doctest: +SKIP
275
+ ... "name": "hexdag.builtin.policies.execution_policies.RetryPolicy",
276
+ ... "params": {"max_retries": 5}
277
+ ... })
278
+ """
279
+ try:
280
+ # Parse the specification
281
+ component_spec = self._parse_component_spec(spec)
282
+
283
+ try:
284
+ policy_class = resolve(component_spec.module_path)
285
+ except Exception as e:
286
+ raise ComponentInstantiationError(
287
+ f"Policy '{component_spec.module_path}' "
288
+ f"could not be resolved. "
289
+ f"Make sure the module path is correct. Error: {e}"
290
+ ) from e
291
+
292
+ if not isinstance(policy_class, type):
293
+ raise ComponentInstantiationError(
294
+ f"Policy '{component_spec.module_path}' resolved to an instance, "
295
+ f"not a class. Cannot instantiate from instance."
296
+ )
297
+
298
+ # Instantiate with parameters
299
+ try:
300
+ # Resolve any deferred environment variables at instantiation time
301
+ resolved_params = _resolve_deferred_env_vars(component_spec.params)
302
+ policy_instance = policy_class(**resolved_params)
303
+ logger.info(f"Instantiated policy '{component_spec.module_path}' ('{policy_name}')")
304
+ return policy_instance
305
+ except Exception as e:
306
+ raise ComponentInstantiationError(
307
+ f"Failed to instantiate policy "
308
+ f"'{component_spec.module_path}' "
309
+ f"with params {component_spec.params}. Error: {e}"
310
+ ) from e
311
+
312
+ except ComponentInstantiationError:
313
+ raise
314
+ except Exception as e:
315
+ raise ComponentInstantiationError(
316
+ f"Failed to instantiate policy '{policy_name}': {e}"
317
+ ) from e
318
+
319
+ def instantiate_ports(self, ports_config: dict[str, dict[str, Any]]) -> dict[str, Any]:
320
+ """Instantiate all ports (adapters) from configuration.
321
+
322
+ Parameters
323
+ ----------
324
+ ports_config : dict[str, dict[str, Any]]
325
+ Dictionary of port_name -> adapter dict spec
326
+
327
+ Returns
328
+ -------
329
+ dict[str, Any]
330
+ Dictionary of port_name -> adapter_instance
331
+
332
+ Examples
333
+ --------
334
+ >>> instantiator = ComponentInstantiator() # doctest: +SKIP
335
+ >>> config = { # doctest: +SKIP
336
+ ... "llm": {"adapter": "hexdag.builtin.adapters.mock.MockLLM",
337
+ ... "config": {"model": "gpt-4"}},
338
+ ... "database": {"adapter": "hexdag.builtin.adapters.mock.MockDatabaseAdapter",
339
+ ... "config": {}}
340
+ ... }
341
+ """
342
+ ports: dict[str, Any] = {}
343
+
344
+ for port_name, adapter_spec in ports_config.items():
345
+ try:
346
+ ports[port_name] = self.instantiate_adapter(adapter_spec, port_name=port_name)
347
+ except ComponentInstantiationError as e:
348
+ logger.error(f"Failed to instantiate adapter for port '{port_name}': {e}")
349
+ raise
350
+
351
+ return ports
352
+
353
+ def instantiate_policies(self, policies_config: dict[str, dict[str, Any]]) -> list[Any]:
354
+ """Instantiate all policies from configuration.
355
+
356
+ Parameters
357
+ ----------
358
+ policies_config : dict[str, dict[str, Any]]
359
+ Dictionary of policy_name -> policy dict spec
360
+
361
+ Returns
362
+ -------
363
+ list[Any]
364
+ List of policy instances
365
+
366
+ Examples
367
+ --------
368
+ >>> instantiator = ComponentInstantiator() # doctest: +SKIP
369
+ >>> config = { # doctest: +SKIP
370
+ ... "retry": {"name": "hexdag.builtin.policies.RetryPolicy",
371
+ ... "params": {"max_retries": 3}},
372
+ ... "timeout": {"name": "hexdag.builtin.policies.TimeoutPolicy",
373
+ ... "params": {"timeout_seconds": 300}}
374
+ ... }
375
+ """
376
+ policies: list[Any] = []
377
+
378
+ for policy_name, policy_spec in policies_config.items():
379
+ try:
380
+ policy = self.instantiate_policy(policy_spec, policy_name=policy_name)
381
+ policies.append(policy)
382
+ except ComponentInstantiationError as e:
383
+ logger.error(f"Failed to instantiate policy '{policy_name}': {e}")
384
+ raise
385
+
386
+ return policies
@@ -0,0 +1,265 @@
1
+ """!include YAML custom tag handler for pipeline composition.
2
+
3
+ This module provides a YAML custom tag constructor that includes content
4
+ from external YAML files, enabling modular pipeline composition.
5
+
6
+ Examples
7
+ --------
8
+ Include a list of nodes::
9
+
10
+ spec:
11
+ nodes:
12
+ - kind: expression_node
13
+ metadata:
14
+ name: start
15
+ spec:
16
+ expressions:
17
+ ready: "true"
18
+
19
+ # Include nodes from external file
20
+ - !include ./shared/validation_nodes.yaml
21
+
22
+ - kind: llm_node
23
+ metadata:
24
+ name: final
25
+ spec:
26
+ prompt_template: "Finalize: {{input}}"
27
+
28
+ Include a partial pipeline fragment::
29
+
30
+ # In shared/validation_nodes.yaml
31
+ - kind: function_node
32
+ metadata:
33
+ name: validate_input
34
+ spec:
35
+ fn: "myapp.validate"
36
+
37
+ - kind: expression_node
38
+ metadata:
39
+ name: check_result
40
+ spec:
41
+ expressions:
42
+ valid: "validate_input.success"
43
+
44
+ Include with variable substitution::
45
+
46
+ # Use !include with a mapping for variable substitution
47
+ - !include
48
+ path: ./templates/processor.yaml
49
+ vars:
50
+ node_name: "custom_processor"
51
+ timeout: 30
52
+ """
53
+
54
+ from pathlib import Path
55
+ from typing import Any
56
+
57
+ import yaml
58
+
59
+ from hexdag.core.logging import get_logger
60
+
61
+ logger = get_logger(__name__)
62
+
63
+ # Thread-local storage for base path during parsing
64
+ _current_base_path: Path | None = None
65
+
66
+
67
+ class IncludeTagError(Exception):
68
+ """Error including external YAML file."""
69
+
70
+ pass
71
+
72
+
73
+ def set_include_base_path(base_path: Path | None) -> None:
74
+ """Set the base path for resolving !include paths.
75
+
76
+ This should be called before parsing YAML that may contain !include tags.
77
+
78
+ Parameters
79
+ ----------
80
+ base_path : Path | None
81
+ Base directory for resolving relative paths
82
+ """
83
+ global _current_base_path
84
+ _current_base_path = base_path
85
+ if base_path:
86
+ logger.debug("Set include base path", base_path=str(base_path))
87
+
88
+
89
+ def get_include_base_path() -> Path:
90
+ """Get the current base path for !include resolution.
91
+
92
+ Returns
93
+ -------
94
+ Path
95
+ Current base path (defaults to cwd if not set)
96
+ """
97
+ return _current_base_path or Path.cwd()
98
+
99
+
100
+ def include_constructor(loader: yaml.SafeLoader, node: yaml.Node) -> Any:
101
+ """Include content from an external YAML file.
102
+
103
+ Supports two forms:
104
+ 1. Simple: !include ./path/to/file.yaml
105
+ 2. With vars: !include {path: ./file.yaml, vars: {key: value}}
106
+
107
+ Parameters
108
+ ----------
109
+ loader : yaml.SafeLoader
110
+ YAML loader instance
111
+ node : yaml.Node
112
+ YAML node (scalar for simple, mapping for vars)
113
+
114
+ Returns
115
+ -------
116
+ Any
117
+ Parsed content from the included file
118
+
119
+ Raises
120
+ ------
121
+ IncludeTagError
122
+ If the file cannot be found or parsed
123
+ """
124
+ base_path = get_include_base_path()
125
+
126
+ # Handle simple scalar form: !include ./path.yaml
127
+ if isinstance(node, yaml.ScalarNode):
128
+ include_path = loader.construct_scalar(node)
129
+ if not isinstance(include_path, str):
130
+ raise IncludeTagError("!include path must be a string")
131
+ vars_dict: dict[str, Any] = {}
132
+
133
+ # Handle mapping form: !include {path: ./path.yaml, vars: {...}}
134
+ elif isinstance(node, yaml.MappingNode):
135
+ # Use deep=True to fully construct nested mappings (like vars)
136
+ mapping = loader.construct_mapping(node, deep=True)
137
+ include_path = mapping.get("path")
138
+ if not include_path:
139
+ raise IncludeTagError("!include mapping requires 'path' key")
140
+ vars_dict = mapping.get("vars", {})
141
+
142
+ else:
143
+ raise IncludeTagError(f"!include expects scalar or mapping, got {type(node)}")
144
+
145
+ # Resolve path relative to base
146
+ resolved_path = _resolve_include_path(include_path, base_path)
147
+
148
+ logger.debug(
149
+ "Including file",
150
+ include_path=include_path,
151
+ resolved_path=str(resolved_path),
152
+ has_vars=bool(vars_dict),
153
+ )
154
+
155
+ # Read and parse the file
156
+ try:
157
+ content = resolved_path.read_text()
158
+ except FileNotFoundError as e:
159
+ raise IncludeTagError(
160
+ f"Include file not found: {resolved_path}\n"
161
+ f" (resolved from '{include_path}' relative to '{base_path}')"
162
+ ) from e
163
+ except Exception as e:
164
+ raise IncludeTagError(f"Failed to read include file {resolved_path}: {e}") from e
165
+
166
+ # Apply variable substitution if vars provided
167
+ if vars_dict:
168
+ content = _substitute_vars(content, vars_dict)
169
+
170
+ # Parse the included content
171
+ try:
172
+ # Set base path for nested includes
173
+ old_base = _current_base_path
174
+ set_include_base_path(resolved_path.parent)
175
+ try:
176
+ result = yaml.safe_load(content)
177
+ finally:
178
+ set_include_base_path(old_base)
179
+ except yaml.YAMLError as e:
180
+ raise IncludeTagError(f"Failed to parse include file {resolved_path}: {e}") from e
181
+
182
+ return result
183
+
184
+
185
+ def _resolve_include_path(include_path: str, base_path: Path) -> Path:
186
+ """Resolve include path relative to base path.
187
+
188
+ Parameters
189
+ ----------
190
+ include_path : str
191
+ Path from !include tag (may be relative or absolute)
192
+ base_path : Path
193
+ Base directory for relative paths
194
+
195
+ Returns
196
+ -------
197
+ Path
198
+ Resolved absolute path
199
+ """
200
+ path = Path(include_path)
201
+
202
+ # If absolute, use as-is
203
+ if path.is_absolute():
204
+ return path
205
+
206
+ # Resolve relative to base path
207
+ return (base_path / path).resolve()
208
+
209
+
210
+ def _substitute_vars(content: str, vars_dict: dict[str, Any]) -> str:
211
+ """Substitute variables in content using {{var}} syntax.
212
+
213
+ Parameters
214
+ ----------
215
+ content : str
216
+ YAML content with {{var}} placeholders
217
+ vars_dict : dict[str, Any]
218
+ Variable values to substitute
219
+
220
+ Returns
221
+ -------
222
+ str
223
+ Content with variables substituted
224
+ """
225
+ result = content
226
+ for key, value in vars_dict.items():
227
+ placeholder = "{{" + key + "}}"
228
+ # Convert value to YAML-safe string representation
229
+ if isinstance(value, str):
230
+ str_value = value
231
+ elif isinstance(value, bool):
232
+ str_value = "true" if value else "false"
233
+ elif value is None:
234
+ str_value = "null"
235
+ else:
236
+ str_value = str(value)
237
+ result = result.replace(placeholder, str_value)
238
+ return result
239
+
240
+
241
+ def register_include_tag() -> None:
242
+ """Register the !include custom tag with YAML SafeLoader.
243
+
244
+ This function should be called during module initialization to enable
245
+ !include tag support in YAML parsing.
246
+
247
+ Examples
248
+ --------
249
+ Import the module to auto-register::
250
+
251
+ import hexdag.core.pipeline_builder.include_tag # Registers !include tag
252
+
253
+ Or explicitly register::
254
+
255
+ from hexdag.core.pipeline_builder.include_tag import register_include_tag
256
+ register_include_tag()
257
+ """
258
+ # Check if already registered to avoid duplicate registration
259
+ if "!include" not in yaml.SafeLoader.yaml_constructors:
260
+ yaml.SafeLoader.add_constructor("!include", include_constructor)
261
+ logger.debug("Registered !include YAML tag")
262
+
263
+
264
+ # Auto-register when module is imported
265
+ register_include_tag()