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
hexdag/core/secrets.py ADDED
@@ -0,0 +1,242 @@
1
+ """Simple secret resolution for adapters.
2
+
3
+ This module provides a clean way to declare secrets in adapter __init__ signatures
4
+ without complex Config classes. Secrets are resolved from environment variables
5
+ or Memory port automatically.
6
+
7
+ Example:
8
+ @adapter("llm", name="openai")
9
+ class OpenAIAdapter:
10
+ def __init__(
11
+ self,
12
+ api_key: str = secret(env="OPENAI_API_KEY"),
13
+ model: str = "gpt-4"
14
+ ):
15
+ self.api_key = api_key
16
+ self.model = model
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import inspect
22
+ import os
23
+ from dataclasses import dataclass
24
+ from typing import Any
25
+
26
+ from hexdag.core.logging import get_logger
27
+
28
+ logger = get_logger(__name__)
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class SecretDescriptor:
33
+ """Descriptor for a secret parameter.
34
+
35
+ This is used as a default value in __init__ to mark a parameter
36
+ as a secret that should be auto-resolved.
37
+
38
+ Attributes
39
+ ----------
40
+ env_var : str
41
+ Environment variable name to resolve from
42
+ memory_key : str | None
43
+ Key in Memory port (defaults to env_var with "secret:" prefix)
44
+ required : bool
45
+ Whether this secret is required
46
+ description : str
47
+ Human-readable description
48
+ """
49
+
50
+ env_var: str
51
+ memory_key: str | None = None
52
+ required: bool = True
53
+ description: str = ""
54
+
55
+ def resolve(self, memory: Any = None) -> str | None:
56
+ """Resolve secret value from environment or memory.
57
+
58
+ Resolution order:
59
+ 1. Environment variable
60
+ 2. Memory port (with "secret:" prefix)
61
+ 3. None (if not required)
62
+
63
+ Parameters
64
+ ----------
65
+ memory : Any, optional
66
+ Memory port instance to read from
67
+
68
+ Returns
69
+ -------
70
+ str | None
71
+ Resolved secret value or None
72
+
73
+ Raises
74
+ ------
75
+ ValueError
76
+ If secret is required but not found
77
+ """
78
+ # Try environment first
79
+ if value := os.getenv(self.env_var):
80
+ logger.debug(f"Resolved secret from env: {self.env_var}")
81
+ return value
82
+
83
+ # Try memory
84
+ if memory:
85
+ memory_key = self.memory_key or f"secret:{self.env_var}"
86
+ try:
87
+ if hasattr(memory, "get"):
88
+ value = memory.get(memory_key)
89
+ elif hasattr(memory, "aget"):
90
+ # Async memory - can't resolve here
91
+ logger.warning(
92
+ f"Memory port is async, cannot resolve {memory_key} in __init__. "
93
+ "Consider using environment variable."
94
+ )
95
+ else:
96
+ value = None
97
+
98
+ if value:
99
+ logger.debug(f"Resolved secret from memory: {memory_key}")
100
+ if hasattr(value, "get_secret_value"):
101
+ return value.get_secret_value() # type: ignore[no-any-return]
102
+ return str(value)
103
+ except Exception as e:
104
+ logger.debug(f"Failed to read from memory: {e}")
105
+
106
+ # Not found
107
+ if self.required:
108
+ raise ValueError(
109
+ f"Required secret '{self.env_var}' not found. "
110
+ f"Set environment variable {self.env_var} or provide in memory."
111
+ )
112
+
113
+ return None
114
+
115
+
116
+ def secret(
117
+ env: str, memory_key: str | None = None, required: bool = True, description: str = ""
118
+ ) -> SecretDescriptor:
119
+ """Mark a parameter as a secret that should be auto-resolved.
120
+
121
+ Use this as a default value in __init__ to declare secrets.
122
+ The @adapter decorator will automatically resolve these.
123
+
124
+ Parameters
125
+ ----------
126
+ env : str
127
+ Environment variable name (e.g., "OPENAI_API_KEY")
128
+ memory_key : str | None, optional
129
+ Alternative key in Memory port. If None, uses "secret:{env}"
130
+ required : bool, default=True
131
+ Whether this secret is required
132
+ description : str, default=""
133
+ Human-readable description for docs/CLI
134
+
135
+ Returns
136
+ -------
137
+ SecretDescriptor
138
+ Secret descriptor that will be resolved by @adapter decorator
139
+
140
+ Examples
141
+ --------
142
+ >>> @adapter("llm", name="openai") # doctest: +SKIP
143
+ ... class OpenAIAdapter:
144
+ ... def __init__(
145
+ ... self,
146
+ ... api_key: str = secret(env="OPENAI_API_KEY", description="OpenAI API key"),
147
+ ... model: str = "gpt-4"
148
+ ... ):
149
+ ... self.api_key = api_key
150
+ ... self.model = model
151
+ """
152
+ return SecretDescriptor(
153
+ env_var=env, memory_key=memory_key, required=required, description=description
154
+ )
155
+
156
+
157
+ def resolve_secrets_in_kwargs(
158
+ cls: type, kwargs: dict[str, Any], memory: Any = None
159
+ ) -> dict[str, Any]:
160
+ """Resolve secrets in kwargs based on __init__ signature.
161
+
162
+ Scans the __init__ signature for SecretDescriptor defaults
163
+ and resolves them from environment or memory.
164
+
165
+ Parameters
166
+ ----------
167
+ cls : type
168
+ Class to inspect
169
+ kwargs : dict[str, Any]
170
+ Keyword arguments passed to __init__
171
+ memory : Any, optional
172
+ Memory port for secret resolution
173
+
174
+ Returns
175
+ -------
176
+ dict[str, Any]
177
+ Updated kwargs with resolved secrets
178
+
179
+ Examples
180
+ --------
181
+ >>> kwargs = resolve_secrets_in_kwargs(OpenAIAdapter, {}, memory) # doctest: +SKIP
182
+ >>> # If OPENAI_API_KEY is set: kwargs = {"api_key": "sk-..."}
183
+ """
184
+ sig = inspect.signature(cls.__init__) # type: ignore[misc]
185
+
186
+ for param_name, param in sig.parameters.items():
187
+ if param_name in ("self", "cls"):
188
+ continue
189
+
190
+ if isinstance(param.default, SecretDescriptor):
191
+ secret_desc = param.default
192
+
193
+ # Skip if already provided in kwargs
194
+ if param_name in kwargs and kwargs[param_name] is not None:
195
+ logger.debug(f"Secret '{param_name}' provided explicitly")
196
+ continue
197
+
198
+ # Resolve the secret
199
+ try:
200
+ resolved_value = secret_desc.resolve(memory=memory)
201
+ if resolved_value is not None:
202
+ kwargs[param_name] = resolved_value
203
+ logger.debug(f"Resolved secret '{param_name}'")
204
+ except ValueError as e:
205
+ # Required secret not found
206
+ logger.error(f"Failed to resolve secret '{param_name}': {e}")
207
+ raise
208
+
209
+ return kwargs
210
+
211
+
212
+ def extract_secrets_from_signature(cls: type) -> dict[str, SecretDescriptor]:
213
+ """Extract all secret declarations from __init__ signature.
214
+
215
+ This is used by CLI to show which secrets are required.
216
+
217
+ Parameters
218
+ ----------
219
+ cls : type
220
+ Class to inspect
221
+
222
+ Returns
223
+ -------
224
+ dict[str, SecretDescriptor]
225
+ Mapping of parameter name to SecretDescriptor
226
+
227
+ Examples
228
+ --------
229
+ >>> secrets = extract_secrets_from_signature(OpenAIAdapter) # doctest: +SKIP
230
+ >>> # {"api_key": SecretDescriptor(env_var="OPENAI_API_KEY", ...)}
231
+ """
232
+ sig = inspect.signature(cls.__init__) # type: ignore[misc]
233
+ secrets = {}
234
+
235
+ for param_name, param in sig.parameters.items():
236
+ if param_name in ("self", "cls"):
237
+ continue
238
+
239
+ if isinstance(param.default, SecretDescriptor):
240
+ secrets[param_name] = param.default
241
+
242
+ return secrets
hexdag/core/types.py ADDED
@@ -0,0 +1,413 @@
1
+ """Central repository for reusable Annotated types.
2
+
3
+ This module provides type aliases with validation constraints that are used
4
+ throughout the hexDAG framework. Using Annotated types reduces boilerplate,
5
+ improves type safety, and provides better IDE support.
6
+
7
+ Type Categories
8
+ ---------------
9
+ - **Numeric Constraints**: PositiveInt, NonNegativeInt, PositiveFloat, etc.
10
+ - **LLM Parameters**: Temperature, TopP, Penalty, etc.
11
+ - **Time/Duration**: TimeoutSeconds, DelaySeconds
12
+ - **String Constraints**: NonEmptyStr, Identifier, QueryId
13
+ - **Domain Types**: TenantId, Confidence
14
+
15
+ Examples
16
+ --------
17
+ ```python
18
+ from hexdag.core.types import Temperature01, TimeoutSeconds
19
+
20
+ class Config(BaseModel):
21
+ temperature: Temperature01 = 0.7
22
+ timeout: TimeoutSeconds = 60.0
23
+ ```
24
+ """
25
+
26
+ from typing import Annotated, Any, get_args, get_origin
27
+
28
+ from pydantic import Field
29
+
30
+ # ============================================================================
31
+ # Numeric Constraints
32
+ # ============================================================================
33
+
34
+ PositiveInt = Annotated[int, Field(gt=0, description="Positive integer (> 0)")]
35
+ """Integer greater than 0."""
36
+
37
+ NonNegativeInt = Annotated[int, Field(ge=0, description="Non-negative integer (>= 0)")]
38
+ """Integer greater than or equal to 0."""
39
+
40
+ PositiveFloat = Annotated[float, Field(gt=0, description="Positive float (> 0)")]
41
+ """Float greater than 0."""
42
+
43
+ NonNegativeFloat = Annotated[float, Field(ge=0.0, description="Non-negative float (>= 0)")]
44
+ """Float greater than or equal to 0."""
45
+
46
+ # ============================================================================
47
+ # LLM Parameters
48
+ # ============================================================================
49
+
50
+ Temperature01 = Annotated[
51
+ float, Field(ge=0.0, le=1.0, description="Temperature sampling parameter [0.0, 1.0]")
52
+ ]
53
+ """LLM temperature parameter, range [0.0, 1.0]. Used by Anthropic/Claude."""
54
+
55
+ Temperature02 = Annotated[
56
+ float, Field(ge=0.0, le=2.0, description="Temperature sampling parameter [0.0, 2.0]")
57
+ ]
58
+ """LLM temperature parameter, range [0.0, 2.0]. Used by OpenAI."""
59
+
60
+ TopP = Annotated[float, Field(ge=0.0, le=1.0, description="Top-p (nucleus) sampling parameter")]
61
+ """Top-p sampling parameter, range [0.0, 1.0]."""
62
+
63
+ FrequencyPenalty = Annotated[
64
+ float, Field(ge=-2.0, le=2.0, description="Frequency penalty [-2.0, 2.0]")
65
+ ]
66
+ """Frequency penalty for token repetition, range [-2.0, 2.0]."""
67
+
68
+ PresencePenalty = Annotated[
69
+ float, Field(ge=-2.0, le=2.0, description="Presence penalty [-2.0, 2.0]")
70
+ ]
71
+ """Presence penalty for token diversity, range [-2.0, 2.0]."""
72
+
73
+ TokenCount = Annotated[int, Field(gt=0, description="Number of tokens")]
74
+ """Token count, must be positive."""
75
+
76
+ # ============================================================================
77
+ # Time and Duration
78
+ # ============================================================================
79
+
80
+ TimeoutSeconds = Annotated[float, Field(gt=0, description="Timeout duration in seconds")]
81
+ """Timeout duration in seconds, must be positive."""
82
+
83
+ DelaySeconds = Annotated[float, Field(ge=0.0, description="Delay duration in seconds")]
84
+ """Delay duration in seconds, non-negative."""
85
+
86
+ # ============================================================================
87
+ # Retry and Resilience
88
+ # ============================================================================
89
+
90
+ RetryCount = Annotated[int, Field(ge=0, description="Maximum retry attempts")]
91
+ """Maximum number of retry attempts, non-negative."""
92
+
93
+ # ============================================================================
94
+ # String Constraints
95
+ # ============================================================================
96
+
97
+ NonEmptyStr = Annotated[str, Field(min_length=1, max_length=255, description="Non-empty string")]
98
+ """Non-empty string with max length 255."""
99
+
100
+ Identifier = Annotated[
101
+ str,
102
+ Field(
103
+ pattern=r"^[a-zA-Z_][a-zA-Z0-9_]*$",
104
+ min_length=1,
105
+ max_length=100,
106
+ description="Valid identifier (Python-style)",
107
+ ),
108
+ ]
109
+ """Valid Python-style identifier."""
110
+
111
+ QueryId = Annotated[
112
+ str,
113
+ Field(
114
+ pattern=r"^[a-zA-Z0-9_-]+$",
115
+ min_length=1,
116
+ max_length=100,
117
+ description="Query identifier",
118
+ ),
119
+ ]
120
+ """Query identifier with alphanumeric, underscore, and hyphen characters."""
121
+
122
+ FilePath = Annotated[str, Field(min_length=1, description="File system path")]
123
+ """File system path, non-empty."""
124
+
125
+ # ============================================================================
126
+ # Domain-Specific Types
127
+ # ============================================================================
128
+
129
+ TenantId = Annotated[int, Field(gt=0, description="Tenant identifier")]
130
+ """Unique tenant identifier, positive integer."""
131
+
132
+ Confidence = Annotated[float, Field(ge=0.0, le=1.0, description="Confidence score [0.0, 1.0]")]
133
+ """Confidence score, range [0.0, 1.0]."""
134
+
135
+ Percentage = Annotated[float, Field(ge=0.0, le=100.0, description="Percentage [0.0, 100.0]")]
136
+ """Percentage value, range [0.0, 100.0]."""
137
+
138
+ # ============================================================================
139
+ # Framework Type Aliases (Python 3.12+ type statement)
140
+ # ============================================================================
141
+
142
+ # These use the modern `type` statement (PEP 695) instead of TypeAlias
143
+ # Note: Using Any here to avoid circular imports and external dependencies
144
+
145
+ # Logger type (loguru.Logger - using Any to avoid import)
146
+ type Logger = Any # loguru.Logger
147
+
148
+ # Port types (to be replaced with proper Protocol definitions)
149
+ type PortInstance = Any # Generic port implementation
150
+ type PortsDict = dict[str, PortInstance] # Dictionary of port name -> port instance
151
+
152
+ # ============================================================================
153
+ # Type Inspection Utilities
154
+ # ============================================================================
155
+
156
+
157
+ def is_literal_type(type_hint: Any) -> bool:
158
+ """Check if type hint is a Literal type.
159
+
160
+ Examples
161
+ --------
162
+ >>> from typing import Literal
163
+ >>> is_literal_type(Literal["a", "b"])
164
+ True
165
+ >>> is_literal_type(str)
166
+ False
167
+ """
168
+ try:
169
+ from typing import Literal
170
+
171
+ origin = get_origin(type_hint)
172
+ return origin is Literal
173
+ except ImportError:
174
+ return False
175
+
176
+
177
+ def is_union_type(type_hint: Any) -> bool:
178
+ """Check if type hint is a Union type (including | syntax).
179
+
180
+ Examples
181
+ --------
182
+ >>> from typing import Union
183
+ >>> is_union_type(Union[str, int])
184
+ True
185
+ >>> is_union_type(str | int)
186
+ True
187
+ >>> is_union_type(str)
188
+ False
189
+ """
190
+ origin = get_origin(type_hint)
191
+
192
+ try:
193
+ from typing import Union
194
+
195
+ if origin is Union:
196
+ return True
197
+ except ImportError:
198
+ pass
199
+
200
+ try:
201
+ from types import UnionType
202
+
203
+ if isinstance(type_hint, UnionType):
204
+ return True
205
+ except ImportError:
206
+ pass
207
+
208
+ return False
209
+
210
+
211
+ def is_list_type(type_hint: Any) -> bool:
212
+ """Check if type hint is a list type.
213
+
214
+ Examples
215
+ --------
216
+ >>> is_list_type(list[str])
217
+ True
218
+ >>> is_list_type(list)
219
+ True
220
+ >>> is_list_type(str)
221
+ False
222
+ """
223
+ origin = get_origin(type_hint)
224
+ return origin is list or type_hint is list
225
+
226
+
227
+ def is_dict_type(type_hint: Any) -> bool:
228
+ """Check if type hint is a dict type.
229
+
230
+ Examples
231
+ --------
232
+ >>> is_dict_type(dict[str, int])
233
+ True
234
+ >>> is_dict_type(dict)
235
+ True
236
+ >>> is_dict_type(str)
237
+ False
238
+ """
239
+ origin = get_origin(type_hint)
240
+ return origin is dict or type_hint is dict
241
+
242
+
243
+ def is_annotated_type(type_hint: Any) -> bool:
244
+ """Check if type hint is an Annotated type.
245
+
246
+ Examples
247
+ --------
248
+ >>> from typing import Annotated
249
+ >>> from pydantic import Field
250
+ >>> is_annotated_type(Annotated[int, Field(ge=0)])
251
+ True
252
+ >>> is_annotated_type(int)
253
+ False
254
+ """
255
+ try:
256
+ from typing import Annotated
257
+
258
+ origin = get_origin(type_hint)
259
+ return origin is Annotated
260
+ except ImportError:
261
+ return False
262
+
263
+
264
+ def get_annotated_metadata(type_hint: Any) -> tuple[Any, tuple[Any, ...]]:
265
+ """Extract base type and metadata from Annotated type.
266
+
267
+ Examples
268
+ --------
269
+ >>> from typing import Annotated
270
+ >>> from pydantic import Field
271
+ >>> base, metadata = get_annotated_metadata(Annotated[int, Field(ge=0)])
272
+ >>> base
273
+ <class 'int'>
274
+ """
275
+ args = get_args(type_hint)
276
+ if not args:
277
+ return type_hint, ()
278
+
279
+ base_type = args[0]
280
+ metadata = args[1:] if len(args) > 1 else ()
281
+
282
+ return base_type, metadata
283
+
284
+
285
+ # ============================================================================
286
+ # Secret Wrapper
287
+ # ============================================================================
288
+
289
+
290
+ class Secret:
291
+ """Minimal secret wrapper to avoid accidental str() in logs.
292
+
293
+ This class wraps sensitive string values to prevent them from being
294
+ accidentally logged or printed. It's used by the SecretPort interface
295
+ to return secret values in a safe manner.
296
+
297
+ The Secret class uses name mangling (double underscore) to make it
298
+ harder to accidentally access the raw value.
299
+
300
+ Examples
301
+ --------
302
+ >>> secret = Secret("my-api-key")
303
+ >>> print(secret) # Safe - won't print the value
304
+ <SECRET>
305
+ >>> str(secret) # Safe - won't convert to string
306
+ '<SECRET>'
307
+ >>> secret.get() # Explicit access required
308
+ 'my-api-key'
309
+
310
+ Usage in adapters::
311
+
312
+ # SecretPort implementation
313
+ async def aget_secret(self, key: str) -> Secret:
314
+ value = os.getenv(key)
315
+ return Secret(value)
316
+
317
+ # Using the secret
318
+ secret = await adapter.aget_secret("API_KEY")
319
+ print(secret) # <SECRET> (safe)
320
+ api_key = secret.get() # "sk-..." (explicit)
321
+ """
322
+
323
+ def __init__(self, value: str) -> None:
324
+ """Initialize secret with a value.
325
+
326
+ Parameters
327
+ ----------
328
+ value : str
329
+ The secret value to wrap
330
+ """
331
+ self.__value = value # Double underscore for name mangling
332
+
333
+ def get(self) -> str:
334
+ """Return the wrapped secret value securely.
335
+
336
+ This is the only way to access the actual secret value.
337
+ Requiring explicit .get() makes it clear when secrets are being accessed.
338
+
339
+ Returns
340
+ -------
341
+ str
342
+ The secret value
343
+ """
344
+ return self.__value
345
+
346
+ def __repr__(self) -> str:
347
+ """Return a safe string representation for debugging.
348
+
349
+ Returns
350
+ -------
351
+ str
352
+ A safe string representation "<SECRET>"
353
+ """
354
+ return "<SECRET>"
355
+
356
+ def __str__(self) -> str:
357
+ """Return a safe string representation for display.
358
+
359
+ Returns
360
+ -------
361
+ str
362
+ A safe string representation "<SECRET>"
363
+ """
364
+ return "<SECRET>"
365
+
366
+
367
+ # ============================================================================
368
+ # Backward Compatibility (deprecated, use specific types above)
369
+ # ============================================================================
370
+
371
+ # These were originally defined in agent_factory/models.py
372
+ # Kept here for backward compatibility but prefer the specific types above
373
+ __all__ = [
374
+ # Numeric
375
+ "PositiveInt",
376
+ "NonNegativeInt",
377
+ "PositiveFloat",
378
+ "NonNegativeFloat",
379
+ # LLM
380
+ "Temperature01",
381
+ "Temperature02",
382
+ "TopP",
383
+ "FrequencyPenalty",
384
+ "PresencePenalty",
385
+ "TokenCount",
386
+ # Time
387
+ "TimeoutSeconds",
388
+ "DelaySeconds",
389
+ # Retry
390
+ "RetryCount",
391
+ # String
392
+ "NonEmptyStr",
393
+ "Identifier",
394
+ "QueryId",
395
+ "FilePath",
396
+ # Domain
397
+ "TenantId",
398
+ "Confidence",
399
+ "Percentage",
400
+ # Secret wrapper
401
+ "Secret",
402
+ # Framework types
403
+ "Logger",
404
+ "PortInstance",
405
+ "PortsDict",
406
+ # Type inspection utilities
407
+ "is_literal_type",
408
+ "is_union_type",
409
+ "is_list_type",
410
+ "is_dict_type",
411
+ "is_annotated_type",
412
+ "get_annotated_metadata",
413
+ ]