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,386 @@
|
|
|
1
|
+
"""Component instantiator for creating adapters and policies from YAML specs.
|
|
2
|
+
|
|
3
|
+
This module handles:
|
|
4
|
+
- Parsing native YAML dict component specifications
|
|
5
|
+
- Resolving components via module paths
|
|
6
|
+
- Instantiating adapters with configuration
|
|
7
|
+
- Instantiating policies with configuration
|
|
8
|
+
- Runtime resolution of deferred environment variables
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
from collections import namedtuple
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from hexdag.core.logging import get_logger
|
|
17
|
+
from hexdag.core.resolver import resolve
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
# Pattern for deferred environment variables: ${VAR} or ${VAR:default}
|
|
22
|
+
_DEFERRED_ENV_VAR_PATTERN = re.compile(r"\$\{([A-Z_][A-Z0-9_]*)(?::([^}]*))?\}")
|
|
23
|
+
|
|
24
|
+
# Simple namedtuple for component specification
|
|
25
|
+
ComponentSpec = namedtuple("ComponentSpec", ["module_path", "params"])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ComponentInstantiationError(Exception):
|
|
29
|
+
"""Error instantiating component from specification."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _resolve_deferred_env_vars(params: dict[str, Any]) -> dict[str, Any]:
|
|
35
|
+
"""Resolve deferred ${VAR} syntax in parameters at runtime.
|
|
36
|
+
|
|
37
|
+
This completes the deferred secret resolution workflow where secrets like
|
|
38
|
+
${OPENAI_API_KEY} are preserved at YAML build-time and resolved here
|
|
39
|
+
at adapter instantiation time.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
params : dict[str, Any]
|
|
44
|
+
Parameters potentially containing ${VAR} or ${VAR:default} syntax
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
dict[str, Any]
|
|
49
|
+
Parameters with environment variables resolved
|
|
50
|
+
|
|
51
|
+
Note
|
|
52
|
+
----
|
|
53
|
+
Raises ComponentInstantiationError (via _resolve_string_value) if a
|
|
54
|
+
required environment variable is not set and has no default.
|
|
55
|
+
|
|
56
|
+
Examples
|
|
57
|
+
--------
|
|
58
|
+
>>> import os
|
|
59
|
+
>>> os.environ["TEST_KEY"] = "secret123"
|
|
60
|
+
>>> _resolve_deferred_env_vars({"api_key": "${TEST_KEY}"})
|
|
61
|
+
{'api_key': 'secret123'}
|
|
62
|
+
>>> _resolve_deferred_env_vars({"model": "${MODEL:gpt-4}"})
|
|
63
|
+
{'model': 'gpt-4'}
|
|
64
|
+
"""
|
|
65
|
+
resolved: dict[str, Any] = {}
|
|
66
|
+
for key, value in params.items():
|
|
67
|
+
if isinstance(value, str):
|
|
68
|
+
resolved[key] = _resolve_string_value(value)
|
|
69
|
+
elif isinstance(value, dict):
|
|
70
|
+
resolved[key] = _resolve_deferred_env_vars(value)
|
|
71
|
+
elif isinstance(value, list):
|
|
72
|
+
resolved[key] = [
|
|
73
|
+
_resolve_string_value(v)
|
|
74
|
+
if isinstance(v, str)
|
|
75
|
+
else _resolve_deferred_env_vars(v)
|
|
76
|
+
if isinstance(v, dict)
|
|
77
|
+
else v
|
|
78
|
+
for v in value
|
|
79
|
+
]
|
|
80
|
+
else:
|
|
81
|
+
resolved[key] = value
|
|
82
|
+
return resolved
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _resolve_string_value(value: str) -> str:
|
|
86
|
+
"""Resolve ${VAR} or ${VAR:default} patterns in a string value.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
value : str
|
|
91
|
+
String potentially containing ${VAR} patterns
|
|
92
|
+
|
|
93
|
+
Returns
|
|
94
|
+
-------
|
|
95
|
+
str
|
|
96
|
+
String with all ${VAR} patterns resolved
|
|
97
|
+
|
|
98
|
+
Note
|
|
99
|
+
----
|
|
100
|
+
Raises ComponentInstantiationError (via nested replacer) if a required
|
|
101
|
+
environment variable is not set and has no default.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def replacer(match: re.Match[str]) -> str:
|
|
105
|
+
var_name = match.group(1)
|
|
106
|
+
default = match.group(2)
|
|
107
|
+
env_value = os.environ.get(var_name)
|
|
108
|
+
|
|
109
|
+
if env_value is None:
|
|
110
|
+
if default is not None:
|
|
111
|
+
logger.debug(f"Using default value for ${{{var_name}}}")
|
|
112
|
+
return default
|
|
113
|
+
raise ComponentInstantiationError(
|
|
114
|
+
f"Environment variable '{var_name}' is not set and no default provided. "
|
|
115
|
+
f"Set the environment variable or use ${{{var_name}:default}} syntax."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
logger.debug(f"Resolved ${{{var_name}}} from environment")
|
|
119
|
+
return env_value
|
|
120
|
+
|
|
121
|
+
return _DEFERRED_ENV_VAR_PATTERN.sub(replacer, value)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ComponentInstantiator:
|
|
125
|
+
"""Instantiates adapters and policies from native YAML dict specifications."""
|
|
126
|
+
|
|
127
|
+
def __init__(self) -> None:
|
|
128
|
+
"""Initialize component instantiator."""
|
|
129
|
+
pass # No bootstrap needed anymore
|
|
130
|
+
|
|
131
|
+
def _parse_component_spec(self, spec: dict[str, Any]) -> ComponentSpec:
|
|
132
|
+
"""Parse component specification from native YAML dict format.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
spec : dict[str, Any]
|
|
137
|
+
Component specification with either:
|
|
138
|
+
- "adapter": "module.path.ClassName" and optional "config": {...}
|
|
139
|
+
- "name": "module.path.ClassName" and optional "params": {...}
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
ComponentSpec
|
|
144
|
+
Parsed component specification with module_path and params
|
|
145
|
+
|
|
146
|
+
Raises
|
|
147
|
+
------
|
|
148
|
+
ComponentInstantiationError
|
|
149
|
+
If specification format is invalid
|
|
150
|
+
"""
|
|
151
|
+
if not isinstance(spec, dict):
|
|
152
|
+
raise ComponentInstantiationError(
|
|
153
|
+
f"Component specification must be a dict, got {type(spec).__name__}. "
|
|
154
|
+
f"Use format: {{adapter: 'module.path.Class', config: {{...}}}}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Support both "adapter" and "name" keys for module path
|
|
158
|
+
module_path = spec.get("adapter") or spec.get("name")
|
|
159
|
+
# Support both "config" and "params" keys for parameters
|
|
160
|
+
params = spec.get("config") or spec.get("params", {})
|
|
161
|
+
|
|
162
|
+
if not module_path:
|
|
163
|
+
raise ComponentInstantiationError(
|
|
164
|
+
f"Component specification requires 'adapter' or 'name' field. Got: {spec}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return ComponentSpec(module_path=module_path, params=params)
|
|
168
|
+
|
|
169
|
+
def instantiate_adapter(self, spec: dict[str, Any], port_name: str | None = None) -> Any:
|
|
170
|
+
"""Instantiate an adapter from native YAML dict specification.
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
spec : dict[str, Any]
|
|
175
|
+
Adapter specification: {"adapter": "hexdag.builtin.adapters.mock.MockLLM",
|
|
176
|
+
"config": {"model": "gpt-4"}}
|
|
177
|
+
port_name : str | None
|
|
178
|
+
Optional port name for context in error messages
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
Any
|
|
183
|
+
Instantiated adapter instance
|
|
184
|
+
|
|
185
|
+
Raises
|
|
186
|
+
------
|
|
187
|
+
ComponentInstantiationError
|
|
188
|
+
If adapter cannot be instantiated
|
|
189
|
+
|
|
190
|
+
Examples
|
|
191
|
+
--------
|
|
192
|
+
>>> instantiator = ComponentInstantiator() # doctest: +SKIP
|
|
193
|
+
>>> adapter = instantiator.instantiate_adapter({ # doctest: +SKIP
|
|
194
|
+
... "adapter": "hexdag.builtin.adapters.mock.MockLLM",
|
|
195
|
+
... "config": {"model": "gpt-4"}
|
|
196
|
+
... })
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
# Parse the specification
|
|
200
|
+
component_spec = self._parse_component_spec(spec)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
adapter_class = resolve(component_spec.module_path)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
raise ComponentInstantiationError(
|
|
206
|
+
f"Adapter '{component_spec.module_path}' "
|
|
207
|
+
f"could not be resolved. "
|
|
208
|
+
f"Make sure the module path is correct. Error: {e}"
|
|
209
|
+
) from e
|
|
210
|
+
|
|
211
|
+
# Instantiate the adapter class with parameters
|
|
212
|
+
if isinstance(adapter_class, type):
|
|
213
|
+
try:
|
|
214
|
+
# Resolve any deferred environment variables at instantiation time
|
|
215
|
+
resolved_params = _resolve_deferred_env_vars(component_spec.params)
|
|
216
|
+
adapter_instance = adapter_class(**resolved_params)
|
|
217
|
+
logger.info(
|
|
218
|
+
f"Instantiated adapter '{component_spec.module_path}' "
|
|
219
|
+
f"for port '{port_name}'"
|
|
220
|
+
)
|
|
221
|
+
return adapter_instance
|
|
222
|
+
except Exception as e:
|
|
223
|
+
raise ComponentInstantiationError(
|
|
224
|
+
f"Failed to instantiate adapter "
|
|
225
|
+
f"'{component_spec.module_path}' "
|
|
226
|
+
f"with params {component_spec.params}. Error: {e}"
|
|
227
|
+
) from e
|
|
228
|
+
else:
|
|
229
|
+
# It's already an instance (runtime-registered non-class component)
|
|
230
|
+
if component_spec.params: # type: ignore[unreachable]
|
|
231
|
+
logger.warning(
|
|
232
|
+
f"Adapter '{component_spec.module_path}' "
|
|
233
|
+
f"resolved to an instance. Parameters {component_spec.params} "
|
|
234
|
+
f"will be ignored."
|
|
235
|
+
)
|
|
236
|
+
logger.info(
|
|
237
|
+
f"Using resolved adapter instance "
|
|
238
|
+
f"'{component_spec.module_path}' "
|
|
239
|
+
f"for port '{port_name}'"
|
|
240
|
+
)
|
|
241
|
+
return adapter_class
|
|
242
|
+
|
|
243
|
+
except ComponentInstantiationError:
|
|
244
|
+
raise
|
|
245
|
+
except Exception as e:
|
|
246
|
+
raise ComponentInstantiationError(
|
|
247
|
+
f"Failed to instantiate adapter for port '{port_name}': {e}"
|
|
248
|
+
) from e
|
|
249
|
+
|
|
250
|
+
def instantiate_policy(self, spec: dict[str, Any], policy_name: str | None = None) -> Any:
|
|
251
|
+
"""Instantiate a policy from native YAML dict specification.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
spec : dict[str, Any]
|
|
256
|
+
Policy specification: {"name": "hexdag.builtin.policies.RetryPolicy",
|
|
257
|
+
"params": {"max_retries": 3}}
|
|
258
|
+
policy_name : str | None
|
|
259
|
+
Optional policy name for context in error messages
|
|
260
|
+
|
|
261
|
+
Returns
|
|
262
|
+
-------
|
|
263
|
+
Any
|
|
264
|
+
Instantiated policy instance
|
|
265
|
+
|
|
266
|
+
Raises
|
|
267
|
+
------
|
|
268
|
+
ComponentInstantiationError
|
|
269
|
+
If policy cannot be instantiated
|
|
270
|
+
|
|
271
|
+
Examples
|
|
272
|
+
--------
|
|
273
|
+
>>> instantiator = ComponentInstantiator() # doctest: +SKIP
|
|
274
|
+
>>> policy = instantiator.instantiate_policy({ # doctest: +SKIP
|
|
275
|
+
... "name": "hexdag.builtin.policies.execution_policies.RetryPolicy",
|
|
276
|
+
... "params": {"max_retries": 5}
|
|
277
|
+
... })
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
# Parse the specification
|
|
281
|
+
component_spec = self._parse_component_spec(spec)
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
policy_class = resolve(component_spec.module_path)
|
|
285
|
+
except Exception as e:
|
|
286
|
+
raise ComponentInstantiationError(
|
|
287
|
+
f"Policy '{component_spec.module_path}' "
|
|
288
|
+
f"could not be resolved. "
|
|
289
|
+
f"Make sure the module path is correct. Error: {e}"
|
|
290
|
+
) from e
|
|
291
|
+
|
|
292
|
+
if not isinstance(policy_class, type):
|
|
293
|
+
raise ComponentInstantiationError(
|
|
294
|
+
f"Policy '{component_spec.module_path}' resolved to an instance, "
|
|
295
|
+
f"not a class. Cannot instantiate from instance."
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Instantiate with parameters
|
|
299
|
+
try:
|
|
300
|
+
# Resolve any deferred environment variables at instantiation time
|
|
301
|
+
resolved_params = _resolve_deferred_env_vars(component_spec.params)
|
|
302
|
+
policy_instance = policy_class(**resolved_params)
|
|
303
|
+
logger.info(f"Instantiated policy '{component_spec.module_path}' ('{policy_name}')")
|
|
304
|
+
return policy_instance
|
|
305
|
+
except Exception as e:
|
|
306
|
+
raise ComponentInstantiationError(
|
|
307
|
+
f"Failed to instantiate policy "
|
|
308
|
+
f"'{component_spec.module_path}' "
|
|
309
|
+
f"with params {component_spec.params}. Error: {e}"
|
|
310
|
+
) from e
|
|
311
|
+
|
|
312
|
+
except ComponentInstantiationError:
|
|
313
|
+
raise
|
|
314
|
+
except Exception as e:
|
|
315
|
+
raise ComponentInstantiationError(
|
|
316
|
+
f"Failed to instantiate policy '{policy_name}': {e}"
|
|
317
|
+
) from e
|
|
318
|
+
|
|
319
|
+
def instantiate_ports(self, ports_config: dict[str, dict[str, Any]]) -> dict[str, Any]:
|
|
320
|
+
"""Instantiate all ports (adapters) from configuration.
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
ports_config : dict[str, dict[str, Any]]
|
|
325
|
+
Dictionary of port_name -> adapter dict spec
|
|
326
|
+
|
|
327
|
+
Returns
|
|
328
|
+
-------
|
|
329
|
+
dict[str, Any]
|
|
330
|
+
Dictionary of port_name -> adapter_instance
|
|
331
|
+
|
|
332
|
+
Examples
|
|
333
|
+
--------
|
|
334
|
+
>>> instantiator = ComponentInstantiator() # doctest: +SKIP
|
|
335
|
+
>>> config = { # doctest: +SKIP
|
|
336
|
+
... "llm": {"adapter": "hexdag.builtin.adapters.mock.MockLLM",
|
|
337
|
+
... "config": {"model": "gpt-4"}},
|
|
338
|
+
... "database": {"adapter": "hexdag.builtin.adapters.mock.MockDatabaseAdapter",
|
|
339
|
+
... "config": {}}
|
|
340
|
+
... }
|
|
341
|
+
"""
|
|
342
|
+
ports: dict[str, Any] = {}
|
|
343
|
+
|
|
344
|
+
for port_name, adapter_spec in ports_config.items():
|
|
345
|
+
try:
|
|
346
|
+
ports[port_name] = self.instantiate_adapter(adapter_spec, port_name=port_name)
|
|
347
|
+
except ComponentInstantiationError as e:
|
|
348
|
+
logger.error(f"Failed to instantiate adapter for port '{port_name}': {e}")
|
|
349
|
+
raise
|
|
350
|
+
|
|
351
|
+
return ports
|
|
352
|
+
|
|
353
|
+
def instantiate_policies(self, policies_config: dict[str, dict[str, Any]]) -> list[Any]:
|
|
354
|
+
"""Instantiate all policies from configuration.
|
|
355
|
+
|
|
356
|
+
Parameters
|
|
357
|
+
----------
|
|
358
|
+
policies_config : dict[str, dict[str, Any]]
|
|
359
|
+
Dictionary of policy_name -> policy dict spec
|
|
360
|
+
|
|
361
|
+
Returns
|
|
362
|
+
-------
|
|
363
|
+
list[Any]
|
|
364
|
+
List of policy instances
|
|
365
|
+
|
|
366
|
+
Examples
|
|
367
|
+
--------
|
|
368
|
+
>>> instantiator = ComponentInstantiator() # doctest: +SKIP
|
|
369
|
+
>>> config = { # doctest: +SKIP
|
|
370
|
+
... "retry": {"name": "hexdag.builtin.policies.RetryPolicy",
|
|
371
|
+
... "params": {"max_retries": 3}},
|
|
372
|
+
... "timeout": {"name": "hexdag.builtin.policies.TimeoutPolicy",
|
|
373
|
+
... "params": {"timeout_seconds": 300}}
|
|
374
|
+
... }
|
|
375
|
+
"""
|
|
376
|
+
policies: list[Any] = []
|
|
377
|
+
|
|
378
|
+
for policy_name, policy_spec in policies_config.items():
|
|
379
|
+
try:
|
|
380
|
+
policy = self.instantiate_policy(policy_spec, policy_name=policy_name)
|
|
381
|
+
policies.append(policy)
|
|
382
|
+
except ComponentInstantiationError as e:
|
|
383
|
+
logger.error(f"Failed to instantiate policy '{policy_name}': {e}")
|
|
384
|
+
raise
|
|
385
|
+
|
|
386
|
+
return policies
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""!include YAML custom tag handler for pipeline composition.
|
|
2
|
+
|
|
3
|
+
This module provides a YAML custom tag constructor that includes content
|
|
4
|
+
from external YAML files, enabling modular pipeline composition.
|
|
5
|
+
|
|
6
|
+
Examples
|
|
7
|
+
--------
|
|
8
|
+
Include a list of nodes::
|
|
9
|
+
|
|
10
|
+
spec:
|
|
11
|
+
nodes:
|
|
12
|
+
- kind: expression_node
|
|
13
|
+
metadata:
|
|
14
|
+
name: start
|
|
15
|
+
spec:
|
|
16
|
+
expressions:
|
|
17
|
+
ready: "true"
|
|
18
|
+
|
|
19
|
+
# Include nodes from external file
|
|
20
|
+
- !include ./shared/validation_nodes.yaml
|
|
21
|
+
|
|
22
|
+
- kind: llm_node
|
|
23
|
+
metadata:
|
|
24
|
+
name: final
|
|
25
|
+
spec:
|
|
26
|
+
prompt_template: "Finalize: {{input}}"
|
|
27
|
+
|
|
28
|
+
Include a partial pipeline fragment::
|
|
29
|
+
|
|
30
|
+
# In shared/validation_nodes.yaml
|
|
31
|
+
- kind: function_node
|
|
32
|
+
metadata:
|
|
33
|
+
name: validate_input
|
|
34
|
+
spec:
|
|
35
|
+
fn: "myapp.validate"
|
|
36
|
+
|
|
37
|
+
- kind: expression_node
|
|
38
|
+
metadata:
|
|
39
|
+
name: check_result
|
|
40
|
+
spec:
|
|
41
|
+
expressions:
|
|
42
|
+
valid: "validate_input.success"
|
|
43
|
+
|
|
44
|
+
Include with variable substitution::
|
|
45
|
+
|
|
46
|
+
# Use !include with a mapping for variable substitution
|
|
47
|
+
- !include
|
|
48
|
+
path: ./templates/processor.yaml
|
|
49
|
+
vars:
|
|
50
|
+
node_name: "custom_processor"
|
|
51
|
+
timeout: 30
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
from typing import Any
|
|
56
|
+
|
|
57
|
+
import yaml
|
|
58
|
+
|
|
59
|
+
from hexdag.core.logging import get_logger
|
|
60
|
+
|
|
61
|
+
logger = get_logger(__name__)
|
|
62
|
+
|
|
63
|
+
# Thread-local storage for base path during parsing
|
|
64
|
+
_current_base_path: Path | None = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class IncludeTagError(Exception):
|
|
68
|
+
"""Error including external YAML file."""
|
|
69
|
+
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def set_include_base_path(base_path: Path | None) -> None:
|
|
74
|
+
"""Set the base path for resolving !include paths.
|
|
75
|
+
|
|
76
|
+
This should be called before parsing YAML that may contain !include tags.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
base_path : Path | None
|
|
81
|
+
Base directory for resolving relative paths
|
|
82
|
+
"""
|
|
83
|
+
global _current_base_path
|
|
84
|
+
_current_base_path = base_path
|
|
85
|
+
if base_path:
|
|
86
|
+
logger.debug("Set include base path", base_path=str(base_path))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_include_base_path() -> Path:
|
|
90
|
+
"""Get the current base path for !include resolution.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
Path
|
|
95
|
+
Current base path (defaults to cwd if not set)
|
|
96
|
+
"""
|
|
97
|
+
return _current_base_path or Path.cwd()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def include_constructor(loader: yaml.SafeLoader, node: yaml.Node) -> Any:
|
|
101
|
+
"""Include content from an external YAML file.
|
|
102
|
+
|
|
103
|
+
Supports two forms:
|
|
104
|
+
1. Simple: !include ./path/to/file.yaml
|
|
105
|
+
2. With vars: !include {path: ./file.yaml, vars: {key: value}}
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
loader : yaml.SafeLoader
|
|
110
|
+
YAML loader instance
|
|
111
|
+
node : yaml.Node
|
|
112
|
+
YAML node (scalar for simple, mapping for vars)
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
Any
|
|
117
|
+
Parsed content from the included file
|
|
118
|
+
|
|
119
|
+
Raises
|
|
120
|
+
------
|
|
121
|
+
IncludeTagError
|
|
122
|
+
If the file cannot be found or parsed
|
|
123
|
+
"""
|
|
124
|
+
base_path = get_include_base_path()
|
|
125
|
+
|
|
126
|
+
# Handle simple scalar form: !include ./path.yaml
|
|
127
|
+
if isinstance(node, yaml.ScalarNode):
|
|
128
|
+
include_path = loader.construct_scalar(node)
|
|
129
|
+
if not isinstance(include_path, str):
|
|
130
|
+
raise IncludeTagError("!include path must be a string")
|
|
131
|
+
vars_dict: dict[str, Any] = {}
|
|
132
|
+
|
|
133
|
+
# Handle mapping form: !include {path: ./path.yaml, vars: {...}}
|
|
134
|
+
elif isinstance(node, yaml.MappingNode):
|
|
135
|
+
# Use deep=True to fully construct nested mappings (like vars)
|
|
136
|
+
mapping = loader.construct_mapping(node, deep=True)
|
|
137
|
+
include_path = mapping.get("path")
|
|
138
|
+
if not include_path:
|
|
139
|
+
raise IncludeTagError("!include mapping requires 'path' key")
|
|
140
|
+
vars_dict = mapping.get("vars", {})
|
|
141
|
+
|
|
142
|
+
else:
|
|
143
|
+
raise IncludeTagError(f"!include expects scalar or mapping, got {type(node)}")
|
|
144
|
+
|
|
145
|
+
# Resolve path relative to base
|
|
146
|
+
resolved_path = _resolve_include_path(include_path, base_path)
|
|
147
|
+
|
|
148
|
+
logger.debug(
|
|
149
|
+
"Including file",
|
|
150
|
+
include_path=include_path,
|
|
151
|
+
resolved_path=str(resolved_path),
|
|
152
|
+
has_vars=bool(vars_dict),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Read and parse the file
|
|
156
|
+
try:
|
|
157
|
+
content = resolved_path.read_text()
|
|
158
|
+
except FileNotFoundError as e:
|
|
159
|
+
raise IncludeTagError(
|
|
160
|
+
f"Include file not found: {resolved_path}\n"
|
|
161
|
+
f" (resolved from '{include_path}' relative to '{base_path}')"
|
|
162
|
+
) from e
|
|
163
|
+
except Exception as e:
|
|
164
|
+
raise IncludeTagError(f"Failed to read include file {resolved_path}: {e}") from e
|
|
165
|
+
|
|
166
|
+
# Apply variable substitution if vars provided
|
|
167
|
+
if vars_dict:
|
|
168
|
+
content = _substitute_vars(content, vars_dict)
|
|
169
|
+
|
|
170
|
+
# Parse the included content
|
|
171
|
+
try:
|
|
172
|
+
# Set base path for nested includes
|
|
173
|
+
old_base = _current_base_path
|
|
174
|
+
set_include_base_path(resolved_path.parent)
|
|
175
|
+
try:
|
|
176
|
+
result = yaml.safe_load(content)
|
|
177
|
+
finally:
|
|
178
|
+
set_include_base_path(old_base)
|
|
179
|
+
except yaml.YAMLError as e:
|
|
180
|
+
raise IncludeTagError(f"Failed to parse include file {resolved_path}: {e}") from e
|
|
181
|
+
|
|
182
|
+
return result
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _resolve_include_path(include_path: str, base_path: Path) -> Path:
|
|
186
|
+
"""Resolve include path relative to base path.
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
include_path : str
|
|
191
|
+
Path from !include tag (may be relative or absolute)
|
|
192
|
+
base_path : Path
|
|
193
|
+
Base directory for relative paths
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
Path
|
|
198
|
+
Resolved absolute path
|
|
199
|
+
"""
|
|
200
|
+
path = Path(include_path)
|
|
201
|
+
|
|
202
|
+
# If absolute, use as-is
|
|
203
|
+
if path.is_absolute():
|
|
204
|
+
return path
|
|
205
|
+
|
|
206
|
+
# Resolve relative to base path
|
|
207
|
+
return (base_path / path).resolve()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _substitute_vars(content: str, vars_dict: dict[str, Any]) -> str:
|
|
211
|
+
"""Substitute variables in content using {{var}} syntax.
|
|
212
|
+
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
content : str
|
|
216
|
+
YAML content with {{var}} placeholders
|
|
217
|
+
vars_dict : dict[str, Any]
|
|
218
|
+
Variable values to substitute
|
|
219
|
+
|
|
220
|
+
Returns
|
|
221
|
+
-------
|
|
222
|
+
str
|
|
223
|
+
Content with variables substituted
|
|
224
|
+
"""
|
|
225
|
+
result = content
|
|
226
|
+
for key, value in vars_dict.items():
|
|
227
|
+
placeholder = "{{" + key + "}}"
|
|
228
|
+
# Convert value to YAML-safe string representation
|
|
229
|
+
if isinstance(value, str):
|
|
230
|
+
str_value = value
|
|
231
|
+
elif isinstance(value, bool):
|
|
232
|
+
str_value = "true" if value else "false"
|
|
233
|
+
elif value is None:
|
|
234
|
+
str_value = "null"
|
|
235
|
+
else:
|
|
236
|
+
str_value = str(value)
|
|
237
|
+
result = result.replace(placeholder, str_value)
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def register_include_tag() -> None:
|
|
242
|
+
"""Register the !include custom tag with YAML SafeLoader.
|
|
243
|
+
|
|
244
|
+
This function should be called during module initialization to enable
|
|
245
|
+
!include tag support in YAML parsing.
|
|
246
|
+
|
|
247
|
+
Examples
|
|
248
|
+
--------
|
|
249
|
+
Import the module to auto-register::
|
|
250
|
+
|
|
251
|
+
import hexdag.core.pipeline_builder.include_tag # Registers !include tag
|
|
252
|
+
|
|
253
|
+
Or explicitly register::
|
|
254
|
+
|
|
255
|
+
from hexdag.core.pipeline_builder.include_tag import register_include_tag
|
|
256
|
+
register_include_tag()
|
|
257
|
+
"""
|
|
258
|
+
# Check if already registered to avoid duplicate registration
|
|
259
|
+
if "!include" not in yaml.SafeLoader.yaml_constructors:
|
|
260
|
+
yaml.SafeLoader.add_constructor("!include", include_constructor)
|
|
261
|
+
logger.debug("Registered !include YAML tag")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# Auto-register when module is imported
|
|
265
|
+
register_include_tag()
|