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,383 @@
1
+ """Azure Cosmos DB adapter for hexDAG framework.
2
+
3
+ Provides persistent memory and state storage for agents and pipelines.
4
+ """
5
+
6
+ import os
7
+ import time
8
+ from typing import Any
9
+
10
+ from hexdag.core.ports.healthcheck import HealthStatus
11
+ from hexdag.core.ports.memory import Memory
12
+
13
+
14
+ class AzureCosmosAdapter(Memory):
15
+ """Azure Cosmos DB adapter for agent memory and pipeline state.
16
+
17
+ Provides persistent, scalable storage for agent memory, conversation
18
+ history, and pipeline execution state using Azure Cosmos DB.
19
+
20
+ Parameters
21
+ ----------
22
+ endpoint : str
23
+ Azure Cosmos DB endpoint URL
24
+ key : str
25
+ Azure Cosmos DB primary or secondary key (auto-resolved from AZURE_COSMOS_KEY)
26
+ database_name : str
27
+ Database name (default: "hexdag")
28
+ container_name : str
29
+ Container name (default: "memory")
30
+ partition_key : str
31
+ Partition key path (default: "/agent_id")
32
+ use_managed_identity : bool
33
+ Use Managed Identity instead of key auth (default: False)
34
+ throughput : int
35
+ Container throughput in RU/s (default: 400)
36
+
37
+ Examples
38
+ --------
39
+ YAML configuration::
40
+
41
+ spec:
42
+ ports:
43
+ memory:
44
+ adapter: hexdag_plugins.azure.AzureCosmosAdapter
45
+ config:
46
+ endpoint: ${AZURE_COSMOS_ENDPOINT}
47
+ database_name: "hexdag"
48
+ container_name: "agent_memory"
49
+
50
+ Python usage::
51
+
52
+ from hexdag_plugins.azure import AzureCosmosAdapter
53
+
54
+ adapter = AzureCosmosAdapter(
55
+ endpoint="https://my-cosmos.documents.azure.com:443/",
56
+ key="...", # or auto-resolved from AZURE_COSMOS_KEY
57
+ database_name="hexdag",
58
+ container_name="agent_memory"
59
+ )
60
+
61
+ # Store agent memory
62
+ await adapter.astore("agent-123", {"context": "...", "history": [...]})
63
+
64
+ # Retrieve memory
65
+ memory = await adapter.aretrieve("agent-123")
66
+
67
+ # Search memories
68
+ results = await adapter.asearch("user query", top_k=5)
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ endpoint: str,
74
+ key: str | None = None,
75
+ database_name: str = "hexdag",
76
+ container_name: str = "memory",
77
+ partition_key: str = "/agent_id",
78
+ use_managed_identity: bool = False,
79
+ throughput: int = 400,
80
+ ):
81
+ """Initialize Azure Cosmos DB adapter.
82
+
83
+ Args
84
+ ----
85
+ endpoint: Azure Cosmos DB endpoint URL
86
+ key: Cosmos DB key (or use managed identity)
87
+ database_name: Database name (default: "hexdag")
88
+ container_name: Container name (default: "memory")
89
+ partition_key: Partition key path (default: "/agent_id")
90
+ use_managed_identity: Use Managed Identity (default: False)
91
+ throughput: Container throughput RU/s (default: 400)
92
+ """
93
+ self.endpoint = endpoint
94
+ self.key = key or os.getenv("AZURE_COSMOS_KEY")
95
+ self.database_name = database_name
96
+ self.container_name = container_name
97
+ self.partition_key = partition_key
98
+ self.use_managed_identity = use_managed_identity
99
+ self.throughput = throughput
100
+
101
+ self._client = None
102
+ self._container = None
103
+
104
+ async def _get_container(self):
105
+ """Get or create Cosmos DB container."""
106
+ if self._container is None:
107
+ try:
108
+ from azure.cosmos import PartitionKey
109
+ from azure.cosmos.aio import CosmosClient
110
+ except ImportError as e:
111
+ raise ImportError(
112
+ "Azure Cosmos SDK not installed. Install with: pip install azure-cosmos"
113
+ ) from e
114
+
115
+ if self.use_managed_identity:
116
+ try:
117
+ from azure.identity.aio import DefaultAzureCredential
118
+
119
+ credential = DefaultAzureCredential()
120
+ self._client = CosmosClient(self.endpoint, credential=credential)
121
+ except ImportError as e:
122
+ raise ImportError(
123
+ "Azure Identity SDK not installed. Install with: pip install azure-identity"
124
+ ) from e
125
+ else:
126
+ if not self.key:
127
+ raise ValueError("key is required when use_managed_identity=False")
128
+ self._client = CosmosClient(self.endpoint, credential=self.key)
129
+
130
+ # Create database if not exists
131
+ database = await self._client.create_database_if_not_exists(id=self.database_name)
132
+
133
+ # Create container if not exists
134
+ self._container = await database.create_container_if_not_exists(
135
+ id=self.container_name,
136
+ partition_key=PartitionKey(path=self.partition_key),
137
+ offer_throughput=self.throughput,
138
+ )
139
+
140
+ return self._container
141
+
142
+ async def astore(
143
+ self,
144
+ key: str,
145
+ value: dict[str, Any],
146
+ metadata: dict[str, Any] | None = None,
147
+ ) -> None:
148
+ """Store data in Cosmos DB.
149
+
150
+ Args
151
+ ----
152
+ key: Unique identifier for the data
153
+ value: Data to store
154
+ metadata: Optional metadata
155
+ """
156
+ container = await self._get_container()
157
+
158
+ # Extract agent_id from key for partitioning
159
+ agent_id = key.split(":")[0] if ":" in key else key
160
+
161
+ document = {
162
+ "id": key,
163
+ "agent_id": agent_id,
164
+ "data": value,
165
+ "metadata": metadata or {},
166
+ "created_at": time.time(),
167
+ "updated_at": time.time(),
168
+ }
169
+
170
+ await container.upsert_item(document)
171
+
172
+ async def aretrieve(self, key: str) -> dict[str, Any] | None:
173
+ """Retrieve data from Cosmos DB.
174
+
175
+ Args
176
+ ----
177
+ key: Unique identifier for the data
178
+
179
+ Returns
180
+ -------
181
+ Stored data or None if not found
182
+ """
183
+ container = await self._get_container()
184
+ agent_id = key.split(":")[0] if ":" in key else key
185
+
186
+ try:
187
+ item = await container.read_item(item=key, partition_key=agent_id)
188
+ return item.get("data")
189
+ except Exception:
190
+ return None
191
+
192
+ async def adelete(self, key: str) -> bool:
193
+ """Delete data from Cosmos DB.
194
+
195
+ Args
196
+ ----
197
+ key: Unique identifier for the data
198
+
199
+ Returns
200
+ -------
201
+ True if deleted, False if not found
202
+ """
203
+ container = await self._get_container()
204
+ agent_id = key.split(":")[0] if ":" in key else key
205
+
206
+ try:
207
+ await container.delete_item(item=key, partition_key=agent_id)
208
+ return True
209
+ except Exception:
210
+ return False
211
+
212
+ async def alist(self, prefix: str | None = None) -> list[str]:
213
+ """List all keys in memory.
214
+
215
+ Args
216
+ ----
217
+ prefix: Optional prefix to filter keys
218
+
219
+ Returns
220
+ -------
221
+ List of keys
222
+ """
223
+ container = await self._get_container()
224
+
225
+ if prefix:
226
+ query = f"SELECT c.id FROM c WHERE STARTSWITH(c.id, '{prefix}')"
227
+ else:
228
+ query = "SELECT c.id FROM c"
229
+
230
+ items = container.query_items(query=query, enable_cross_partition_query=True)
231
+ return [item["id"] async for item in items]
232
+
233
+ async def asearch(
234
+ self,
235
+ query: str,
236
+ top_k: int = 5,
237
+ filter_metadata: dict[str, Any] | None = None,
238
+ ) -> list[dict[str, Any]]:
239
+ """Search memories by content.
240
+
241
+ Note: This performs a simple text search. For vector similarity search,
242
+ consider using Azure Cognitive Search integration.
243
+
244
+ Args
245
+ ----
246
+ query: Search query string
247
+ top_k: Maximum number of results
248
+ filter_metadata: Optional metadata filters
249
+
250
+ Returns
251
+ -------
252
+ List of matching documents
253
+ """
254
+ container = await self._get_container()
255
+
256
+ # Build SQL query with CONTAINS for text search
257
+ sql_query = f"""
258
+ SELECT TOP {top_k} c.id, c.data, c.metadata, c.created_at
259
+ FROM c
260
+ WHERE CONTAINS(LOWER(c.data), LOWER('{query}'))
261
+ """
262
+
263
+ if filter_metadata:
264
+ for key, value in filter_metadata.items():
265
+ sql_query += f" AND c.metadata.{key} = '{value}'"
266
+
267
+ sql_query += " ORDER BY c.created_at DESC"
268
+
269
+ items = container.query_items(query=sql_query, enable_cross_partition_query=True)
270
+ return [item async for item in items]
271
+
272
+ async def astore_conversation(
273
+ self,
274
+ agent_id: str,
275
+ messages: list[dict[str, str]],
276
+ session_id: str | None = None,
277
+ ) -> None:
278
+ """Store conversation history.
279
+
280
+ Args
281
+ ----
282
+ agent_id: Agent identifier
283
+ messages: List of message dicts with 'role' and 'content'
284
+ session_id: Optional session identifier
285
+ """
286
+ key = f"{agent_id}:conversation:{session_id or 'default'}"
287
+ await self.astore(key, {"messages": messages}, {"type": "conversation"})
288
+
289
+ async def aretrieve_conversation(
290
+ self,
291
+ agent_id: str,
292
+ session_id: str | None = None,
293
+ ) -> list[dict[str, str]]:
294
+ """Retrieve conversation history.
295
+
296
+ Args
297
+ ----
298
+ agent_id: Agent identifier
299
+ session_id: Optional session identifier
300
+
301
+ Returns
302
+ -------
303
+ List of messages
304
+ """
305
+ key = f"{agent_id}:conversation:{session_id or 'default'}"
306
+ data = await self.aretrieve(key)
307
+ return data.get("messages", []) if data else []
308
+
309
+ async def aclear_agent(self, agent_id: str) -> int:
310
+ """Clear all data for an agent.
311
+
312
+ Args
313
+ ----
314
+ agent_id: Agent identifier
315
+
316
+ Returns
317
+ -------
318
+ Number of items deleted
319
+ """
320
+ keys = await self.alist(prefix=agent_id)
321
+ count = 0
322
+ for key in keys:
323
+ if await self.adelete(key):
324
+ count += 1
325
+ return count
326
+
327
+ async def ahealth_check(self) -> HealthStatus:
328
+ """Check Azure Cosmos DB connectivity.
329
+
330
+ Returns
331
+ -------
332
+ HealthStatus with connectivity details
333
+ """
334
+ try:
335
+ start_time = time.time()
336
+ container = await self._get_container()
337
+
338
+ # Simple query to verify connectivity
339
+ query = "SELECT VALUE COUNT(1) FROM c"
340
+ items = container.query_items(query=query, enable_cross_partition_query=True)
341
+ count = 0
342
+ async for item in items:
343
+ count = item
344
+
345
+ latency_ms = (time.time() - start_time) * 1000
346
+
347
+ return HealthStatus(
348
+ status="healthy",
349
+ adapter_name="AzureCosmos",
350
+ latency_ms=latency_ms,
351
+ details={
352
+ "endpoint": self.endpoint,
353
+ "database": self.database_name,
354
+ "container": self.container_name,
355
+ "document_count": count,
356
+ },
357
+ )
358
+
359
+ except Exception as e:
360
+ return HealthStatus(
361
+ status="unhealthy",
362
+ adapter_name="AzureCosmos",
363
+ latency_ms=0.0,
364
+ details={"error": str(e)},
365
+ )
366
+
367
+ async def aclose(self) -> None:
368
+ """Close the Cosmos DB client."""
369
+ if self._client:
370
+ await self._client.close()
371
+ self._client = None
372
+ self._container = None
373
+
374
+ def to_dict(self) -> dict[str, Any]:
375
+ """Serialize adapter configuration (excluding secrets)."""
376
+ return {
377
+ "endpoint": self.endpoint,
378
+ "database_name": self.database_name,
379
+ "container_name": self.container_name,
380
+ "partition_key": self.partition_key,
381
+ "use_managed_identity": self.use_managed_identity,
382
+ "throughput": self.throughput,
383
+ }
@@ -0,0 +1,314 @@
1
+ """Azure Key Vault adapter for hexDAG framework.
2
+
3
+ Provides secret resolution from Azure Key Vault for production deployments.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from hexdag.core.ports.healthcheck import HealthStatus
9
+
10
+
11
+ class AzureKeyVaultAdapter:
12
+ """Azure Key Vault adapter for secret resolution.
13
+
14
+ Supports both API key authentication and Azure Managed Identity for
15
+ secure, credential-free access in Azure environments.
16
+
17
+ Parameters
18
+ ----------
19
+ vault_url : str
20
+ Azure Key Vault URL (e.g., "https://my-vault.vault.azure.net")
21
+ use_managed_identity : bool, optional
22
+ Use Azure Managed Identity instead of explicit credentials (default: True)
23
+ tenant_id : str, optional
24
+ Azure AD tenant ID (only needed if use_managed_identity=False)
25
+ client_id : str, optional
26
+ Azure AD client ID (only needed if use_managed_identity=False)
27
+ client_secret : str, optional
28
+ Azure AD client secret (only needed if use_managed_identity=False)
29
+ cache_secrets : bool, optional
30
+ Cache retrieved secrets in memory (default: True)
31
+ cache_ttl : int, optional
32
+ Cache TTL in seconds (default: 300)
33
+
34
+ Examples
35
+ --------
36
+ YAML configuration with managed identity::
37
+
38
+ spec:
39
+ ports:
40
+ secret:
41
+ adapter: hexdag_plugins.azure.AzureKeyVaultAdapter
42
+ config:
43
+ vault_url: "https://my-vault.vault.azure.net"
44
+ use_managed_identity: true
45
+
46
+ YAML configuration with service principal::
47
+
48
+ spec:
49
+ ports:
50
+ secret:
51
+ adapter: hexdag_plugins.azure.AzureKeyVaultAdapter
52
+ config:
53
+ vault_url: "https://my-vault.vault.azure.net"
54
+ use_managed_identity: false
55
+ tenant_id: ${AZURE_TENANT_ID}
56
+ client_id: ${AZURE_CLIENT_ID}
57
+ client_secret: ${AZURE_CLIENT_SECRET}
58
+
59
+ Python usage::
60
+
61
+ from hexdag_plugins.azure import AzureKeyVaultAdapter
62
+
63
+ # With managed identity (recommended for Azure deployments)
64
+ adapter = AzureKeyVaultAdapter(
65
+ vault_url="https://my-vault.vault.azure.net",
66
+ use_managed_identity=True
67
+ )
68
+
69
+ # Retrieve secrets
70
+ api_key = await adapter.aget("OPENAI-API-KEY")
71
+ db_password = await adapter.aget("DB-PASSWORD")
72
+
73
+ # Batch retrieval
74
+ secrets = await adapter.aget_batch(["SECRET1", "SECRET2"])
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ vault_url: str,
80
+ use_managed_identity: bool = True,
81
+ tenant_id: str | None = None,
82
+ client_id: str | None = None,
83
+ client_secret: str | None = None,
84
+ cache_secrets: bool = True,
85
+ cache_ttl: int = 300,
86
+ ):
87
+ """Initialize Azure Key Vault adapter.
88
+
89
+ Args
90
+ ----
91
+ vault_url: Azure Key Vault URL
92
+ use_managed_identity: Use Managed Identity for auth (default: True)
93
+ tenant_id: Azure AD tenant ID (for service principal auth)
94
+ client_id: Azure AD client ID (for service principal auth)
95
+ client_secret: Azure AD client secret (for service principal auth)
96
+ cache_secrets: Cache retrieved secrets (default: True)
97
+ cache_ttl: Cache TTL in seconds (default: 300)
98
+ """
99
+ self.vault_url = vault_url
100
+ self.use_managed_identity = use_managed_identity
101
+ self.tenant_id = tenant_id
102
+ self.client_id = client_id
103
+ self.client_secret = client_secret
104
+ self.cache_secrets = cache_secrets
105
+ self.cache_ttl = cache_ttl
106
+
107
+ self._client = None
108
+ self._cache: dict[str, tuple[str, float]] = {}
109
+
110
+ def _get_client(self):
111
+ """Get or create Key Vault client."""
112
+ if self._client is None:
113
+ try:
114
+ from azure.identity import (
115
+ ClientSecretCredential,
116
+ DefaultAzureCredential,
117
+ )
118
+ from azure.keyvault.secrets import SecretClient
119
+ except ImportError as e:
120
+ raise ImportError(
121
+ "Azure SDK not installed. Install with: "
122
+ "pip install azure-identity azure-keyvault-secrets"
123
+ ) from e
124
+
125
+ if self.use_managed_identity:
126
+ credential = DefaultAzureCredential()
127
+ else:
128
+ if not all([self.tenant_id, self.client_id, self.client_secret]):
129
+ raise ValueError(
130
+ "tenant_id, client_id, and client_secret are required "
131
+ "when use_managed_identity=False"
132
+ )
133
+ credential = ClientSecretCredential(
134
+ tenant_id=self.tenant_id,
135
+ client_id=self.client_id,
136
+ client_secret=self.client_secret,
137
+ )
138
+
139
+ self._client = SecretClient(vault_url=self.vault_url, credential=credential)
140
+
141
+ return self._client
142
+
143
+ def _get_from_cache(self, secret_name: str) -> str | None:
144
+ """Get secret from cache if valid."""
145
+ if not self.cache_secrets:
146
+ return None
147
+
148
+ import time
149
+
150
+ if secret_name in self._cache:
151
+ value, timestamp = self._cache[secret_name]
152
+ if time.time() - timestamp < self.cache_ttl:
153
+ return value
154
+ del self._cache[secret_name]
155
+
156
+ return None
157
+
158
+ def _set_cache(self, secret_name: str, value: str) -> None:
159
+ """Set secret in cache."""
160
+ if self.cache_secrets:
161
+ import time
162
+
163
+ self._cache[secret_name] = (value, time.time())
164
+
165
+ async def aget(self, secret_name: str) -> str:
166
+ """Retrieve a secret from Azure Key Vault.
167
+
168
+ Args
169
+ ----
170
+ secret_name: Name of the secret in Key Vault
171
+
172
+ Returns
173
+ -------
174
+ Secret value as string
175
+
176
+ Raises
177
+ ------
178
+ ValueError: If secret not found
179
+ RuntimeError: If Key Vault access fails
180
+ """
181
+ # Check cache first
182
+ cached = self._get_from_cache(secret_name)
183
+ if cached is not None:
184
+ return cached
185
+
186
+ try:
187
+ client = self._get_client()
188
+ secret = client.get_secret(secret_name)
189
+
190
+ if secret.value is None:
191
+ raise ValueError(f"Secret '{secret_name}' has no value")
192
+
193
+ self._set_cache(secret_name, secret.value)
194
+ return secret.value
195
+
196
+ except Exception as e:
197
+ if "SecretNotFound" in str(e):
198
+ raise ValueError(f"Secret '{secret_name}' not found in Key Vault") from e
199
+ raise RuntimeError(f"Failed to retrieve secret '{secret_name}': {e}") from e
200
+
201
+ async def aget_batch(self, secret_names: list[str]) -> dict[str, str]:
202
+ """Retrieve multiple secrets from Azure Key Vault.
203
+
204
+ Args
205
+ ----
206
+ secret_names: List of secret names to retrieve
207
+
208
+ Returns
209
+ -------
210
+ Dictionary mapping secret names to values
211
+ """
212
+ results = {}
213
+ for name in secret_names:
214
+ try:
215
+ results[name] = await self.aget(name)
216
+ except ValueError: # noqa: SIM105 - contextlib.suppress doesn't work with async
217
+ # Skip secrets that don't exist
218
+ pass
219
+ return results
220
+
221
+ async def aset(self, secret_name: str, value: str) -> None:
222
+ """Set a secret in Azure Key Vault.
223
+
224
+ Args
225
+ ----
226
+ secret_name: Name of the secret
227
+ value: Secret value to store
228
+ """
229
+ try:
230
+ client = self._get_client()
231
+ client.set_secret(secret_name, value)
232
+ self._set_cache(secret_name, value)
233
+ except Exception as e:
234
+ raise RuntimeError(f"Failed to set secret '{secret_name}': {e}") from e
235
+
236
+ async def adelete(self, secret_name: str) -> None:
237
+ """Delete a secret from Azure Key Vault.
238
+
239
+ Args
240
+ ----
241
+ secret_name: Name of the secret to delete
242
+ """
243
+ try:
244
+ client = self._get_client()
245
+ client.begin_delete_secret(secret_name)
246
+ if secret_name in self._cache:
247
+ del self._cache[secret_name]
248
+ except Exception as e:
249
+ raise RuntimeError(f"Failed to delete secret '{secret_name}': {e}") from e
250
+
251
+ async def alist(self) -> list[str]:
252
+ """List all secret names in the Key Vault.
253
+
254
+ Returns
255
+ -------
256
+ List of secret names
257
+ """
258
+ try:
259
+ client = self._get_client()
260
+ return [secret.name for secret in client.list_properties_of_secrets()]
261
+ except Exception as e:
262
+ raise RuntimeError(f"Failed to list secrets: {e}") from e
263
+
264
+ def clear_cache(self) -> None:
265
+ """Clear the secret cache."""
266
+ self._cache.clear()
267
+
268
+ async def ahealth_check(self) -> HealthStatus:
269
+ """Check Azure Key Vault connectivity.
270
+
271
+ Returns
272
+ -------
273
+ HealthStatus with connectivity details
274
+ """
275
+ import time
276
+
277
+ try:
278
+ start_time = time.time()
279
+ client = self._get_client()
280
+
281
+ # Try to list secrets (limited to 1) to verify connectivity
282
+ list(client.list_properties_of_secrets())
283
+
284
+ latency_ms = (time.time() - start_time) * 1000
285
+
286
+ return HealthStatus(
287
+ status="healthy",
288
+ adapter_name="AzureKeyVault",
289
+ latency_ms=latency_ms,
290
+ details={
291
+ "vault_url": self.vault_url,
292
+ "auth_method": "managed_identity"
293
+ if self.use_managed_identity
294
+ else "service_principal",
295
+ "cache_enabled": self.cache_secrets,
296
+ },
297
+ )
298
+
299
+ except Exception as e:
300
+ return HealthStatus(
301
+ status="unhealthy",
302
+ adapter_name="AzureKeyVault",
303
+ latency_ms=0.0,
304
+ details={"error": str(e), "vault_url": self.vault_url},
305
+ )
306
+
307
+ def to_dict(self) -> dict[str, Any]:
308
+ """Serialize adapter configuration (excluding secrets)."""
309
+ return {
310
+ "vault_url": self.vault_url,
311
+ "use_managed_identity": self.use_managed_identity,
312
+ "cache_secrets": self.cache_secrets,
313
+ "cache_ttl": self.cache_ttl,
314
+ }