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,623 @@
1
+ """Fluent builder for organizing orchestrator ports into logical categories.
2
+
3
+ This builder provides a clean, type-safe interface for configuring orchestrator
4
+ dependencies while maintaining backward compatibility with the flat dictionary format.
5
+
6
+ Enhanced with per-node and per-type port configuration support for fine-grained
7
+ control over port assignment across different node types and specific nodes.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Any, Self
13
+
14
+ if TYPE_CHECKING:
15
+ from hexdag.core.ports import (
16
+ LLM,
17
+ APICall,
18
+ DatabasePort,
19
+ Memory,
20
+ ObserverManagerPort,
21
+ ToolRouter,
22
+ )
23
+
24
+ # Type alias for all valid port types
25
+ PortType = (
26
+ LLM
27
+ | APICall
28
+ | DatabasePort
29
+ | Memory
30
+ | ObserverManagerPort
31
+ | ToolRouter
32
+ | Any # Allow Any for backward compatibility with custom ports
33
+ )
34
+ else:
35
+ # At runtime, just use Any (backward compatible)
36
+ PortType = Any
37
+
38
+ from hexdag.core.orchestration.models import PortConfig, PortsConfiguration
39
+
40
+
41
+ class PortsBuilder:
42
+ """Fluent builder for organizing ports into logical categories.
43
+
44
+ Provides a type-safe, discoverable API for configuring orchestrator
45
+ dependencies while maintaining backward compatibility.
46
+
47
+ Example
48
+ -------
49
+ ```python
50
+ # Traditional flat dictionary approach
51
+ ports = {
52
+ "llm": OpenAIAdapter(),
53
+ "database": PostgresAdapter(),
54
+ "observer_manager": ObserverManager(),
55
+ # ... many more mixed together
56
+ }
57
+
58
+ # New builder approach - organized and type-safe
59
+ ports = (
60
+ PortsBuilder()
61
+ .with_llm(OpenAIAdapter())
62
+ .with_database(PostgresAdapter())
63
+ .with_defaults()
64
+ .build()
65
+ )
66
+
67
+ # Or use with_defaults() for automatic setup
68
+ ports = PortsBuilder().with_defaults().build()
69
+ ```
70
+ """
71
+
72
+ def __init__(self) -> None:
73
+ """Initialize an empty ports builder."""
74
+ self._ports: dict[str, PortType] = {}
75
+ self._type_ports: dict[str, dict[str, PortType]] = {}
76
+ self._node_ports: dict[str, dict[str, PortType]] = {}
77
+
78
+ def _add_port(self, key: str, port: PortType) -> Self:
79
+ """Add a port to the internal registry.
80
+
81
+ Args
82
+ ----
83
+ key: Port identifier
84
+ port: Port implementation
85
+
86
+ Returns
87
+ -------
88
+ Self for method chaining
89
+ """
90
+ self._ports[key] = port
91
+ return self
92
+
93
+ # Core AI Capabilities
94
+ # --------------------
95
+
96
+ def with_llm(self, llm: LLM) -> Self:
97
+ """Add a Language Model adapter.
98
+
99
+ Args
100
+ ----
101
+ llm: LLM adapter instance (OpenAI, Anthropic, etc.)
102
+
103
+ Returns
104
+ -------
105
+ Self for method chaining
106
+ """
107
+ return self._add_port("llm", llm)
108
+
109
+ def with_tool_router(self, router: ToolRouter) -> Self:
110
+ """Add a tool router for function calling.
111
+
112
+ Args
113
+ ----
114
+ router: Tool router instance for managing tool execution
115
+
116
+ Returns
117
+ -------
118
+ Self for method chaining
119
+ """
120
+ return self._add_port("tool_router", router)
121
+
122
+ # Storage & Persistence
123
+ # ---------------------
124
+
125
+ def with_database(self, database: DatabasePort) -> Self:
126
+ """Add a database adapter.
127
+
128
+ Args
129
+ ----
130
+ database: Database adapter instance
131
+
132
+ Returns
133
+ -------
134
+ Self for method chaining
135
+ """
136
+ return self._add_port("database", database)
137
+
138
+ def with_memory(self, memory: Memory) -> Self:
139
+ """Add a memory system for agents.
140
+
141
+ Args
142
+ ----
143
+ memory: Memory adapter for conversation history
144
+
145
+ Returns
146
+ -------
147
+ Self for method chaining
148
+ """
149
+ return self._add_port("memory", memory)
150
+
151
+ # Event & Control Systems
152
+ # -----------------------
153
+
154
+ def with_observer_manager(self, manager: ObserverManagerPort) -> Self:
155
+ """Add an observer manager for event monitoring.
156
+
157
+ Args
158
+ ----
159
+ manager: Observer manager for read-only event handling
160
+
161
+ Returns
162
+ -------
163
+ Self for method chaining
164
+ """
165
+ return self._add_port("observer_manager", manager)
166
+
167
+ # External Integrations
168
+ # ---------------------
169
+
170
+ def with_api_call(self, api_call: APICall) -> Self:
171
+ """Add an API call adapter.
172
+
173
+ Args
174
+ ----
175
+ api_call: API call adapter for external services
176
+
177
+ Returns
178
+ -------
179
+ Self for method chaining
180
+ """
181
+ return self._add_port("api_call", api_call)
182
+
183
+ # Convenience Methods
184
+ # ------------------
185
+
186
+ def with_defaults(self) -> Self:
187
+ """Add default implementations for common ports.
188
+
189
+ This method provides sensible defaults:
190
+ - LocalObserverManager for event observation
191
+ - MockLLM for testing (should be overridden in production)
192
+
193
+ Returns
194
+ -------
195
+ Self for method chaining
196
+ """
197
+ # Only add defaults if not already configured
198
+ if "observer_manager" not in self._ports:
199
+ try:
200
+ from hexdag.builtin.adapters.local import LocalObserverManager
201
+
202
+ self._ports["observer_manager"] = LocalObserverManager()
203
+ except ImportError:
204
+ pass # Optional dependency
205
+
206
+ if "llm" not in self._ports:
207
+ try:
208
+ from hexdag.builtin.adapters.mock import MockLLM
209
+
210
+ self._ports["llm"] = MockLLM()
211
+ except ImportError:
212
+ pass # Optional dependency
213
+
214
+ return self
215
+
216
+ def with_custom(self, key: str, port: Any) -> Self:
217
+ """Add a custom port with any key.
218
+
219
+ This provides backward compatibility and flexibility for
220
+ custom port implementations.
221
+
222
+ Args
223
+ ----
224
+ key: The port key name
225
+ port: The port implementation
226
+
227
+ Returns
228
+ -------
229
+ Self for method chaining
230
+ """
231
+ return self._add_port(key, port)
232
+
233
+ def update(self, ports: dict[str, Any]) -> Self:
234
+ """Update multiple ports at once from a dictionary.
235
+
236
+ Useful for migrating from existing dictionary-based configs.
237
+
238
+ Args
239
+ ----
240
+ ports: Dictionary of port implementations
241
+
242
+ Returns
243
+ -------
244
+ Self for method chaining
245
+ """
246
+ self._ports.update(ports)
247
+ return self
248
+
249
+ # Builder Methods
250
+ # --------------
251
+
252
+ def build(self) -> dict[str, Any]:
253
+ """Build and return the final ports dictionary.
254
+
255
+ Returns
256
+ -------
257
+ Dictionary of configured ports for orchestrator use
258
+ """
259
+ return self._ports.copy()
260
+
261
+ def clear(self) -> Self:
262
+ """Clear all configured ports.
263
+
264
+ Returns
265
+ -------
266
+ Self for method chaining
267
+ """
268
+ self._ports.clear()
269
+ return self
270
+
271
+ # Inspection Methods
272
+ # -----------------
273
+
274
+ def has(self, key: str) -> bool:
275
+ """Check if a port is configured.
276
+
277
+ Args
278
+ ----
279
+ key: The port key to check
280
+
281
+ Returns
282
+ -------
283
+ True if the port is configured
284
+ """
285
+ return key in self._ports
286
+
287
+ def __contains__(self, key: str) -> bool:
288
+ """Check if a port is configured using 'in' operator.
289
+
290
+ Parameters
291
+ ----------
292
+ key : str
293
+ Port key to check
294
+
295
+ Returns
296
+ -------
297
+ bool
298
+ True if the port is configured at global level
299
+
300
+ Examples
301
+ --------
302
+ >>> builder = PortsBuilder()
303
+ >>> class MockPort: pass
304
+ >>> builder["llm"] = MockPort()
305
+ >>> "llm" in builder
306
+ True
307
+ >>> "database" in builder
308
+ False
309
+ """
310
+ return key in self._ports
311
+
312
+ def get(self, key: str, default: Any = None) -> Any:
313
+ """Get a configured port by key.
314
+
315
+ Args
316
+ ----
317
+ key: The port key
318
+ default: Default value if not found
319
+
320
+ Returns
321
+ -------
322
+ The port instance or default value
323
+ """
324
+ return self._ports.get(key, default)
325
+
326
+ def keys(self) -> list[str]:
327
+ """Get list of configured port keys.
328
+
329
+ Returns
330
+ -------
331
+ List of port keys
332
+ """
333
+ return list(self._ports.keys())
334
+
335
+ def __len__(self) -> int:
336
+ """Get number of configured ports.
337
+
338
+ Returns
339
+ -------
340
+ Number of configured ports
341
+ """
342
+ return len(self._ports)
343
+
344
+ def __repr__(self) -> str:
345
+ """Get string representation of builder state.
346
+
347
+ Returns
348
+ -------
349
+ String showing configured ports
350
+ """
351
+ configured = ", ".join(self._ports.keys()) if self._ports else "none"
352
+ return f"PortsBuilder(configured: {configured})"
353
+
354
+ def __getitem__(self, key: str | tuple) -> Any:
355
+ """Get port(s) using dictionary-style access with tuple key support.
356
+
357
+ Supports simple and hierarchical access patterns:
358
+ - Simple: builder["llm"] - Get global port
359
+ - Tuple (2 elements): builder["agent", "llm"] - Get type-level port
360
+ - Tuple (3 elements): builder["researcher", "agent", "llm"] - Get node-level port
361
+
362
+ Parameters
363
+ ----------
364
+ key : str | tuple
365
+ Port key or tuple of (node, type, port) for hierarchical lookup
366
+
367
+ Returns
368
+ -------
369
+ Any
370
+ The port instance
371
+
372
+ Raises
373
+ ------
374
+ KeyError
375
+ If the port is not configured
376
+
377
+ Examples
378
+ --------
379
+ >>> builder = PortsBuilder()
380
+ >>> class MockLLM: pass
381
+ >>> class AgentLLM: pass
382
+ >>> builder["llm"] = MockLLM()
383
+ >>> port = builder["llm"] # Simple access
384
+ >>> isinstance(port, MockLLM)
385
+ True
386
+ >>> result = builder.for_type("agent", llm=AgentLLM()) # Returns self
387
+ >>> port = builder["agent", "llm"] # Type-level access
388
+ >>> isinstance(port, AgentLLM)
389
+ True
390
+ """
391
+ if isinstance(key, tuple):
392
+ if len(key) == 2:
393
+ # (type, port_name) - Get type-level port
394
+ node_type, port_name = key
395
+ if node_type in self._type_ports and port_name in self._type_ports[node_type]:
396
+ return self._type_ports[node_type][port_name]
397
+ # Fall back to global
398
+ return self._ports.get(port_name)
399
+ if len(key) == 3:
400
+ # (node_name, type, port_name) - Get node-level port with resolution
401
+ node_name, node_type, port_name = key
402
+ # Try node-level first
403
+ if node_name in self._node_ports and port_name in self._node_ports[node_name]:
404
+ return self._node_ports[node_name][port_name]
405
+ # Fall back to type-level
406
+ if node_type in self._type_ports and port_name in self._type_ports[node_type]:
407
+ return self._type_ports[node_type][port_name]
408
+ # Fall back to global
409
+ return self._ports.get(port_name)
410
+ raise KeyError(f"Tuple key must have 2 or 3 elements, got {len(key)}")
411
+ return self._ports[key]
412
+
413
+ def __setitem__(self, key: str | tuple, value: Any) -> None:
414
+ """Set port(s) using dictionary-style access with tuple key support.
415
+
416
+ Supports simple and hierarchical configuration:
417
+ - Simple: builder["llm"] = adapter - Set global port
418
+ - Tuple (2 elements): builder["agent", "llm"] = adapter - Set type-level port
419
+ - Tuple (3 elements): builder["node", "agent", "llm"] = adapter - Set node-level port
420
+
421
+ Parameters
422
+ ----------
423
+ key : str | tuple
424
+ Port key or tuple of (node, type, port) for hierarchical configuration
425
+ value : Any
426
+ Port implementation to configure
427
+
428
+ Examples
429
+ --------
430
+ >>> builder = PortsBuilder()
431
+ >>> class MockLLM: pass
432
+ >>> class OpenAIAdapter: pass
433
+ >>> class ClaudeAdapter: pass
434
+ >>> builder["llm"] = MockLLM() # Global
435
+ >>> builder["agent", "llm"] = OpenAIAdapter() # Type-level
436
+ >>> builder["researcher", "agent", "llm"] = ClaudeAdapter() # Node-level
437
+ >>> isinstance(builder["llm"], MockLLM)
438
+ True
439
+ >>> isinstance(builder["agent", "llm"], OpenAIAdapter)
440
+ True
441
+ >>> isinstance(builder["researcher", "agent", "llm"], ClaudeAdapter)
442
+ True
443
+ """
444
+ if isinstance(key, tuple):
445
+ if len(key) == 2:
446
+ # (type, port_name) - Set type-level port
447
+ node_type, port_name = key
448
+ self.for_type(node_type, **{port_name: value})
449
+ elif len(key) == 3:
450
+ # (node_name, type, port_name) - Set node-level port
451
+ node_name, _node_type, port_name = key
452
+ self.for_node(node_name, **{port_name: value})
453
+ else:
454
+ raise KeyError(f"Tuple key must have 2 or 3 elements, got {len(key)}")
455
+ else:
456
+ self._add_port(key, value)
457
+
458
+ def __delitem__(self, key: str) -> None:
459
+ """Remove a port from the builder.
460
+
461
+ Parameters
462
+ ----------
463
+ key : str
464
+ Port key to remove
465
+
466
+ Examples
467
+ --------
468
+ .. code-block:: python
469
+
470
+ builder = PortsBuilder()
471
+ builder["llm"] = MockLLM()
472
+ del builder["llm"]
473
+ assert "llm" not in builder
474
+ """
475
+ del self._ports[key]
476
+
477
+ # Enhanced Configuration Methods
478
+ # ------------------------------
479
+
480
+ def for_type(self, node_type: str, **ports: Any) -> Self:
481
+ """Configure ports for all nodes of a specific type.
482
+
483
+ This method allows setting default ports for all nodes of a given type
484
+ (e.g., "agent", "llm", "function"). These type-level defaults override
485
+ global defaults but are overridden by per-node configurations.
486
+
487
+ Args
488
+ ----
489
+ node_type: The node type to configure (e.g., "agent", "llm", "function")
490
+ **ports: Port implementations as keyword arguments
491
+
492
+ Returns
493
+ -------
494
+ Self for method chaining
495
+
496
+ Examples
497
+ --------
498
+ Example usage::
499
+
500
+ from hexdag.builtin.adapters.openai import OpenAIAdapter
501
+ from hexdag.builtin.adapters.mock import MockLLM
502
+
503
+ builder = (
504
+ PortsBuilder()
505
+ .with_llm(MockLLM()) # Global default
506
+ .for_type("agent", llm=OpenAIAdapter(model="gpt-4")) # Agent nodes
507
+ .build_configuration()
508
+ )
509
+
510
+ # All agent nodes will use OpenAI, other nodes use MockLLM
511
+ config = builder.build_configuration()
512
+ agent_ports = config.resolve_ports("my_agent", "agent")
513
+ assert isinstance(agent_ports["llm"].port, OpenAIAdapter)
514
+ """
515
+ if node_type not in self._type_ports:
516
+ self._type_ports[node_type] = {}
517
+ self._type_ports[node_type].update(ports)
518
+ return self
519
+
520
+ def for_node(self, node_name: str, **ports: Any) -> Self:
521
+ """Configure ports for a specific node by name.
522
+
523
+ This method allows overriding ports for individual nodes, providing
524
+ the highest level of configuration precedence. Perfect for nodes that
525
+ require special adapters or configurations.
526
+
527
+ Args
528
+ ----
529
+ node_name: The node name to configure
530
+ **ports: Port implementations as keyword arguments
531
+
532
+ Returns
533
+ -------
534
+ Self for method chaining
535
+
536
+ Examples
537
+ --------
538
+ Example usage::
539
+
540
+ from hexdag.builtin.adapters.anthropic import AnthropicAdapter
541
+ from hexdag.builtin.adapters.openai import OpenAIAdapter
542
+
543
+ builder = (
544
+ PortsBuilder()
545
+ .for_type("agent", llm=OpenAIAdapter(model="gpt-4")) # Agent default
546
+ .for_node("researcher", llm=AnthropicAdapter(model="claude-3")) # Override
547
+ .build_configuration()
548
+ )
549
+
550
+ # Researcher node gets Claude, other agents get GPT-4
551
+ config = builder.build_configuration()
552
+ researcher_ports = config.resolve_ports("researcher", "agent")
553
+ assert isinstance(researcher_ports["llm"].port, AnthropicAdapter)
554
+ """
555
+ if node_name not in self._node_ports:
556
+ self._node_ports[node_name] = {}
557
+ self._node_ports[node_name].update(ports)
558
+ return self
559
+
560
+ def build_configuration(self) -> PortsConfiguration:
561
+ """Build a PortsConfiguration with full inheritance support.
562
+
563
+ Creates a PortsConfiguration object that encapsulates global, per-type,
564
+ and per-node port configurations. This provides more flexibility than
565
+ the flat dictionary returned by build().
566
+
567
+ Returns
568
+ -------
569
+ PortsConfiguration
570
+ Configuration with port inheritance and resolution support
571
+
572
+ Examples
573
+ --------
574
+ Example usage::
575
+
576
+ from hexdag.builtin.adapters.mock import MockLLM
577
+ from hexdag.builtin.adapters.openai import OpenAIAdapter
578
+ from hexdag.builtin.adapters.anthropic import AnthropicAdapter
579
+
580
+ config = (
581
+ PortsBuilder()
582
+ .with_llm(MockLLM()) # Global default
583
+ .for_type("agent", llm=OpenAIAdapter(model="gpt-4")) # Agent default
584
+ .for_node("researcher", llm=AnthropicAdapter(model="claude-3")) # Override
585
+ .build_configuration()
586
+ )
587
+
588
+ # Resolve ports for different nodes
589
+ researcher = config.to_flat_dict("researcher", "agent") # AnthropicAdapter
590
+ analyzer = config.to_flat_dict("analyzer", "agent") # OpenAIAdapter
591
+ transformer = config.to_flat_dict("transformer", "function") # MockLLM
592
+
593
+ Notes
594
+ -----
595
+ Resolution order: per-node > per-type > global defaults
596
+
597
+ See Also
598
+ --------
599
+ build : For backward-compatible flat dictionary output
600
+ """
601
+
602
+ def _wrap_ports(ports_dict: dict[str, PortType] | None) -> dict[str, PortConfig] | None:
603
+ """Helper to wrap ports in PortConfig."""
604
+ return {k: PortConfig(port=v) for k, v in ports_dict.items()} if ports_dict else None
605
+
606
+ def _wrap_nested_ports(
607
+ nested: dict[str, dict[str, PortType]] | None,
608
+ ) -> dict[str, dict[str, PortConfig]] | None:
609
+ """Helper to wrap nested ports in PortConfig."""
610
+ return (
611
+ {
612
+ name: {k: PortConfig(port=v) for k, v in ports.items()}
613
+ for name, ports in nested.items()
614
+ }
615
+ if nested
616
+ else None
617
+ )
618
+
619
+ return PortsConfiguration(
620
+ global_ports=_wrap_ports(self._ports),
621
+ type_ports=_wrap_nested_ports(self._type_ports),
622
+ node_ports=_wrap_nested_ports(self._node_ports),
623
+ )