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