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,248 @@
1
+ """Local environment variable based secret adapter."""
2
+
3
+ import os
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from hexdag.core.logging import get_logger
7
+ from hexdag.core.types import Secret
8
+
9
+ if TYPE_CHECKING:
10
+ from hexdag.core.ports.healthcheck import HealthStatus
11
+ from hexdag.core.ports.memory import Memory
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ class LocalSecretAdapter:
17
+ """Local secret adapter that reads from environment variables.
18
+
19
+ This adapter implements the SecretPort interface using local environment
20
+ variables as the secret source. It's useful for:
21
+ - Development and testing
22
+ - CI/CD pipelines
23
+ - Simple deployments without external secret managers
24
+
25
+ The adapter wraps secrets in the Secret class to prevent accidental logging.
26
+
27
+ Examples
28
+ --------
29
+ Basic usage::
30
+
31
+ secrets = LocalSecretAdapter()
32
+
33
+ api_key = await secrets.aget_secret("OPENAI_API_KEY")
34
+ print(api_key) # <SECRET> (hidden)
35
+ print(api_key.get()) # "sk-..." (actual value)
36
+
37
+ # Load secrets into Memory for orchestrator
38
+ mapping = await secrets.aload_secrets_to_memory(
39
+ memory=memory,
40
+ keys=["OPENAI_API_KEY", "DATABASE_PASSWORD"]
41
+ )
42
+ # Returns: {"OPENAI_API_KEY": "secret:OPENAI_API_KEY", ...}
43
+
44
+ With prefix filtering::
45
+
46
+ # Only load secrets with specific prefix
47
+ secrets = LocalSecretAdapter(env_prefix="MYAPP_")
48
+ # Will look for MYAPP_OPENAI_API_KEY, etc.
49
+ """
50
+
51
+ # Type annotations for attributes
52
+ env_prefix: str
53
+ allow_empty: bool
54
+ _cache: dict[str, Secret]
55
+
56
+ def __init__(self, **kwargs: Any) -> None:
57
+ """Initialize local secret adapter.
58
+
59
+ Args
60
+ ----
61
+ **kwargs: Configuration options (env_prefix, allow_empty)
62
+ """
63
+ self.env_prefix = kwargs.get("env_prefix", "")
64
+ self.allow_empty = kwargs.get("allow_empty", False)
65
+
66
+ self._cache: dict[str, Secret] = {}
67
+
68
+ async def aget_secret(self, key: str) -> Secret:
69
+ """Retrieve a single secret from environment variables.
70
+
71
+ Args
72
+ ----
73
+ key: Secret identifier (environment variable name)
74
+
75
+ Returns
76
+ -------
77
+ Secret
78
+ Secret wrapper containing the secret value
79
+
80
+ Raises
81
+ ------
82
+ KeyError
83
+ If the secret is not found in environment variables
84
+ ValueError
85
+ If the secret value is empty (unless allow_empty=True)
86
+
87
+ Examples
88
+ --------
89
+ >>> import os
90
+ >>> os.environ["OPENAI_API_KEY"] = "sk-test"
91
+ >>> adapter = LocalSecretAdapter()
92
+ >>> secret = await adapter.aget_secret("OPENAI_API_KEY") # doctest: +SKIP
93
+ >>> print(secret) # doctest: +SKIP
94
+ <SECRET>
95
+ >>> secret.get() # doctest: +SKIP
96
+ 'sk-test'
97
+ """
98
+ # Check cache first (environment variables don't change at runtime)
99
+ if key in self._cache:
100
+ logger.debug(f"Retrieved secret '{key}' from cache")
101
+ return self._cache[key]
102
+
103
+ env_var_name = f"{self.env_prefix}{key}"
104
+
105
+ value = os.getenv(env_var_name)
106
+
107
+ if value is None:
108
+ raise KeyError(
109
+ f"Secret '{key}' not found in environment variables (looked for: {env_var_name})"
110
+ )
111
+
112
+ if value == "" and not self.allow_empty:
113
+ raise ValueError(
114
+ f"Secret '{key}' cannot be empty (set allow_empty=True to allow empty secrets)"
115
+ )
116
+
117
+ logger.debug(f"Retrieved secret '{key}' from environment")
118
+ secret = Secret(value)
119
+
120
+ self._cache[key] = secret
121
+
122
+ return secret
123
+
124
+ async def aload_secrets_to_memory(
125
+ self,
126
+ memory: "Memory",
127
+ prefix: str = "secret:",
128
+ keys: list[str] | None = None,
129
+ ) -> dict[str, str]:
130
+ """Bulk load secrets from environment into Memory port.
131
+
132
+ Args
133
+ ----
134
+ memory: Memory port instance to store secrets in
135
+ prefix: Key prefix for stored secrets (default: "secret:")
136
+ keys: List of secret keys to load. If None, loads all env vars.
137
+
138
+ Returns
139
+ -------
140
+ dict[str, str]
141
+ Mapping of original key → memory key
142
+
143
+ Examples
144
+ --------
145
+ >>> import os
146
+ >>> os.environ["OPENAI_API_KEY"] = "sk-test"
147
+ >>> os.environ["DATABASE_PASSWORD"] = "pass123"
148
+ >>> adapter = LocalSecretAdapter()
149
+ >>> # Load specific secrets
150
+ >>> mapping = await adapter.aload_secrets_to_memory( # doctest: +SKIP
151
+ ... memory=memory,
152
+ ... keys=["OPENAI_API_KEY"]
153
+ ... )
154
+ >>> mapping # doctest: +SKIP
155
+ {'OPENAI_API_KEY': 'secret:OPENAI_API_KEY'}
156
+ """
157
+ mapping: dict[str, str] = {}
158
+
159
+ if keys is None:
160
+ # Load all environment variables (with prefix if configured)
161
+ keys = [
162
+ key.removeprefix(self.env_prefix)
163
+ for key in os.environ
164
+ if key.startswith(self.env_prefix)
165
+ ]
166
+ logger.debug(f"Auto-discovered {len(keys)} environment variables")
167
+
168
+ # Load each secret
169
+ loaded_count = 0
170
+ for key in keys:
171
+ try:
172
+ secret = await self.aget_secret(key)
173
+ memory_key = f"{prefix}{key}"
174
+ await memory.aset(memory_key, secret.get())
175
+ mapping[key] = memory_key
176
+ loaded_count += 1
177
+ logger.debug(f"Loaded secret '{key}' → '{memory_key}'")
178
+ except (KeyError, ValueError) as e:
179
+ logger.warning(f"Failed to load secret '{key}': {e}")
180
+ continue
181
+
182
+ logger.info(f"Loaded {loaded_count}/{len(keys)} secrets into Memory with prefix '{prefix}'")
183
+ return mapping
184
+
185
+ async def alist_secret_names(self) -> list[str]:
186
+ """List all available secret names from environment variables.
187
+
188
+ Returns
189
+ -------
190
+ list[str]
191
+ List of environment variable names (with prefix removed)
192
+
193
+ Examples
194
+ --------
195
+ >>> import os
196
+ >>> os.environ["OPENAI_API_KEY"] = "sk-test"
197
+ >>> os.environ["DATABASE_PASSWORD"] = "pass123"
198
+ >>> adapter = LocalSecretAdapter()
199
+ >>> await adapter.alist_secret_names() # doctest: +SKIP
200
+ ['OPENAI_API_KEY', 'DATABASE_PASSWORD', ...]
201
+ """
202
+ names = [
203
+ key.removeprefix(self.env_prefix)
204
+ for key in os.environ
205
+ if key.startswith(self.env_prefix)
206
+ ]
207
+ logger.debug(f"Found {len(names)} environment variables")
208
+ return names
209
+
210
+ async def ahealth_check(self) -> "HealthStatus":
211
+ """Check health status of local environment variable access.
212
+
213
+ Returns
214
+ -------
215
+ HealthStatus
216
+ Health status with environment variable count
217
+
218
+ Examples
219
+ --------
220
+ >>> adapter = LocalSecretAdapter()
221
+ >>> status = await adapter.ahealth_check() # doctest: +SKIP
222
+ >>> status.status # doctest: +SKIP
223
+ 'healthy'
224
+ """
225
+ from hexdag.core.ports.healthcheck import HealthStatus
226
+
227
+ try:
228
+ # Count available env vars
229
+ names = await self.alist_secret_names()
230
+ return HealthStatus(
231
+ status="healthy",
232
+ adapter_name="local_env",
233
+ port_name="secret",
234
+ details={
235
+ "env_vars_count": len(names),
236
+ "env_prefix": self.env_prefix or "(none)",
237
+ },
238
+ )
239
+ except Exception as e:
240
+ return HealthStatus(
241
+ status="unhealthy",
242
+ adapter_name="local_env",
243
+ port_name="secret",
244
+ error=e,
245
+ details={
246
+ "error_message": str(e),
247
+ },
248
+ )
@@ -0,0 +1,280 @@
1
+ """ToolRouter adapter that aggregates multiple tool routers with namespacing."""
2
+
3
+ import asyncio
4
+ import inspect
5
+ from typing import Any
6
+
7
+ from hexdag.builtin.nodes.tool_utils import ToolDefinition, ToolParameter
8
+ from hexdag.core.exceptions import ResourceNotFoundError
9
+ from hexdag.core.logging import get_logger
10
+ from hexdag.core.ports.tool_router import ToolRouter
11
+ from hexdag.core.protocols import has_execute_method
12
+
13
+ logger = get_logger(__name__)
14
+
15
+ __all__ = ["UnifiedToolRouter"]
16
+
17
+
18
+ class UnifiedToolRouter(ToolRouter):
19
+ """ToolRouter adapter that supports multiple tool sources with namespacing.
20
+
21
+ This adapter aggregates multiple tool routers and provides unified access
22
+ with namespace prefixes (e.g., "builtin::tool", "mcp::tool").
23
+
24
+ Example
25
+ -------
26
+ router = UnifiedToolRouter(routers={
27
+ "builtin": PythonToolRouter(),
28
+ "mcp_sql": MCPToolRouter("sqlite"),
29
+ })
30
+ result = await router.acall_tool("builtin::search_papers", {"query": "..."})
31
+ """
32
+
33
+ def __init__(self, routers: dict[str, ToolRouter] | None = None, **kwargs: Any) -> None:
34
+ """Initialize the router.
35
+
36
+ Args
37
+ ----
38
+ routers: Dict of {router_id: ToolRouter} for multi-router mode
39
+ **kwargs: Additional configuration options
40
+ """
41
+ self.routers = routers or {}
42
+
43
+ # Store first router as default for unprefixed calls
44
+ self.default_router = next(iter(self.routers.values())) if self.routers else None
45
+
46
+ async def acall_tool(self, tool_name: str, params: dict[str, Any]) -> Any:
47
+ """Call a tool with parameters.
48
+
49
+ Supports both namespaced ("router_id::tool_name") and plain tool names.
50
+
51
+ Args
52
+ ----
53
+ tool_name: Name of the tool to execute (with optional "router::" prefix)
54
+ params: Parameters to pass to the tool
55
+
56
+ Returns
57
+ -------
58
+ Tool execution result
59
+ """
60
+ # Parse namespace: "builtin::tool" or "tool"
61
+ if "::" in tool_name:
62
+ router_id, actual_tool = tool_name.split("::", 1)
63
+ if router_id not in self.routers:
64
+ available_routers = list(self.routers.keys())
65
+ raise ResourceNotFoundError(
66
+ "tool_router",
67
+ router_id,
68
+ available_routers,
69
+ )
70
+ router = self.routers[router_id]
71
+ else:
72
+ # No prefix: use default router
73
+ if not self.default_router:
74
+ raise ResourceNotFoundError(
75
+ "tool",
76
+ tool_name,
77
+ [],
78
+ )
79
+ router = self.default_router
80
+ actual_tool = tool_name
81
+
82
+ # Delegate to the specific router
83
+ return await router.acall_tool(actual_tool, params)
84
+
85
+ async def _execute_tool(self, tool: Any, params: dict[str, Any]) -> Any:
86
+ """Execute any tool (function, class, or instance) with parameters.
87
+
88
+ Raises
89
+ ------
90
+ ValueError
91
+ If tool is not executable
92
+ """
93
+ try:
94
+ if inspect.iscoroutinefunction(tool):
95
+ return await self._call_with_params(tool, params, is_async=True)
96
+ if has_execute_method(tool):
97
+ # Class with execute method (protocol-based check)
98
+ execute_method = tool.execute
99
+ if asyncio.iscoroutinefunction(execute_method):
100
+ return await self._call_with_params(execute_method, params, is_async=True)
101
+ return self._call_with_params(execute_method, params, is_async=False)
102
+ if callable(tool):
103
+ # Regular function
104
+ return self._call_with_params(tool, params, is_async=False)
105
+ raise ValueError(f"Tool {tool} is not executable")
106
+
107
+ except Exception as e:
108
+ raise e
109
+
110
+ def _call_with_params(self, func: Any, params: dict[str, Any], is_async: bool) -> Any:
111
+ """Call function with appropriate parameters.
112
+
113
+ Args
114
+ ----
115
+ func: Function to call
116
+ params: Parameters dict
117
+ is_async: Whether function is async
118
+
119
+ Returns
120
+ -------
121
+ Function result (or coroutine if async)
122
+ """
123
+ sig = inspect.signature(func)
124
+
125
+ has_var_keyword = any(
126
+ p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
127
+ )
128
+
129
+ if has_var_keyword:
130
+ kwargs = params
131
+ else:
132
+ # Filter to only expected parameters
133
+ kwargs = {}
134
+ for param_name in sig.parameters:
135
+ if param_name in params:
136
+ kwargs[param_name] = params[param_name]
137
+
138
+ if is_async:
139
+ return func(**kwargs) # Return coroutine, caller will await
140
+ return func(**kwargs)
141
+
142
+ def get_available_tools(self) -> list[str]:
143
+ """Get list of available tool names.
144
+
145
+ Returns namespaced tools: ["router1::tool1", "router2::tool2"]
146
+ """
147
+ all_tools = []
148
+ for router_id, router in self.routers.items():
149
+ try:
150
+ router_tools = router.get_available_tools()
151
+ # Prefix each tool with router ID
152
+ namespaced_tools = [f"{router_id}::{tool}" for tool in router_tools]
153
+ all_tools.extend(namespaced_tools)
154
+ except Exception as e:
155
+ logger.debug("Could not list tools from router %s: %s", router_id, e)
156
+ return all_tools
157
+
158
+ def get_tool_schema(self, tool_name: str) -> dict[str, Any]:
159
+ """Get schema for a specific tool.
160
+
161
+ Args
162
+ ----
163
+ tool_name: Name of the tool (with optional "router::" prefix)
164
+
165
+ Returns
166
+ -------
167
+ Tool schema dictionary or empty dict if not found
168
+ """
169
+ # Parse namespace
170
+ router_id: str
171
+ if "::" in tool_name:
172
+ router_id, actual_tool = tool_name.split("::", 1)
173
+ if router_id not in self.routers:
174
+ return {}
175
+ router = self.routers[router_id]
176
+ else:
177
+ if not self.default_router:
178
+ return {}
179
+ router = self.default_router
180
+ router_id = "default"
181
+ actual_tool = tool_name
182
+
183
+ # Get schema from specific router
184
+ try:
185
+ schema = router.get_tool_schema(actual_tool)
186
+ # Add router info
187
+ schema["_router"] = router_id
188
+ return schema
189
+ except Exception as e:
190
+ logger.debug("Could not get tool schema from router: %s", e)
191
+ return {}
192
+
193
+ def get_all_tool_schemas(self) -> dict[str, dict[str, Any]]:
194
+ """Get schemas for all available tools.
195
+
196
+ Returns schemas with namespaced keys.
197
+ """
198
+ schemas = {}
199
+ for tool_name in self.get_available_tools():
200
+ if schema := self.get_tool_schema(tool_name):
201
+ schemas[tool_name] = schema
202
+ return schemas
203
+
204
+ async def aget_available_tools(self) -> list[str]:
205
+ """Async version of get_available_tools."""
206
+ all_tools = []
207
+ for router_id, router in self.routers.items():
208
+ try:
209
+ # Use async method if available
210
+ if hasattr(router, "aget_available_tools"):
211
+ router_tools = await router.aget_available_tools()
212
+ else:
213
+ router_tools = router.get_available_tools()
214
+ namespaced_tools = [f"{router_id}::{tool}" for tool in router_tools]
215
+ all_tools.extend(namespaced_tools)
216
+ except Exception as e:
217
+ logger.debug("Could not list tools from router %s: %s", router_id, e)
218
+ return all_tools
219
+
220
+ async def aget_tool_schema(self, tool_name: str) -> dict[str, Any]:
221
+ """Async version of get_tool_schema."""
222
+ # Parse namespace
223
+ router_id: str
224
+ if "::" in tool_name:
225
+ router_id, actual_tool = tool_name.split("::", 1)
226
+ if router_id not in self.routers:
227
+ return {}
228
+ router = self.routers[router_id]
229
+ else:
230
+ if not self.default_router:
231
+ return {}
232
+ router = self.default_router
233
+ router_id = "default"
234
+ actual_tool = tool_name
235
+
236
+ # Get schema from specific router (async if available)
237
+ try:
238
+ if hasattr(router, "aget_tool_schema"):
239
+ schema = await router.aget_tool_schema(actual_tool)
240
+ else:
241
+ schema = router.get_tool_schema(actual_tool)
242
+ schema["_router"] = router_id
243
+ return schema
244
+ except Exception as e:
245
+ logger.debug("Could not get tool schema from router: %s", e)
246
+ return {}
247
+
248
+ def get_tool_definitions(self) -> list[ToolDefinition]:
249
+ """Get ToolDefinitions from all routers.
250
+
251
+ Returns
252
+ -------
253
+ List of ToolDefinitions generated from router tools
254
+ """
255
+ definitions = []
256
+ for router_id, router in self.routers.items():
257
+ try:
258
+ for tool_name in router.get_available_tools():
259
+ schema = router.get_tool_schema(tool_name)
260
+ if schema:
261
+ tool_def = ToolDefinition(
262
+ name=f"{router_id}::{tool_name}",
263
+ simplified_description=schema.get("description", f"Tool {tool_name}"),
264
+ detailed_description=schema.get("description", f"Tool {tool_name}"),
265
+ parameters=[
266
+ ToolParameter(
267
+ name=p.get("name", ""),
268
+ description=p.get("description", ""),
269
+ param_type=p.get("type", "Any"),
270
+ required=p.get("required", False),
271
+ default=p.get("default"),
272
+ )
273
+ for p in schema.get("parameters", [])
274
+ ],
275
+ examples=[f"{router_id}::{tool_name}()"],
276
+ )
277
+ definitions.append(tool_def)
278
+ except Exception as e:
279
+ logger.debug("Could not get tool definitions from router %s: %s", router_id, e)
280
+ return definitions
@@ -0,0 +1,17 @@
1
+ """Built-in macro implementations for hexDAG."""
2
+
3
+ from hexdag.builtin.macros.conversation_agent import ConversationConfig, ConversationMacro
4
+ from hexdag.builtin.macros.llm_macro import LLMMacro, LLMMacroConfig
5
+ from hexdag.builtin.macros.reasoning_agent import ReasoningAgentConfig, ReasoningAgentMacro
6
+ from hexdag.builtin.macros.tool_macro import ToolMacro, ToolMacroConfig
7
+
8
+ __all__ = [
9
+ "ConversationConfig",
10
+ "ConversationMacro",
11
+ "LLMMacro",
12
+ "LLMMacroConfig",
13
+ "ReasoningAgentConfig",
14
+ "ReasoningAgentMacro",
15
+ "ToolMacro",
16
+ "ToolMacroConfig",
17
+ ]