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