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.
- hexdag/__init__.py +116 -0
- hexdag/__main__.py +30 -0
- hexdag/adapters/executors/__init__.py +5 -0
- hexdag/adapters/executors/local_executor.py +316 -0
- hexdag/builtin/__init__.py +6 -0
- hexdag/builtin/adapters/__init__.py +51 -0
- hexdag/builtin/adapters/anthropic/__init__.py +5 -0
- hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
- hexdag/builtin/adapters/database/__init__.py +6 -0
- hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
- hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
- hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
- hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
- hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
- hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
- hexdag/builtin/adapters/local/README.md +59 -0
- hexdag/builtin/adapters/local/__init__.py +7 -0
- hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
- hexdag/builtin/adapters/memory/__init__.py +47 -0
- hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
- hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
- hexdag/builtin/adapters/memory/schemas.py +57 -0
- hexdag/builtin/adapters/memory/session_memory.py +178 -0
- hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
- hexdag/builtin/adapters/memory/state_memory.py +280 -0
- hexdag/builtin/adapters/mock/README.md +89 -0
- hexdag/builtin/adapters/mock/__init__.py +15 -0
- hexdag/builtin/adapters/mock/hexdag.toml +50 -0
- hexdag/builtin/adapters/mock/mock_database.py +225 -0
- hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
- hexdag/builtin/adapters/mock/mock_llm.py +177 -0
- hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
- hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
- hexdag/builtin/adapters/openai/__init__.py +5 -0
- hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
- hexdag/builtin/adapters/secret/__init__.py +7 -0
- hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
- hexdag/builtin/adapters/unified_tool_router.py +280 -0
- hexdag/builtin/macros/__init__.py +17 -0
- hexdag/builtin/macros/conversation_agent.py +390 -0
- hexdag/builtin/macros/llm_macro.py +151 -0
- hexdag/builtin/macros/reasoning_agent.py +423 -0
- hexdag/builtin/macros/tool_macro.py +380 -0
- hexdag/builtin/nodes/__init__.py +38 -0
- hexdag/builtin/nodes/_discovery.py +123 -0
- hexdag/builtin/nodes/agent_node.py +696 -0
- hexdag/builtin/nodes/base_node_factory.py +242 -0
- hexdag/builtin/nodes/composite_node.py +926 -0
- hexdag/builtin/nodes/data_node.py +201 -0
- hexdag/builtin/nodes/expression_node.py +487 -0
- hexdag/builtin/nodes/function_node.py +454 -0
- hexdag/builtin/nodes/llm_node.py +491 -0
- hexdag/builtin/nodes/loop_node.py +920 -0
- hexdag/builtin/nodes/mapped_input.py +518 -0
- hexdag/builtin/nodes/port_call_node.py +269 -0
- hexdag/builtin/nodes/tool_call_node.py +195 -0
- hexdag/builtin/nodes/tool_utils.py +390 -0
- hexdag/builtin/prompts/__init__.py +68 -0
- hexdag/builtin/prompts/base.py +422 -0
- hexdag/builtin/prompts/chat_prompts.py +303 -0
- hexdag/builtin/prompts/error_correction_prompts.py +320 -0
- hexdag/builtin/prompts/tool_prompts.py +160 -0
- hexdag/builtin/tools/builtin_tools.py +84 -0
- hexdag/builtin/tools/database_tools.py +164 -0
- hexdag/cli/__init__.py +17 -0
- hexdag/cli/__main__.py +7 -0
- hexdag/cli/commands/__init__.py +27 -0
- hexdag/cli/commands/build_cmd.py +812 -0
- hexdag/cli/commands/create_cmd.py +208 -0
- hexdag/cli/commands/docs_cmd.py +293 -0
- hexdag/cli/commands/generate_types_cmd.py +252 -0
- hexdag/cli/commands/init_cmd.py +188 -0
- hexdag/cli/commands/pipeline_cmd.py +494 -0
- hexdag/cli/commands/plugin_dev_cmd.py +529 -0
- hexdag/cli/commands/plugins_cmd.py +441 -0
- hexdag/cli/commands/studio_cmd.py +101 -0
- hexdag/cli/commands/validate_cmd.py +221 -0
- hexdag/cli/main.py +84 -0
- hexdag/core/__init__.py +83 -0
- hexdag/core/config/__init__.py +20 -0
- hexdag/core/config/loader.py +479 -0
- hexdag/core/config/models.py +150 -0
- hexdag/core/configurable.py +294 -0
- hexdag/core/context/__init__.py +37 -0
- hexdag/core/context/execution_context.py +378 -0
- hexdag/core/docs/__init__.py +26 -0
- hexdag/core/docs/extractors.py +678 -0
- hexdag/core/docs/generators.py +890 -0
- hexdag/core/docs/models.py +120 -0
- hexdag/core/domain/__init__.py +10 -0
- hexdag/core/domain/dag.py +1225 -0
- hexdag/core/exceptions.py +234 -0
- hexdag/core/expression_parser.py +569 -0
- hexdag/core/logging.py +449 -0
- hexdag/core/models/__init__.py +17 -0
- hexdag/core/models/base.py +138 -0
- hexdag/core/orchestration/__init__.py +46 -0
- hexdag/core/orchestration/body_executor.py +481 -0
- hexdag/core/orchestration/components/__init__.py +97 -0
- hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
- hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
- hexdag/core/orchestration/components/execution_coordinator.py +360 -0
- hexdag/core/orchestration/components/health_check_manager.py +176 -0
- hexdag/core/orchestration/components/input_mapper.py +143 -0
- hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
- hexdag/core/orchestration/components/node_executor.py +377 -0
- hexdag/core/orchestration/components/secret_manager.py +202 -0
- hexdag/core/orchestration/components/wave_executor.py +158 -0
- hexdag/core/orchestration/constants.py +17 -0
- hexdag/core/orchestration/events/README.md +312 -0
- hexdag/core/orchestration/events/__init__.py +104 -0
- hexdag/core/orchestration/events/batching.py +330 -0
- hexdag/core/orchestration/events/decorators.py +139 -0
- hexdag/core/orchestration/events/events.py +573 -0
- hexdag/core/orchestration/events/observers/__init__.py +30 -0
- hexdag/core/orchestration/events/observers/core_observers.py +690 -0
- hexdag/core/orchestration/events/observers/models.py +111 -0
- hexdag/core/orchestration/events/taxonomy.py +269 -0
- hexdag/core/orchestration/hook_context.py +237 -0
- hexdag/core/orchestration/hooks.py +437 -0
- hexdag/core/orchestration/models.py +418 -0
- hexdag/core/orchestration/orchestrator.py +910 -0
- hexdag/core/orchestration/orchestrator_factory.py +275 -0
- hexdag/core/orchestration/port_wrappers.py +327 -0
- hexdag/core/orchestration/prompt/__init__.py +32 -0
- hexdag/core/orchestration/prompt/template.py +332 -0
- hexdag/core/pipeline_builder/__init__.py +21 -0
- hexdag/core/pipeline_builder/component_instantiator.py +386 -0
- hexdag/core/pipeline_builder/include_tag.py +265 -0
- hexdag/core/pipeline_builder/pipeline_config.py +133 -0
- hexdag/core/pipeline_builder/py_tag.py +223 -0
- hexdag/core/pipeline_builder/tag_discovery.py +268 -0
- hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
- hexdag/core/pipeline_builder/yaml_validator.py +569 -0
- hexdag/core/ports/__init__.py +65 -0
- hexdag/core/ports/api_call.py +133 -0
- hexdag/core/ports/database.py +489 -0
- hexdag/core/ports/embedding.py +215 -0
- hexdag/core/ports/executor.py +237 -0
- hexdag/core/ports/file_storage.py +117 -0
- hexdag/core/ports/healthcheck.py +87 -0
- hexdag/core/ports/llm.py +551 -0
- hexdag/core/ports/memory.py +70 -0
- hexdag/core/ports/observer_manager.py +130 -0
- hexdag/core/ports/secret.py +145 -0
- hexdag/core/ports/tool_router.py +94 -0
- hexdag/core/ports_builder.py +623 -0
- hexdag/core/protocols.py +273 -0
- hexdag/core/resolver.py +304 -0
- hexdag/core/schema/__init__.py +9 -0
- hexdag/core/schema/generator.py +742 -0
- hexdag/core/secrets.py +242 -0
- hexdag/core/types.py +413 -0
- hexdag/core/utils/async_warnings.py +206 -0
- hexdag/core/utils/schema_conversion.py +78 -0
- hexdag/core/utils/sql_validation.py +86 -0
- hexdag/core/validation/secure_json.py +148 -0
- hexdag/core/yaml_macro.py +517 -0
- hexdag/mcp_server.py +3120 -0
- hexdag/studio/__init__.py +10 -0
- hexdag/studio/build_ui.py +92 -0
- hexdag/studio/server/__init__.py +1 -0
- hexdag/studio/server/main.py +100 -0
- hexdag/studio/server/routes/__init__.py +9 -0
- hexdag/studio/server/routes/execute.py +208 -0
- hexdag/studio/server/routes/export.py +558 -0
- hexdag/studio/server/routes/files.py +207 -0
- hexdag/studio/server/routes/plugins.py +419 -0
- hexdag/studio/server/routes/validate.py +220 -0
- hexdag/studio/ui/index.html +13 -0
- hexdag/studio/ui/package-lock.json +2992 -0
- hexdag/studio/ui/package.json +31 -0
- hexdag/studio/ui/postcss.config.js +6 -0
- hexdag/studio/ui/public/hexdag.svg +5 -0
- hexdag/studio/ui/src/App.tsx +251 -0
- hexdag/studio/ui/src/components/Canvas.tsx +408 -0
- hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
- hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
- hexdag/studio/ui/src/components/Header.tsx +181 -0
- hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
- hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
- hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
- hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
- hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
- hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
- hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
- hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
- hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
- hexdag/studio/ui/src/components/index.ts +8 -0
- hexdag/studio/ui/src/index.css +92 -0
- hexdag/studio/ui/src/main.tsx +10 -0
- hexdag/studio/ui/src/types/index.ts +123 -0
- hexdag/studio/ui/src/vite-env.d.ts +1 -0
- hexdag/studio/ui/tailwind.config.js +29 -0
- hexdag/studio/ui/tsconfig.json +37 -0
- hexdag/studio/ui/tsconfig.node.json +13 -0
- hexdag/studio/ui/vite.config.ts +35 -0
- hexdag/visualization/__init__.py +69 -0
- hexdag/visualization/dag_visualizer.py +1020 -0
- hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
- hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
- hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
- hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
- hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
- hexdag_plugins/.gitignore +43 -0
- hexdag_plugins/README.md +73 -0
- hexdag_plugins/__init__.py +1 -0
- hexdag_plugins/azure/LICENSE +21 -0
- hexdag_plugins/azure/README.md +414 -0
- hexdag_plugins/azure/__init__.py +21 -0
- hexdag_plugins/azure/azure_blob_adapter.py +450 -0
- hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
- hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
- hexdag_plugins/azure/azure_openai_adapter.py +415 -0
- hexdag_plugins/azure/pyproject.toml +107 -0
- hexdag_plugins/azure/tests/__init__.py +1 -0
- hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
- hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
- hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
- hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
- hexdag_plugins/hexdag_etl/README.md +168 -0
- hexdag_plugins/hexdag_etl/__init__.py +53 -0
- hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
- hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
- hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
- hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
- hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
- hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
- hexdag_plugins/hexdag_etl/test_transform.py +54 -0
- hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
- hexdag_plugins/mysql_adapter/LICENSE +21 -0
- hexdag_plugins/mysql_adapter/README.md +224 -0
- hexdag_plugins/mysql_adapter/__init__.py +6 -0
- hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
- hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
- hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
- hexdag_plugins/storage/README.md +184 -0
- hexdag_plugins/storage/__init__.py +19 -0
- hexdag_plugins/storage/file/__init__.py +5 -0
- hexdag_plugins/storage/file/local.py +325 -0
- hexdag_plugins/storage/ports/__init__.py +5 -0
- hexdag_plugins/storage/ports/vector_store.py +236 -0
- hexdag_plugins/storage/sql/__init__.py +7 -0
- hexdag_plugins/storage/sql/base.py +187 -0
- hexdag_plugins/storage/sql/mysql.py +27 -0
- hexdag_plugins/storage/sql/postgresql.py +27 -0
- hexdag_plugins/storage/tests/__init__.py +1 -0
- hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
- hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
- hexdag_plugins/storage/vector/__init__.py +7 -0
- hexdag_plugins/storage/vector/chromadb.py +223 -0
- hexdag_plugins/storage/vector/in_memory.py +285 -0
- 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
|
+
}
|