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,294 @@
1
+ """Configurable interface and base class for plugins and adapters.
2
+
3
+ This module provides:
4
+ 1. ConfigurableComponent - Protocol for components with configuration
5
+ 2. ConfigurableAdapter - Base class that implements the protocol and eliminates boilerplate
6
+ 3. ConfigurableNode - Base class for node factories with configuration
7
+ 4. ConfigurablePolicy - Base class for policies with configuration
8
+ 5. ConfigurableMacro - Base class for macros with configuration
9
+ 6. SecretField - Helper for declaring secret configuration fields
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any, Protocol, runtime_checkable
15
+
16
+ from pydantic import BaseModel, ConfigDict, Field # noqa: TC002
17
+
18
+ from hexdag.core.logging import get_logger
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ def _extract_config_fields(kwargs: dict[str, Any], config_class: type[BaseModel]) -> dict[str, Any]:
24
+ """Extract config fields from kwargs that match the config class schema.
25
+
26
+ Parameters
27
+ ----------
28
+ kwargs : dict[str, Any]
29
+ Keyword arguments containing config values
30
+ config_class : type[BaseModel]
31
+ Pydantic model class defining the config schema
32
+
33
+ Returns
34
+ -------
35
+ dict[str, Any]
36
+ Dictionary containing only the fields defined in config_class
37
+ """
38
+ return {
39
+ field_name: kwargs[field_name]
40
+ for field_name in config_class.model_fields
41
+ if field_name in kwargs
42
+ }
43
+
44
+
45
+ # Legacy Config base classes removed - use simplified decorator pattern instead
46
+ # See CLAUDE.md and SIMPLIFIED_PATTERN.md for migration guide
47
+
48
+
49
+ class MacroConfig(BaseModel):
50
+ """Base configuration class for all macros.
51
+
52
+ Macros should define a nested Config class inheriting from this.
53
+ This enables:
54
+ - Type-safe configuration
55
+ - YAML schema generation
56
+ - Runtime validation
57
+ - Expansion strategy configuration
58
+
59
+ Examples
60
+ --------
61
+ Define a macro configuration class::
62
+
63
+ class ResearchMacroConfig(MacroConfig):
64
+ depth: int = 3
65
+ enable_synthesis: bool = True
66
+
67
+ config = ResearchMacroConfig(depth=5)
68
+ assert config.depth == 5
69
+ """
70
+
71
+ model_config = ConfigDict(frozen=True)
72
+
73
+
74
+ def SecretField(
75
+ env_var: str,
76
+ memory_key: str | None = None,
77
+ default: Any = None,
78
+ description: str | None = None,
79
+ **field_kwargs: Any,
80
+ ) -> Any:
81
+ """Create a secret field with auto-resolution from Memory or environment.
82
+
83
+ This helper marks a Pydantic field as a secret, which enables:
84
+ 1. Auto-hiding in logs/repr (uses Pydantic SecretStr)
85
+ 2. Auto-resolution from Memory (secret:KEY) via orchestrator
86
+ 3. Fallback to environment variable if not in Memory
87
+ 4. Clear documentation of secret requirements
88
+
89
+ Parameters
90
+ ----------
91
+ env_var : str
92
+ Environment variable name to read from (e.g., "OPENAI_API_KEY")
93
+ memory_key : str | None, optional
94
+ Key to use in Memory port (defaults to env_var).
95
+ Will be prefixed with "secret:" automatically.
96
+ default : Any, optional
97
+ Default value if not found (default: None)
98
+ description : str | None, optional
99
+ Field description for schema documentation
100
+ **field_kwargs : Any
101
+ Additional Pydantic Field() parameters
102
+
103
+ Returns
104
+ -------
105
+ Any
106
+ Pydantic Field with secret metadata (typed as Any for use in assignments)
107
+
108
+ Examples
109
+ --------
110
+ >>> from pydantic import BaseModel
111
+ >>> class MyAdapterConfig(BaseModel):
112
+ ... api_key: SecretStr | None = SecretField(
113
+ ... env_var="OPENAI_API_KEY",
114
+ ... description="OpenAI API key"
115
+ ... )
116
+ ... timeout: float = 30.0
117
+ """
118
+ return Field(
119
+ default=default,
120
+ description=description,
121
+ json_schema_extra={
122
+ "secret": True,
123
+ "env_var": env_var,
124
+ "memory_key": memory_key or env_var,
125
+ },
126
+ **field_kwargs,
127
+ )
128
+
129
+
130
+ @runtime_checkable
131
+ class ConfigurableComponent(Protocol):
132
+ """Protocol for components that support configuration.
133
+
134
+ Any adapter or plugin that implements this protocol will be
135
+ automatically discoverable by the CLI config generation system.
136
+ """
137
+
138
+ @classmethod
139
+ def get_config_class(cls) -> type[BaseModel]:
140
+ """Return the Pydantic model class that defines configuration schema.
141
+
142
+ Returns
143
+ -------
144
+ type[BaseModel]
145
+ A Pydantic model class with field definitions, defaults, and validation
146
+ """
147
+ ...
148
+
149
+
150
+ class ConfigurableMacro:
151
+ """Base class for macros with configuration support.
152
+
153
+ Macros are pipeline templates that expand into DirectedGraph subgraphs.
154
+ They are first-class registry components like nodes, adapters, and policies.
155
+
156
+ Key Concepts
157
+ ------------
158
+ - Macros live at a higher abstraction level than nodes
159
+ - Nodes are atomic operations (single LLM call, single function)
160
+ - Macros are compositions (multi-step workflows)
161
+ - Macros expand to graphs of nodes at build time or runtime
162
+
163
+ Architecture
164
+ ------------
165
+ ConfigurableMacro provides:
166
+ 1. Type-safe configuration via MacroConfig subclasses
167
+ 2. Consistent expansion interface via expand() method
168
+ 3. Registry integration via @macro decorator
169
+ 4. Support for both static and dynamic expansion strategies
170
+
171
+ Subclasses must:
172
+ - Define a nested Config class inheriting from MacroConfig
173
+ - Implement expand() method that returns DirectedGraph
174
+
175
+ Examples
176
+ --------
177
+ >>> from hexdag.core.configurable import ConfigurableMacro, MacroConfig
178
+ >>> from hexdag.core.domain.dag import DirectedGraph
179
+ >>>
180
+ >>> class ResearchMacroConfig(MacroConfig):
181
+ ... depth: int = 3
182
+ ... enable_synthesis: bool = True
183
+ >>>
184
+ >>> class ResearchMacro(ConfigurableMacro):
185
+ ... Config = ResearchMacroConfig
186
+ ...
187
+ ... def expand(self, instance_name, inputs, dependencies):
188
+ ... # Build and return DirectedGraph
189
+ ... return DirectedGraph([...])
190
+ """
191
+
192
+ Config: type[MacroConfig]
193
+
194
+ def __init__(self, **kwargs: Any) -> None:
195
+ """Initialize macro with configuration.
196
+
197
+ Parameters
198
+ ----------
199
+ **kwargs : Any
200
+ Configuration options matching Config schema fields
201
+ """
202
+ if not hasattr(self.__class__, "Config"):
203
+ raise AttributeError(
204
+ f"{self.__class__.__name__} must define a nested Config class (MacroConfig)"
205
+ )
206
+
207
+ config_data = _extract_config_fields(kwargs, self.Config)
208
+
209
+ self.config = self.Config(**config_data)
210
+ self._extra_kwargs = {k: v for k, v in kwargs.items() if k not in self.Config.model_fields}
211
+
212
+ def expand(
213
+ self,
214
+ instance_name: str,
215
+ inputs: dict[str, Any],
216
+ dependencies: list[str],
217
+ ) -> Any: # Returns DirectedGraph but avoid circular import
218
+ """Expand macro into a concrete subgraph.
219
+
220
+ This is the core method that subclasses must implement.
221
+ It transforms the macro template into an actual DirectedGraph
222
+ with concrete nodes.
223
+
224
+ Parameters
225
+ ----------
226
+ instance_name : str
227
+ Unique name for this macro instance (used as prefix for generated nodes).
228
+ Example: "deep_research" → generates "deep_research_step_1", etc.
229
+ inputs : dict[str, Any]
230
+ Input values to bind to macro parameters.
231
+ Example: {"topic": "AI safety", "depth": 5}
232
+ dependencies : list[str]
233
+ External node names that this macro instance depends on.
234
+ The macro's entry nodes will be connected to these.
235
+ Example: ["query_parser", "validator"]
236
+
237
+ Returns
238
+ -------
239
+ DirectedGraph
240
+ Subgraph containing the expanded nodes with proper dependencies
241
+
242
+ Raises
243
+ ------
244
+ NotImplementedError
245
+ If subclass doesn't implement this method
246
+ ValueError
247
+ If inputs don't match macro parameter requirements
248
+ """
249
+ raise NotImplementedError(f"{self.__class__.__name__} must implement expand() method")
250
+
251
+ def validate_inputs(
252
+ self, inputs: dict[str, Any], required: list[str], optional: dict[str, Any]
253
+ ) -> dict[str, Any]:
254
+ """Validate and normalize macro inputs.
255
+
256
+ Helper method for subclasses to validate inputs against requirements.
257
+
258
+ Parameters
259
+ ----------
260
+ inputs : dict[str, Any]
261
+ Provided input values
262
+ required : list[str]
263
+ Names of required input parameters
264
+ optional : dict[str, Any]
265
+ Optional parameters with their default values
266
+
267
+ Returns
268
+ -------
269
+ dict[str, Any]
270
+ Validated and normalized inputs (with defaults applied)
271
+
272
+ Raises
273
+ ------
274
+ ValueError
275
+ If required inputs are missing
276
+ """
277
+ # Check required inputs
278
+ if missing := [name for name in required if name not in inputs]:
279
+ raise ValueError(
280
+ f"Missing required inputs for {self.__class__.__name__}: {', '.join(missing)}"
281
+ )
282
+
283
+ # Apply defaults for optional inputs
284
+ return {**optional, **inputs}
285
+
286
+ @classmethod
287
+ def get_config_class(cls) -> type[BaseModel]:
288
+ """Get the configuration model class."""
289
+ return cls.Config
290
+
291
+ def __repr__(self) -> str:
292
+ """Readable representation for debugging."""
293
+ config_dict = self.config.model_dump() if hasattr(self.config, "model_dump") else {}
294
+ return f"{self.__class__.__name__}(config={config_dict})"
@@ -0,0 +1,37 @@
1
+ """Execution context for async-safe cross-cutting concerns."""
2
+
3
+ from hexdag.core.context.execution_context import (
4
+ ExecutionContext,
5
+ clear_execution_context,
6
+ get_current_graph,
7
+ get_current_node_name,
8
+ get_node_results,
9
+ get_observer_manager,
10
+ get_port,
11
+ get_ports,
12
+ get_run_id,
13
+ set_current_graph,
14
+ set_current_node_name,
15
+ set_node_results,
16
+ set_observer_manager,
17
+ set_ports,
18
+ set_run_id,
19
+ )
20
+
21
+ __all__ = [
22
+ "ExecutionContext",
23
+ "clear_execution_context",
24
+ "get_current_graph",
25
+ "get_current_node_name",
26
+ "get_node_results",
27
+ "get_observer_manager",
28
+ "get_port",
29
+ "get_ports",
30
+ "get_run_id",
31
+ "set_current_graph",
32
+ "set_current_node_name",
33
+ "set_node_results",
34
+ "set_observer_manager",
35
+ "set_ports",
36
+ "set_run_id",
37
+ ]
@@ -0,0 +1,378 @@
1
+ """Execution context for orchestrator components.
2
+
3
+ This module provides async-safe context management for cross-cutting concerns
4
+ like observer management. This eliminates parameter drilling and provides a
5
+ clean way to access these services throughout the execution call stack.
6
+
7
+ The context is automatically propagated through async call chains, making
8
+ observer_manager and other orchestration services available to all components
9
+ without explicit parameter passing.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from contextvars import ContextVar
15
+ from types import MappingProxyType
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ if TYPE_CHECKING:
19
+ from hexdag.core.ports.observer_manager import ObserverManagerPort
20
+
21
+ # Context variables for orchestrator components (async-safe)
22
+ _observer_manager_context: ContextVar[ObserverManagerPort | None] = ContextVar(
23
+ "observer_manager", default=None
24
+ )
25
+
26
+ _run_id_context: ContextVar[str | None] = ContextVar("run_id", default=None)
27
+
28
+ # Ports stored as immutable MappingProxyType to prevent race conditions in concurrent execution
29
+ _ports_context: ContextVar[MappingProxyType[str, Any] | None] = ContextVar("ports", default=None)
30
+
31
+ # Dynamic graph context - for runtime expansion support
32
+ _current_graph_context: ContextVar[Any | None] = ContextVar(
33
+ "current_graph", default=None
34
+ ) # Any to avoid circular import
35
+
36
+ # Node results context - for accessing intermediate results during execution
37
+ _node_results_context: ContextVar[dict[str, Any] | None] = ContextVar("node_results", default=None)
38
+
39
+ # Current node name context - for event emission with proper node attribution
40
+ _current_node_name_context: ContextVar[str | None] = ContextVar("current_node_name", default=None)
41
+
42
+
43
+ # ============================================================================
44
+ # Observer Manager Context
45
+ # ============================================================================
46
+
47
+
48
+ def set_observer_manager(manager: ObserverManagerPort | None) -> None:
49
+ """Set observer manager for current async execution context.
50
+
51
+ This should be called by the orchestrator at the start of DAG execution.
52
+ All components within this context will automatically have access to
53
+ the observer manager for event emission.
54
+
55
+ Parameters
56
+ ----------
57
+ manager : ObserverManagerPort | None
58
+ Observer manager instance, or None to clear context
59
+
60
+ Examples
61
+ --------
62
+ Example usage::
63
+
64
+ # In orchestrator
65
+ observer_manager = ports.get("observer_manager")
66
+ set_observer_manager(observer_manager)
67
+ # Now all components can emit events
68
+ """
69
+ _observer_manager_context.set(manager)
70
+
71
+
72
+ def get_observer_manager() -> ObserverManagerPort | None:
73
+ """Get observer manager from current async execution context.
74
+
75
+ This is called by components to access the observer manager for event
76
+ emission. Returns None if not set or not in orchestrator context.
77
+
78
+ Returns
79
+ -------
80
+ ObserverManagerPort | None
81
+ Current observer manager, or None if not in orchestrator context
82
+
83
+ Examples
84
+ --------
85
+ Example usage::
86
+
87
+ # In any component
88
+ if (observer_manager := get_observer_manager()):
89
+ await observer_manager.notify(NodeStarted(...))
90
+ """
91
+ return _observer_manager_context.get()
92
+
93
+
94
+ # ============================================================================
95
+ # Run ID Context
96
+ # ============================================================================
97
+
98
+
99
+ def set_run_id(run_id: str | None) -> None:
100
+ """Set run ID for current async execution context.
101
+
102
+ Parameters
103
+ ----------
104
+ run_id : str | None
105
+ Unique run identifier for this execution
106
+ """
107
+ _run_id_context.set(run_id)
108
+
109
+
110
+ def get_run_id() -> str | None:
111
+ """Get run ID from current async execution context.
112
+
113
+ Returns
114
+ -------
115
+ str | None
116
+ Current run ID, or None if not in orchestrator context
117
+ """
118
+ return _run_id_context.get()
119
+
120
+
121
+ # ============================================================================
122
+ # Ports Context
123
+ # ============================================================================
124
+
125
+
126
+ def set_ports(ports: dict[str, Any] | None) -> None:
127
+ """Set ports dict for current async execution context.
128
+
129
+ This allows adapters and components to access ports without explicit passing.
130
+
131
+ IMPORTANT: Ports are stored as immutable MappingProxyType to prevent race
132
+ conditions when multiple nodes execute concurrently.
133
+
134
+ Parameters
135
+ ----------
136
+ ports : dict[str, Any] | None
137
+ Ports dictionary containing all available adapters and services
138
+ """
139
+ # Wrap in MappingProxyType to make immutable and prevent concurrent modification
140
+ _ports_context.set(MappingProxyType(ports) if ports else None)
141
+
142
+
143
+ def get_ports() -> MappingProxyType[str, Any] | None:
144
+ """Get immutable ports mapping from current async execution context.
145
+
146
+ Returns
147
+ -------
148
+ MappingProxyType[str, Any] | None
149
+ Immutable view of ports dictionary, or None if not in orchestrator context
150
+ """
151
+ return _ports_context.get()
152
+
153
+
154
+ def get_port(port_name: str) -> Any:
155
+ """Get a specific port from current async execution context.
156
+
157
+ Parameters
158
+ ----------
159
+ port_name : str
160
+ Name of the port to retrieve (e.g., "llm", "database", "memory")
161
+
162
+ Returns
163
+ -------
164
+ Any
165
+ The port adapter, or None if not found or not in orchestrator context
166
+
167
+ Examples
168
+ --------
169
+ Example usage::
170
+
171
+ # In any component
172
+ if (llm := get_port("llm")):
173
+ response = await llm.aresponse(messages)
174
+ """
175
+ ports = _ports_context.get()
176
+ if ports is None:
177
+ return None
178
+ return ports.get(port_name)
179
+
180
+
181
+ # ============================================================================
182
+ # Current Node Name Context
183
+ # ============================================================================
184
+
185
+
186
+ def set_current_node_name(node_name: str | None) -> None:
187
+ """Set current node name for event attribution.
188
+
189
+ Parameters
190
+ ----------
191
+ node_name : str | None
192
+ Name of the currently executing node, or None to clear
193
+ """
194
+ _current_node_name_context.set(node_name)
195
+
196
+
197
+ def get_current_node_name() -> str | None:
198
+ """Get current node name from execution context.
199
+
200
+ Returns
201
+ -------
202
+ str | None
203
+ Name of currently executing node, or None if not set
204
+ """
205
+ return _current_node_name_context.get()
206
+
207
+
208
+ # ============================================================================
209
+ # Batch Context Management
210
+ # ============================================================================
211
+
212
+
213
+ class ExecutionContext:
214
+ """Context manager for setting up orchestrator execution context.
215
+
216
+ This provides a clean way to set all execution context variables at once
217
+ using a context manager pattern.
218
+
219
+ Important
220
+ ---------
221
+ The context is automatically cleaned up on exit via __aexit__. This means:
222
+
223
+ 1. **All async operations (observers, hooks) MUST complete before context exit**
224
+ 2. Observer notifications should always use `await` before exiting context
225
+ 3. Post-DAG hooks must execute INSIDE the context, not after
226
+ 4. Do not create fire-and-forget tasks that outlive the context
227
+
228
+ The orchestrator correctly handles this by awaiting all notifications and
229
+ executing post-hooks inside the context block before cleanup.
230
+
231
+ Examples
232
+ --------
233
+ Example usage::
234
+
235
+ async with ExecutionContext(
236
+ observer_manager=observer,
237
+ run_id="run-123",
238
+ ports=all_ports
239
+ ):
240
+ # All components can access context
241
+ result = await execute_dag(dag, inputs)
242
+ # IMPORTANT: All observer notifications and hooks complete here
243
+ # Context cleaned up here - observers no longer accessible
244
+ """
245
+
246
+ def __init__(
247
+ self,
248
+ observer_manager: ObserverManagerPort | None = None,
249
+ run_id: str | None = None,
250
+ ports: dict[str, Any] | None = None,
251
+ ):
252
+ """Initialize execution context.
253
+
254
+ Parameters
255
+ ----------
256
+ observer_manager : ObserverManagerPort | None
257
+ Observer manager for event emission
258
+ run_id : str | None
259
+ Unique run identifier
260
+ ports : dict[str, Any] | None
261
+ Ports dictionary with all adapters
262
+ """
263
+ self.observer_manager = observer_manager
264
+ self.run_id = run_id
265
+ self.ports = ports
266
+
267
+ def __enter__(self) -> ExecutionContext:
268
+ """Set up execution context (sync context manager)."""
269
+ set_observer_manager(self.observer_manager)
270
+ set_run_id(self.run_id)
271
+ set_ports(self.ports)
272
+ return self
273
+
274
+ def __exit__(self, _exc_type: Any, _exc_val: Any, _exc_tb: Any) -> None:
275
+ """Clean up execution context (sync context manager)."""
276
+ clear_execution_context()
277
+
278
+ async def __aenter__(self) -> ExecutionContext:
279
+ """Set up execution context (async context manager)."""
280
+ set_observer_manager(self.observer_manager)
281
+ set_run_id(self.run_id)
282
+ set_ports(self.ports)
283
+ return self
284
+
285
+ async def __aexit__(self, _exc_type: Any, _exc_val: Any, _exc_tb: Any) -> None:
286
+ """Clean up execution context (async context manager)."""
287
+ clear_execution_context()
288
+
289
+
290
+ # ============================================================================
291
+ # Cleanup
292
+ # ============================================================================
293
+
294
+
295
+ def clear_execution_context() -> None:
296
+ """Clear all execution context variables.
297
+
298
+ Useful for cleanup after orchestrator execution or in tests.
299
+ """
300
+ _observer_manager_context.set(None)
301
+ _run_id_context.set(None)
302
+ _ports_context.set(None)
303
+ _current_graph_context.set(None)
304
+ _node_results_context.set(None)
305
+
306
+
307
+ # ============================================================================
308
+ # Dynamic Graph Context (for runtime expansion)
309
+ # ============================================================================
310
+
311
+
312
+ def set_current_graph(graph: Any) -> None:
313
+ """Set current graph for dynamic expansion.
314
+
315
+ This allows expander nodes to access and modify the graph during execution.
316
+ Only used when executing DynamicDirectedGraph.
317
+
318
+ Parameters
319
+ ----------
320
+ graph : Any
321
+ The current graph being executed (typically DynamicDirectedGraph)
322
+ """
323
+ _current_graph_context.set(graph)
324
+
325
+
326
+ def get_current_graph() -> Any | None:
327
+ """Get current graph from execution context.
328
+
329
+ Used by expander nodes to inject new nodes during runtime.
330
+
331
+ Returns
332
+ -------
333
+ Any | None
334
+ Current graph, or None if not in dynamic execution context
335
+
336
+ Examples
337
+ --------
338
+ >>> # In an expander node
339
+ >>> graph = get_current_graph()
340
+ >>> if graph and hasattr(graph, 'add'):
341
+ ... new_node = create_next_step()
342
+ ... graph.add(new_node)
343
+ """
344
+ return _current_graph_context.get()
345
+
346
+
347
+ def set_node_results(results: dict[str, Any]) -> None:
348
+ """Set accumulated node results for dynamic expansion.
349
+
350
+ This allows expander nodes to inspect previous results when deciding
351
+ whether to expand the graph.
352
+
353
+ Parameters
354
+ ----------
355
+ results : dict[str, Any]
356
+ Dictionary mapping node names to their execution results
357
+ """
358
+ _node_results_context.set(results)
359
+
360
+
361
+ def get_node_results() -> dict[str, Any] | None:
362
+ """Get accumulated node results from execution context.
363
+
364
+ Returns
365
+ -------
366
+ dict[str, Any] | None
367
+ Node results dictionary, or None if not in execution context
368
+
369
+ Examples
370
+ --------
371
+ >>> # In an expander node
372
+ >>> results = get_node_results()
373
+ >>> if results:
374
+ ... previous_step = results.get("step_1")
375
+ ... if should_continue(previous_step):
376
+ ... inject_next_step()
377
+ """
378
+ return _node_results_context.get()