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,275 @@
1
+ """Orchestrator Factory - Creates orchestrator instances from pipeline configuration.
2
+
3
+ This factory bridges the gap between declarative YAML configuration and the
4
+ runtime orchestrator, instantiating adapters and policies from their specs.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from hexdag.core.logging import get_logger
10
+ from hexdag.core.orchestration.orchestrator import Orchestrator
11
+ from hexdag.core.pipeline_builder.component_instantiator import ComponentInstantiator
12
+ from hexdag.core.pipeline_builder.pipeline_config import PipelineConfig
13
+ from hexdag.core.ports_builder import PortsBuilder
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class OrchestratorFactory:
19
+ """Factory for creating orchestrator instances from pipeline configuration.
20
+
21
+ This factory handles the complex task of:
22
+ 1. Instantiating adapter instances from port specs
23
+ 2. Instantiating policy instances from policy specs
24
+ 3. Wiring everything together into a configured orchestrator
25
+
26
+ Examples
27
+ --------
28
+ ```python
29
+ from hexdag.core.pipeline_builder.yaml_builder import YamlPipelineBuilder
30
+ from hexdag.core.orchestration.orchestrator_factory import OrchestratorFactory
31
+
32
+ # Parse YAML pipeline
33
+ builder = YamlPipelineBuilder()
34
+ graph, pipeline_config = builder.build_from_yaml_file("pipeline.yaml")
35
+
36
+ factory = OrchestratorFactory()
37
+ orchestrator = factory.create_orchestrator(pipeline_config)
38
+
39
+ # Execute the pipeline
40
+ result = await orchestrator.aexecute_dag(graph, initial_input={"query": "..."})
41
+ ```
42
+ """
43
+
44
+ def __init__(self) -> None:
45
+ """Initialize the orchestrator factory."""
46
+ self.component_instantiator = ComponentInstantiator()
47
+
48
+ def create_orchestrator(
49
+ self,
50
+ pipeline_config: PipelineConfig,
51
+ max_concurrent_nodes: int = 10,
52
+ strict_validation: bool = False,
53
+ default_node_timeout: float | None = None,
54
+ additional_ports: dict[str, Any] | None = None,
55
+ ) -> Orchestrator:
56
+ """Create an orchestrator instance from pipeline configuration.
57
+
58
+ Parameters
59
+ ----------
60
+ pipeline_config : PipelineConfig
61
+ Pipeline configuration with ports, policies, and metadata
62
+ max_concurrent_nodes : int, optional
63
+ Maximum number of nodes to execute concurrently, by default 10
64
+ strict_validation : bool, optional
65
+ If True, raise errors on validation failure, by default False
66
+ default_node_timeout : float | None, optional
67
+ Default timeout in seconds for each node, by default None
68
+ additional_ports : dict[str, Any] | None, optional
69
+ Additional ports to merge with configured ports, by default None
70
+
71
+ Returns
72
+ -------
73
+ Orchestrator
74
+ Configured orchestrator ready to execute pipelines
75
+
76
+ Examples
77
+ --------
78
+ Basic usage with global ports::
79
+
80
+ factory = OrchestratorFactory()
81
+
82
+ orchestrator = factory.create_orchestrator(
83
+ pipeline_config=config,
84
+ max_concurrent_nodes=5,
85
+ strict_validation=True,
86
+ default_node_timeout=60.0,
87
+ )
88
+
89
+ Notes
90
+ -----
91
+ **Known Limitations (Phase 4)**:
92
+
93
+ - **Type-specific ports** (``type_ports`` in YAML): Parsed and validated
94
+ but not yet used during execution. All nodes currently receive global ports.
95
+
96
+ - **Node-level port overrides**: Not yet implemented. Requires orchestrator
97
+ changes to resolve ports per-node at execution time.
98
+
99
+ **Workaround for Advanced Port Configuration**:
100
+
101
+ For type-specific or node-level port customization, use PortsBuilder
102
+ programmatically::
103
+
104
+ from hexdag.core.ports_builder import PortsBuilder
105
+
106
+ builder = (
107
+ PortsBuilder()
108
+ .with_llm(MockLLM()) # Global default
109
+ .for_type("agent", llm=OpenAIAdapter(model="gpt-4"))
110
+ .for_node("researcher", llm=AnthropicAdapter(model="claude-3"))
111
+ )
112
+
113
+ # Pass as additional_ports
114
+ orchestrator = factory.create_orchestrator(
115
+ pipeline_config,
116
+ additional_ports=builder.build()
117
+ )
118
+
119
+ These features are planned for future implementation when orchestrator
120
+ supports per-node port resolution.
121
+ """
122
+ logger.info(
123
+ "Creating orchestrator from pipeline config: {} ports, {} type_ports, {} policies",
124
+ len(pipeline_config.ports),
125
+ len(pipeline_config.type_ports),
126
+ len(pipeline_config.policies),
127
+ )
128
+
129
+ # Step 1: Build PortsConfiguration if type_ports or node_ports are configured
130
+ use_ports_config = bool(pipeline_config.type_ports)
131
+
132
+ if use_ports_config:
133
+ ports_config = self._build_ports_configuration(pipeline_config, additional_ports)
134
+ # Note: PortsConfiguration converts dicts to tuples for immutability
135
+ global_ports = {k: v.port for k, v in (ports_config.global_ports or ())}
136
+ else:
137
+ # Simple case: just global ports
138
+ ports_config = None
139
+ global_ports = self._instantiate_ports(pipeline_config.ports)
140
+ if additional_ports:
141
+ global_ports.update(additional_ports)
142
+
143
+ # Step 2: Create orchestrator with configured ports
144
+ orchestrator = Orchestrator(
145
+ max_concurrent_nodes=max_concurrent_nodes,
146
+ ports=ports_config if ports_config else global_ports,
147
+ strict_validation=strict_validation,
148
+ default_node_timeout=default_node_timeout,
149
+ )
150
+
151
+ logger.info(
152
+ "✅ Orchestrator created with {} ports",
153
+ len(global_ports),
154
+ )
155
+
156
+ return orchestrator
157
+
158
+ def _instantiate_ports(self, port_specs: dict[str, dict[str, Any]]) -> dict[str, Any]:
159
+ """Instantiate adapter instances from port specifications.
160
+
161
+ Parameters
162
+ ----------
163
+ port_specs : dict[str, dict[str, Any]]
164
+ Map of port_name -> adapter dict spec
165
+
166
+ Returns
167
+ -------
168
+ dict[str, Any]
169
+ Map of port_name -> adapter_instance
170
+ """
171
+ ports = {}
172
+
173
+ for port_name, port_spec in port_specs.items():
174
+ try:
175
+ logger.debug("Instantiating port: {} = {}", port_name, port_spec)
176
+ adapter = self.component_instantiator.instantiate_adapter(port_spec, port_name)
177
+ ports[port_name] = adapter
178
+ logger.debug("✅ Port instantiated: {}", port_name)
179
+ except Exception as e:
180
+ logger.error(
181
+ "Failed to instantiate port '{}' from spec '{}': {}",
182
+ port_name,
183
+ port_spec,
184
+ e,
185
+ )
186
+ raise
187
+
188
+ return ports
189
+
190
+ def _build_ports_configuration(
191
+ self, pipeline_config: PipelineConfig, additional_ports: dict[str, Any] | None
192
+ ) -> Any:
193
+ """Build a PortsConfiguration from pipeline config.
194
+
195
+ Parameters
196
+ ----------
197
+ pipeline_config : PipelineConfig
198
+ Pipeline configuration with ports, type_ports
199
+ additional_ports : dict[str, Any] | None
200
+ Additional ports to merge
201
+
202
+ Returns
203
+ -------
204
+ PortsConfiguration
205
+ Hierarchical ports configuration with type-specific support
206
+ """
207
+ from hexdag.core.orchestration.models import PortConfig, PortsConfiguration
208
+
209
+ # Step 1: Instantiate global ports
210
+ global_ports_dict = self._instantiate_ports(pipeline_config.ports)
211
+ if additional_ports:
212
+ global_ports_dict.update(additional_ports)
213
+
214
+ # Wrap in PortConfig
215
+ global_ports = {k: PortConfig(port=v) for k, v in global_ports_dict.items()}
216
+
217
+ # Step 2: Instantiate type-specific ports
218
+ type_ports = None
219
+ if pipeline_config.type_ports:
220
+ type_ports = {}
221
+ for node_type, type_port_specs in pipeline_config.type_ports.items():
222
+ type_port_instances = self._instantiate_ports(type_port_specs)
223
+ type_ports[node_type] = {
224
+ k: PortConfig(port=v) for k, v in type_port_instances.items()
225
+ }
226
+ logger.debug(
227
+ "Configured {} ports for node type '{}'",
228
+ len(type_port_instances),
229
+ node_type,
230
+ )
231
+
232
+ # Step 3: TODO: Add node-level port support when available in YAML builder
233
+
234
+ return PortsConfiguration(
235
+ global_ports=global_ports,
236
+ type_ports=type_ports,
237
+ node_ports=None, # Not yet implemented
238
+ )
239
+
240
+ def create_ports_builder(self, pipeline_config: PipelineConfig) -> PortsBuilder:
241
+ """Create a PortsBuilder from pipeline configuration (convenience method).
242
+
243
+ This method provides an alternative workflow using PortsBuilder for
244
+ advanced use cases that need type-specific or node-specific port overrides.
245
+
246
+ Parameters
247
+ ----------
248
+ pipeline_config : PipelineConfig
249
+ Pipeline configuration with ports
250
+
251
+ Returns
252
+ -------
253
+ PortsBuilder
254
+ Builder with ports configured from pipeline config
255
+
256
+ Examples
257
+ --------
258
+ ```python
259
+ factory = OrchestratorFactory()
260
+
261
+ builder = factory.create_ports_builder(pipeline_config)
262
+
263
+ # Optionally add node-specific overrides
264
+ builder.for_node("researcher", llm=custom_llm)
265
+
266
+ ports = builder.build()
267
+ ```
268
+ """
269
+ ports = self._instantiate_ports(pipeline_config.ports)
270
+
271
+ builder = PortsBuilder()
272
+ for port_name, port_instance in ports.items():
273
+ builder.with_custom(port_name, port_instance)
274
+
275
+ return builder
@@ -0,0 +1,327 @@
1
+ """Port wrappers that provide observability and control.
2
+
3
+ These wrappers intercept port method calls to:
4
+ 1. **Observability** - Emit events (LLMPromptSent, LLMResponseReceived, etc.)
5
+ 2. **Control** - Enable policy-based control (rate limiting, caching, retry, fallback)
6
+ 3. **Metrics** - Track duration and performance
7
+ 4. **Extensibility** - Provide hooks for custom behavior
8
+
9
+ This allows centralized control over infrastructure interactions without
10
+ polluting business logic in nodes and macros.
11
+ """
12
+
13
+ import time
14
+ from typing import Any
15
+
16
+ from hexdag.core.context import get_current_node_name, get_observer_manager
17
+ from hexdag.core.logging import get_logger
18
+ from hexdag.core.orchestration.events import (
19
+ LLMPromptSent,
20
+ LLMResponseReceived,
21
+ ToolCalled,
22
+ ToolCompleted,
23
+ )
24
+ from hexdag.core.ports.llm import LLM, LLMResponse, MessageList, SupportsGeneration
25
+
26
+ logger = get_logger(__name__)
27
+
28
+
29
+ class ObservableLLMWrapper:
30
+ """Wraps an LLM port for observability and policy-based control.
31
+
32
+ This wrapper provides:
33
+ - Automatic event emission for all LLM calls
34
+ - Policy evaluation before/after calls (future: rate limiting, caching)
35
+ - Duration tracking and performance metrics
36
+ - Transparent forwarding to underlying LLM
37
+ """
38
+
39
+ def __init__(self, llm: LLM):
40
+ """Initialize wrapper with underlying LLM port.
41
+
42
+ Parameters
43
+ ----------
44
+ llm : LLM
45
+ The underlying LLM port to wrap
46
+ """
47
+ self._llm = llm
48
+
49
+ async def aresponse(self, messages: MessageList, **kwargs: Any) -> str | None:
50
+ """Call LLM with observability and policy control.
51
+
52
+ This method:
53
+ 1. Emits LLMPromptSent event (for observability)
54
+ 2. Calls underlying LLM
55
+ 3. Emits LLMResponseReceived event with duration
56
+ 4. (Future: Policy evaluation for rate limiting, caching, retry)
57
+
58
+ Parameters
59
+ ----------
60
+ messages : MessageList
61
+ Messages to send to LLM
62
+ **kwargs : Any
63
+ Additional LLM parameters
64
+
65
+ Returns
66
+ -------
67
+ str | None
68
+ LLM response text
69
+ """
70
+ node_name = get_current_node_name() or "unknown"
71
+
72
+ # Emit prompt sent event (OBSERVABILITY)
73
+ if observer_mgr := get_observer_manager():
74
+ await observer_mgr.notify(
75
+ LLMPromptSent(
76
+ node_name=node_name,
77
+ messages=[{"role": m.role, "content": m.content} for m in messages],
78
+ )
79
+ )
80
+
81
+ # TODO(hexdag-team): Add policy evaluation here (CONTROL) #noqa: TD003
82
+ # if policy_mgr := get_policy_manager():
83
+ # policy_response = await policy_mgr.evaluate(LLMCallPolicy(...))
84
+ # if policy_response.signal == PolicySignal.SKIP:
85
+ # return policy_response.data # Cached response
86
+ # elif policy_response.signal == PolicySignal.FAIL:
87
+ # raise RateLimitError("LLM rate limit exceeded")
88
+
89
+ # Call underlying LLM
90
+ start_time = time.perf_counter()
91
+
92
+ # Check if the adapter supports generation
93
+ if isinstance(self._llm, SupportsGeneration):
94
+ response = await self._llm.aresponse(messages, **kwargs)
95
+ else:
96
+ raise NotImplementedError(
97
+ f"LLM adapter {type(self._llm).__name__} does not support text generation. "
98
+ "It must implement SupportsGeneration protocol."
99
+ )
100
+
101
+ duration_ms = (time.perf_counter() - start_time) * 1000
102
+
103
+ # Emit response received event (OBSERVABILITY)
104
+ if observer_mgr := get_observer_manager():
105
+ await observer_mgr.notify(
106
+ LLMResponseReceived(
107
+ node_name=node_name, response=response or "", duration_ms=duration_ms
108
+ )
109
+ )
110
+
111
+ return response
112
+
113
+ async def aresponse_with_tools(
114
+ self,
115
+ messages: MessageList,
116
+ tools: list[dict[str, Any]],
117
+ tool_choice: str | dict[str, Any] = "auto",
118
+ **kwargs: Any,
119
+ ) -> LLMResponse:
120
+ """Call LLM with tools and emit events.
121
+
122
+ Parameters
123
+ ----------
124
+ messages : MessageList
125
+ Messages to send to LLM
126
+ tools : list[dict[str, Any]]
127
+ Tool definitions
128
+ tool_choice : str | dict[str, Any]
129
+ Tool choice strategy
130
+ **kwargs : Any
131
+ Additional LLM parameters
132
+
133
+ Returns
134
+ -------
135
+ LLMResponse
136
+ Structured response with tool calls
137
+ """
138
+ node_name = get_current_node_name() or "unknown"
139
+
140
+ # Emit prompt sent event
141
+ if observer_mgr := get_observer_manager():
142
+ await observer_mgr.notify(
143
+ LLMPromptSent(
144
+ node_name=node_name,
145
+ messages=[{"role": m.role, "content": m.content} for m in messages],
146
+ )
147
+ )
148
+
149
+ # Call underlying LLM
150
+ start_time = time.perf_counter()
151
+ response = await self._llm.aresponse_with_tools(messages, tools, tool_choice, **kwargs)
152
+ duration_ms = (time.perf_counter() - start_time) * 1000
153
+
154
+ # Emit response received event
155
+ if observer_mgr := get_observer_manager():
156
+ await observer_mgr.notify(
157
+ LLMResponseReceived(
158
+ node_name=node_name, response=response.content or "", duration_ms=duration_ms
159
+ )
160
+ )
161
+
162
+ return response
163
+
164
+ def __getattr__(self, name: str) -> Any:
165
+ """Forward all other attribute access to underlying LLM."""
166
+ return getattr(self._llm, name)
167
+
168
+
169
+ class ObservableToolRouterWrapper:
170
+ """Wraps a ToolRouter port for observability and policy-based control.
171
+
172
+ This wrapper provides:
173
+ - Automatic event emission for all tool calls (ToolCalled, ToolCompleted)
174
+ - Policy evaluation before/after calls (future: auth, rate limiting)
175
+ - Duration tracking and performance metrics
176
+ - Error handling and logging
177
+ """
178
+
179
+ def __init__(self, tool_router: Any):
180
+ """Initialize wrapper with underlying tool router.
181
+
182
+ Parameters
183
+ ----------
184
+ tool_router : Any
185
+ The underlying tool router to wrap
186
+ """
187
+ self._tool_router = tool_router
188
+
189
+ async def acall_tool(self, tool_name: str, params: dict[str, Any]) -> Any:
190
+ """Call tool with observability and policy control.
191
+
192
+ This method:
193
+ 1. Emits ToolCalled event (for observability)
194
+ 2. Calls underlying tool
195
+ 3. Emits ToolCompleted event with duration and result
196
+ 4. (Future: Policy evaluation for auth, rate limiting)
197
+
198
+ Parameters
199
+ ----------
200
+ tool_name : str
201
+ Name of the tool to call
202
+ params : dict[str, Any]
203
+ Tool parameters
204
+
205
+ Returns
206
+ -------
207
+ Any
208
+ Tool execution result
209
+ """
210
+ node_name = get_current_node_name() or "unknown"
211
+
212
+ # Emit tool called event (OBSERVABILITY)
213
+ if observer_mgr := get_observer_manager():
214
+ await observer_mgr.notify(
215
+ ToolCalled(node_name=node_name, tool_name=tool_name, params=params)
216
+ )
217
+
218
+ # TODO(hexdag-team): Add policy evaluation here (CONTROL) #noqa: TD003
219
+ # if policy_mgr := get_policy_manager():
220
+ # policy_response = await policy_mgr.evaluate(ToolCallPolicy(...))
221
+ # if policy_response.signal == PolicySignal.FAIL:
222
+ # raise AuthorizationError(f"Tool '{tool_name}' not authorized")
223
+
224
+ # Call underlying tool
225
+ start_time = time.perf_counter()
226
+ try:
227
+ result = await self._tool_router.acall_tool(tool_name, params)
228
+ duration_ms = (time.perf_counter() - start_time) * 1000
229
+
230
+ # Emit tool completed event (OBSERVABILITY)
231
+ if observer_mgr := get_observer_manager():
232
+ await observer_mgr.notify(
233
+ ToolCompleted(
234
+ node_name=node_name,
235
+ tool_name=tool_name,
236
+ result=result,
237
+ duration_ms=duration_ms,
238
+ )
239
+ )
240
+
241
+ return result
242
+
243
+ except Exception as e:
244
+ duration_ms = (time.perf_counter() - start_time) * 1000
245
+ logger.error(f"Tool '{tool_name}' failed in {duration_ms:.2f}ms: {e}")
246
+
247
+ # Still emit completed event with error info
248
+ if observer_mgr := get_observer_manager():
249
+ await observer_mgr.notify(
250
+ ToolCompleted(
251
+ node_name=node_name,
252
+ tool_name=tool_name,
253
+ result={"error": str(e)},
254
+ duration_ms=duration_ms,
255
+ )
256
+ )
257
+
258
+ raise
259
+
260
+ def __getattr__(self, name: str) -> Any:
261
+ """Forward all other attribute access to underlying tool router."""
262
+ return getattr(self._tool_router, name)
263
+
264
+
265
+ def wrap_llm_port(llm: Any) -> Any:
266
+ """Wrap LLM port with event emission if it implements the LLM protocol.
267
+
268
+ Parameters
269
+ ----------
270
+ llm : Any
271
+ The LLM port to potentially wrap
272
+
273
+ Returns
274
+ -------
275
+ Any
276
+ Wrapped LLM if it has aresponse method, otherwise original object
277
+ """
278
+ if hasattr(llm, "aresponse"):
279
+ return ObservableLLMWrapper(llm)
280
+ return llm
281
+
282
+
283
+ def wrap_tool_router_port(tool_router: Any) -> Any:
284
+ """Wrap ToolRouter port with event emission.
285
+
286
+ Parameters
287
+ ----------
288
+ tool_router : Any
289
+ The tool router to potentially wrap
290
+
291
+ Returns
292
+ -------
293
+ Any
294
+ Wrapped tool router if it has acall_tool method, otherwise original object
295
+ """
296
+ if hasattr(tool_router, "acall_tool"):
297
+ return ObservableToolRouterWrapper(tool_router)
298
+ return tool_router
299
+
300
+
301
+ def wrap_ports_with_observability(ports: dict[str, Any]) -> dict[str, Any]:
302
+ """Wrap all ports with event-emitting wrappers.
303
+
304
+ This wraps:
305
+ - LLM ports: Emit LLMPromptSent/LLMResponseReceived events
306
+ - ToolRouter ports: Emit ToolCalled/ToolCompleted events
307
+ - (Future: Database, Memory, etc.)
308
+
309
+ Parameters
310
+ ----------
311
+ ports : dict[str, Any]
312
+ Dictionary of port name to port adapter
313
+
314
+ Returns
315
+ -------
316
+ dict[str, Any]
317
+ Dictionary with wrapped ports
318
+ """
319
+ wrapped = {}
320
+ for name, port in ports.items():
321
+ if name == "llm":
322
+ wrapped[name] = wrap_llm_port(port)
323
+ elif name == "tool_router":
324
+ wrapped[name] = wrap_tool_router_port(port)
325
+ else:
326
+ wrapped[name] = port
327
+ return wrapped
@@ -0,0 +1,32 @@
1
+ """Prompt template utilities for the application layer.
2
+
3
+ This module contains prompt template classes used by application nodes for rendering templated
4
+ prompts with variable substitution.
5
+
6
+ Note: ChatPromptTemplate, FewShotPromptTemplate, and ChatFewShotTemplate have been moved to
7
+ hexdag.builtin.prompts.base but are re-exported here for backward compatibility.
8
+ """
9
+
10
+ # Core template (minimal)
11
+ # Advanced templates (from builtin, re-exported for backward compatibility)
12
+ from hexdag.builtin.prompts.base import (
13
+ ChatFewShotTemplate,
14
+ ChatPromptTemplate,
15
+ FewShotPromptTemplate,
16
+ )
17
+ from hexdag.core.orchestration.prompt.template import PromptTemplate
18
+
19
+ # Type aliases for template types (used across the framework)
20
+ PromptInput = (
21
+ str | PromptTemplate | ChatPromptTemplate | ChatFewShotTemplate | FewShotPromptTemplate
22
+ )
23
+ TemplateType = PromptTemplate | ChatPromptTemplate | ChatFewShotTemplate | FewShotPromptTemplate
24
+
25
+ __all__ = [
26
+ "PromptTemplate",
27
+ "FewShotPromptTemplate",
28
+ "ChatPromptTemplate",
29
+ "ChatFewShotTemplate",
30
+ "PromptInput",
31
+ "TemplateType",
32
+ ]