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,517 @@
|
|
|
1
|
+
"""YAML-defined macros for declarative pipeline composition.
|
|
2
|
+
|
|
3
|
+
This module enables defining macros entirely in YAML without Python code.
|
|
4
|
+
YAML macros provide the same capabilities as Python macros but with a
|
|
5
|
+
declarative syntax suitable for version control and team collaboration.
|
|
6
|
+
|
|
7
|
+
Architecture:
|
|
8
|
+
YamlMacro - ConfigurableMacro subclass that expands from YAML definition
|
|
9
|
+
Parameter validation - Same as nodes (Pydantic-based)
|
|
10
|
+
Template expansion - Jinja2 for node generation
|
|
11
|
+
Output mapping - Same as Python macros (DirectedGraph)
|
|
12
|
+
|
|
13
|
+
Examples
|
|
14
|
+
--------
|
|
15
|
+
YAML macro definition::
|
|
16
|
+
|
|
17
|
+
apiVersion: hexdag/v1
|
|
18
|
+
kind: Macro
|
|
19
|
+
metadata:
|
|
20
|
+
name: retry_workflow
|
|
21
|
+
description: Retry a node with exponential backoff
|
|
22
|
+
parameters:
|
|
23
|
+
- name: max_retries
|
|
24
|
+
type: int
|
|
25
|
+
default: 3
|
|
26
|
+
- name: base_delay
|
|
27
|
+
type: float
|
|
28
|
+
default: 1.0
|
|
29
|
+
nodes:
|
|
30
|
+
- kind: function_node
|
|
31
|
+
metadata:
|
|
32
|
+
name: "{{name}}_attempt"
|
|
33
|
+
spec:
|
|
34
|
+
fn: "{{fn}}"
|
|
35
|
+
max_retries: "{{max_retries}}"
|
|
36
|
+
|
|
37
|
+
YAML macro usage::
|
|
38
|
+
|
|
39
|
+
nodes:
|
|
40
|
+
- kind: macro_invocation
|
|
41
|
+
metadata:
|
|
42
|
+
name: api_call
|
|
43
|
+
spec:
|
|
44
|
+
macro: user:retry_workflow
|
|
45
|
+
config:
|
|
46
|
+
max_retries: 5
|
|
47
|
+
inputs:
|
|
48
|
+
fn: "myapp.api.fetch_data"
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
from typing import Any
|
|
54
|
+
|
|
55
|
+
from jinja2 import ChainableUndefined, Environment, TemplateSyntaxError, UndefinedError
|
|
56
|
+
from pydantic import BaseModel, Field, field_validator
|
|
57
|
+
|
|
58
|
+
from hexdag.core.configurable import ConfigurableMacro, MacroConfig
|
|
59
|
+
from hexdag.core.domain.dag import DirectedGraph
|
|
60
|
+
from hexdag.core.logging import get_logger
|
|
61
|
+
|
|
62
|
+
logger = get_logger(__name__)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PreserveUndefined(ChainableUndefined):
|
|
66
|
+
"""Custom Jinja2 Undefined that preserves template syntax for undefined variables.
|
|
67
|
+
|
|
68
|
+
This allows partial template rendering where:
|
|
69
|
+
- Known variables (from macro context) are replaced
|
|
70
|
+
- Unknown variables (runtime references) are preserved as {{var}}
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __str__(self) -> str:
|
|
74
|
+
"""Return the original template syntax for undefined variables."""
|
|
75
|
+
return f"{{{{{self._undefined_name}}}}}"
|
|
76
|
+
|
|
77
|
+
def __getattr__(self, name: str) -> Any:
|
|
78
|
+
"""Handle attribute access on undefined variables.
|
|
79
|
+
|
|
80
|
+
This preserves the full path for dotted references like {{node.output}}.
|
|
81
|
+
"""
|
|
82
|
+
if name.startswith("_"):
|
|
83
|
+
# Internal Jinja2 attributes - use parent implementation
|
|
84
|
+
return super().__getattr__(name)
|
|
85
|
+
|
|
86
|
+
# Create a new PreserveUndefined with the full path
|
|
87
|
+
return self.__class__(
|
|
88
|
+
name=f"{self._undefined_name}.{name}",
|
|
89
|
+
exc=self._undefined_exception,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class YamlMacroParameterSpec(BaseModel):
|
|
94
|
+
"""Schema for a single YAML macro parameter.
|
|
95
|
+
|
|
96
|
+
Attributes
|
|
97
|
+
----------
|
|
98
|
+
name : str
|
|
99
|
+
Parameter name
|
|
100
|
+
type : str
|
|
101
|
+
Python type name (e.g., "str", "int", "list", "dict")
|
|
102
|
+
description : str | None
|
|
103
|
+
Parameter description for documentation
|
|
104
|
+
required : bool
|
|
105
|
+
Whether parameter is required (default: False)
|
|
106
|
+
default : Any
|
|
107
|
+
Default value if not provided
|
|
108
|
+
enum : list[Any] | None
|
|
109
|
+
Valid values for enumeration types
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
name: str
|
|
113
|
+
type: str = "str"
|
|
114
|
+
description: str | None = None
|
|
115
|
+
required: bool = False
|
|
116
|
+
default: Any = None
|
|
117
|
+
enum: list[Any] | None = None
|
|
118
|
+
|
|
119
|
+
@field_validator("type")
|
|
120
|
+
@classmethod
|
|
121
|
+
def validate_type(cls, v: str) -> str:
|
|
122
|
+
"""Validate that type is a recognized Python type."""
|
|
123
|
+
# Support common types and union syntax
|
|
124
|
+
valid_base_types = {"str", "int", "float", "bool", "list", "dict", "Any"}
|
|
125
|
+
# Split on | for union types
|
|
126
|
+
types = [t.strip() for t in v.split("|")]
|
|
127
|
+
for t in types:
|
|
128
|
+
if t not in valid_base_types:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"Invalid type '{t}'. Must be one of: {', '.join(valid_base_types)}"
|
|
131
|
+
)
|
|
132
|
+
return v
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class YamlMacroConfig(MacroConfig):
|
|
136
|
+
"""Configuration for YAML-defined macros.
|
|
137
|
+
|
|
138
|
+
This config is dynamically generated from the YAML macro definition.
|
|
139
|
+
It stores the macro structure (parameters, nodes, outputs) that will
|
|
140
|
+
be expanded when the macro is invoked.
|
|
141
|
+
|
|
142
|
+
Attributes
|
|
143
|
+
----------
|
|
144
|
+
macro_name : str
|
|
145
|
+
Name of the macro
|
|
146
|
+
macro_description : str | None
|
|
147
|
+
Description from metadata
|
|
148
|
+
parameters : list[YamlMacroParameterSpec]
|
|
149
|
+
Parameter definitions
|
|
150
|
+
nodes : list[dict[str, Any]]
|
|
151
|
+
Node templates (will be rendered with Jinja2)
|
|
152
|
+
outputs : dict[str, str] | None
|
|
153
|
+
Output mappings (optional, like Python macros)
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
macro_name: str
|
|
157
|
+
macro_description: str | None = None
|
|
158
|
+
parameters: list[YamlMacroParameterSpec] = Field(default_factory=list)
|
|
159
|
+
nodes: list[dict[str, Any]] = Field(default_factory=list)
|
|
160
|
+
outputs: dict[str, str] | None = None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class YamlMacro(ConfigurableMacro):
|
|
164
|
+
"""Macro defined entirely in YAML.
|
|
165
|
+
|
|
166
|
+
This class provides the runtime implementation for YAML macro expansion.
|
|
167
|
+
It's instantiated by MacroDefinitionPlugin when processing `kind: Macro`
|
|
168
|
+
declarations in YAML files.
|
|
169
|
+
|
|
170
|
+
The expansion process:
|
|
171
|
+
1. Validate provided config against parameter definitions
|
|
172
|
+
2. Build Jinja2 context from parameters + inputs
|
|
173
|
+
3. Render node templates
|
|
174
|
+
4. Build DirectedGraph from rendered nodes
|
|
175
|
+
5. Return expanded graph
|
|
176
|
+
|
|
177
|
+
Examples
|
|
178
|
+
--------
|
|
179
|
+
YAML macro definition::
|
|
180
|
+
|
|
181
|
+
apiVersion: hexdag/v1
|
|
182
|
+
kind: Macro
|
|
183
|
+
metadata:
|
|
184
|
+
name: hitl_decision
|
|
185
|
+
description: Human-in-the-loop decision point
|
|
186
|
+
parameters:
|
|
187
|
+
- name: mode
|
|
188
|
+
type: str
|
|
189
|
+
default: human
|
|
190
|
+
enum: [human, auto]
|
|
191
|
+
- name: timeout
|
|
192
|
+
type: int
|
|
193
|
+
default: 60
|
|
194
|
+
nodes:
|
|
195
|
+
- kind: conditional
|
|
196
|
+
metadata:
|
|
197
|
+
name: "{{name}}_route"
|
|
198
|
+
spec:
|
|
199
|
+
condition: "{{mode == 'human'}}"
|
|
200
|
+
true_branch: "{{name}}_human"
|
|
201
|
+
false_branch: "{{name}}_auto"
|
|
202
|
+
|
|
203
|
+
Python invocation::
|
|
204
|
+
|
|
205
|
+
from hexdag.core.resolver import resolve
|
|
206
|
+
|
|
207
|
+
MacroClass = resolve("myapp.macros.HitlDecisionMacro")
|
|
208
|
+
macro = MacroClass()
|
|
209
|
+
graph = macro.expand(
|
|
210
|
+
instance_name="approval",
|
|
211
|
+
inputs={"mode": "auto"},
|
|
212
|
+
dependencies=["validator"]
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
YAML invocation::
|
|
216
|
+
|
|
217
|
+
nodes:
|
|
218
|
+
- kind: macro_invocation
|
|
219
|
+
metadata:
|
|
220
|
+
name: approval
|
|
221
|
+
spec:
|
|
222
|
+
macro: user:hitl_decision
|
|
223
|
+
config:
|
|
224
|
+
mode: auto
|
|
225
|
+
dependencies: [validator]
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
Config = YamlMacroConfig
|
|
229
|
+
|
|
230
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
231
|
+
"""Initialize YAML macro from config.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
**kwargs : Any
|
|
236
|
+
Configuration matching YamlMacroConfig schema
|
|
237
|
+
"""
|
|
238
|
+
super().__init__(**kwargs)
|
|
239
|
+
|
|
240
|
+
# Create Jinja2 environment for template rendering
|
|
241
|
+
# Use PreserveUndefined to allow partial rendering:
|
|
242
|
+
# - Macro variables ({{name}}, {{param}}) are replaced
|
|
243
|
+
# - Runtime variables ({{node.output}}) are preserved
|
|
244
|
+
# Note: autoescape=False is intentional - we're processing YAML, not HTML
|
|
245
|
+
self.jinja_env = Environment(
|
|
246
|
+
autoescape=False, # nosec B701 - YAML processing, not HTML
|
|
247
|
+
undefined=PreserveUndefined,
|
|
248
|
+
keep_trailing_newline=True,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Build parameter schema for validation
|
|
252
|
+
self._param_schema = self._build_param_schema()
|
|
253
|
+
|
|
254
|
+
def _build_param_schema(self) -> dict[str, YamlMacroParameterSpec]:
|
|
255
|
+
"""Build parameter schema from config for fast lookup."""
|
|
256
|
+
config: YamlMacroConfig = self.config # type: ignore[assignment]
|
|
257
|
+
return {param.name: param for param in config.parameters}
|
|
258
|
+
|
|
259
|
+
def expand(
|
|
260
|
+
self,
|
|
261
|
+
instance_name: str,
|
|
262
|
+
inputs: dict[str, Any],
|
|
263
|
+
dependencies: list[str],
|
|
264
|
+
) -> DirectedGraph:
|
|
265
|
+
"""Expand YAML macro into a DirectedGraph.
|
|
266
|
+
|
|
267
|
+
Parameters
|
|
268
|
+
----------
|
|
269
|
+
instance_name : str
|
|
270
|
+
Unique name for this macro instance (used in templates as {{name}})
|
|
271
|
+
inputs : dict[str, Any]
|
|
272
|
+
Input values for macro parameters (merged with defaults)
|
|
273
|
+
dependencies : list[str]
|
|
274
|
+
External node names this macro depends on
|
|
275
|
+
|
|
276
|
+
Returns
|
|
277
|
+
-------
|
|
278
|
+
DirectedGraph
|
|
279
|
+
Expanded graph with rendered nodes
|
|
280
|
+
"""
|
|
281
|
+
config: YamlMacroConfig = self.config # type: ignore[assignment]
|
|
282
|
+
|
|
283
|
+
# Step 1: Validate and normalize inputs
|
|
284
|
+
validated_inputs = self._validate_and_normalize_inputs(inputs)
|
|
285
|
+
|
|
286
|
+
# Step 2: Build Jinja2 context
|
|
287
|
+
context = self._build_template_context(instance_name, validated_inputs, dependencies)
|
|
288
|
+
|
|
289
|
+
# Step 3: Render node templates
|
|
290
|
+
rendered_nodes = self._render_node_templates(config.nodes, context)
|
|
291
|
+
|
|
292
|
+
# Step 4: Build DirectedGraph from rendered nodes
|
|
293
|
+
graph = self._build_graph_from_nodes(rendered_nodes)
|
|
294
|
+
|
|
295
|
+
logger.info(
|
|
296
|
+
f"✅ Expanded YAML macro '{config.macro_name}' as '{instance_name}' "
|
|
297
|
+
f"({len(graph.nodes)} nodes)"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
return graph
|
|
301
|
+
|
|
302
|
+
def _validate_and_normalize_inputs(self, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
303
|
+
"""Validate inputs against parameter schema and apply defaults.
|
|
304
|
+
|
|
305
|
+
Parameters
|
|
306
|
+
----------
|
|
307
|
+
inputs : dict[str, Any]
|
|
308
|
+
Provided input values
|
|
309
|
+
|
|
310
|
+
Returns
|
|
311
|
+
-------
|
|
312
|
+
dict[str, Any]
|
|
313
|
+
Validated inputs with defaults applied
|
|
314
|
+
|
|
315
|
+
Raises
|
|
316
|
+
------
|
|
317
|
+
ValueError
|
|
318
|
+
If required parameters are missing or enum validation fails
|
|
319
|
+
"""
|
|
320
|
+
result = {}
|
|
321
|
+
config: YamlMacroConfig = self.config # type: ignore[assignment]
|
|
322
|
+
|
|
323
|
+
for param in config.parameters:
|
|
324
|
+
param_name = param.name
|
|
325
|
+
|
|
326
|
+
# Check if value provided
|
|
327
|
+
if param_name in inputs:
|
|
328
|
+
value = inputs[param_name]
|
|
329
|
+
|
|
330
|
+
# Enum validation
|
|
331
|
+
if param.enum is not None and value not in param.enum:
|
|
332
|
+
raise ValueError(
|
|
333
|
+
f"Parameter '{param_name}' must be one of {param.enum}, got '{value}'"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
result[param_name] = value
|
|
337
|
+
|
|
338
|
+
elif param.required:
|
|
339
|
+
# Required but not provided
|
|
340
|
+
raise ValueError(
|
|
341
|
+
f"Required parameter '{param_name}' not provided for macro "
|
|
342
|
+
f"'{config.macro_name}'"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
elif param.default is not None:
|
|
346
|
+
# Apply default
|
|
347
|
+
result[param_name] = param.default
|
|
348
|
+
|
|
349
|
+
return result
|
|
350
|
+
|
|
351
|
+
def _build_template_context(
|
|
352
|
+
self,
|
|
353
|
+
instance_name: str,
|
|
354
|
+
validated_inputs: dict[str, Any],
|
|
355
|
+
dependencies: list[str],
|
|
356
|
+
) -> dict[str, Any]:
|
|
357
|
+
"""Build Jinja2 context for template rendering.
|
|
358
|
+
|
|
359
|
+
Parameters
|
|
360
|
+
----------
|
|
361
|
+
instance_name : str
|
|
362
|
+
Macro instance name
|
|
363
|
+
validated_inputs : dict[str, Any]
|
|
364
|
+
Validated parameter values
|
|
365
|
+
dependencies : list[str]
|
|
366
|
+
External dependencies
|
|
367
|
+
|
|
368
|
+
Returns
|
|
369
|
+
-------
|
|
370
|
+
dict[str, Any]
|
|
371
|
+
Template context with special variables + parameters
|
|
372
|
+
"""
|
|
373
|
+
return {
|
|
374
|
+
# Special variables
|
|
375
|
+
"name": instance_name,
|
|
376
|
+
"dependencies": dependencies,
|
|
377
|
+
# All validated parameters
|
|
378
|
+
**validated_inputs,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
def _render_node_templates(
|
|
382
|
+
self,
|
|
383
|
+
node_templates: list[dict[str, Any]],
|
|
384
|
+
context: dict[str, Any],
|
|
385
|
+
) -> list[dict[str, Any]]:
|
|
386
|
+
"""Render node templates with Jinja2.
|
|
387
|
+
|
|
388
|
+
Parameters
|
|
389
|
+
----------
|
|
390
|
+
node_templates : list[dict[str, Any]]
|
|
391
|
+
Node template definitions
|
|
392
|
+
context : dict[str, Any]
|
|
393
|
+
Template rendering context
|
|
394
|
+
|
|
395
|
+
Returns
|
|
396
|
+
-------
|
|
397
|
+
list[dict[str, Any]]
|
|
398
|
+
Rendered node configurations
|
|
399
|
+
|
|
400
|
+
Raises
|
|
401
|
+
------
|
|
402
|
+
ValueError
|
|
403
|
+
If template rendering fails
|
|
404
|
+
"""
|
|
405
|
+
rendered = []
|
|
406
|
+
|
|
407
|
+
for i, node_template in enumerate(node_templates):
|
|
408
|
+
try:
|
|
409
|
+
rendered_node = self._render_dict_recursive(node_template, context)
|
|
410
|
+
rendered.append(rendered_node)
|
|
411
|
+
except (TemplateSyntaxError, UndefinedError) as e:
|
|
412
|
+
config: YamlMacroConfig = self.config # type: ignore[assignment]
|
|
413
|
+
raise ValueError(
|
|
414
|
+
f"Failed to render node template {i} in macro '{config.macro_name}': {e}"
|
|
415
|
+
) from e
|
|
416
|
+
|
|
417
|
+
return rendered
|
|
418
|
+
|
|
419
|
+
def _render_dict_recursive(self, obj: Any, context: dict[str, Any]) -> Any:
|
|
420
|
+
"""Recursively render Jinja2 templates in nested structures.
|
|
421
|
+
|
|
422
|
+
Uses PreserveUndefined to enable partial rendering:
|
|
423
|
+
- Macro variables ({{name}}, {{param}}) are replaced with actual values
|
|
424
|
+
- Runtime variables ({{node.output}}) are preserved as-is for execution time
|
|
425
|
+
|
|
426
|
+
Example:
|
|
427
|
+
Template: "Macro: {{name}}, Runtime: {{other_node.result}}"
|
|
428
|
+
Context: {"name": "my_instance"}
|
|
429
|
+
Result: "Macro: my_instance, Runtime: {{other_node.result}}"
|
|
430
|
+
|
|
431
|
+
Parameters
|
|
432
|
+
----------
|
|
433
|
+
obj : Any
|
|
434
|
+
Object to render (dict, list, str, or primitive)
|
|
435
|
+
context : dict[str, Any]
|
|
436
|
+
Template context
|
|
437
|
+
|
|
438
|
+
Returns
|
|
439
|
+
-------
|
|
440
|
+
Any
|
|
441
|
+
Rendered object with same structure
|
|
442
|
+
"""
|
|
443
|
+
if isinstance(obj, str):
|
|
444
|
+
# Render string template (undefined vars are preserved)
|
|
445
|
+
if "{{" in obj or "{%" in obj:
|
|
446
|
+
template = self.jinja_env.from_string(obj)
|
|
447
|
+
return template.render(context)
|
|
448
|
+
return obj
|
|
449
|
+
|
|
450
|
+
if isinstance(obj, dict):
|
|
451
|
+
# Render dict values recursively
|
|
452
|
+
return {k: self._render_dict_recursive(v, context) for k, v in obj.items()}
|
|
453
|
+
|
|
454
|
+
if isinstance(obj, list):
|
|
455
|
+
# Render list items recursively
|
|
456
|
+
return [self._render_dict_recursive(item, context) for item in obj]
|
|
457
|
+
|
|
458
|
+
# Primitives (int, float, bool, None)
|
|
459
|
+
return obj
|
|
460
|
+
|
|
461
|
+
def _build_graph_from_nodes(self, rendered_nodes: list[dict[str, Any]]) -> DirectedGraph:
|
|
462
|
+
"""Build DirectedGraph from rendered node configurations.
|
|
463
|
+
|
|
464
|
+
This uses the same YamlPipelineBuilder logic to build nodes,
|
|
465
|
+
ensuring consistency with regular YAML pipelines.
|
|
466
|
+
|
|
467
|
+
Parameters
|
|
468
|
+
----------
|
|
469
|
+
rendered_nodes : list[dict[str, Any]]
|
|
470
|
+
Rendered node configurations
|
|
471
|
+
|
|
472
|
+
Returns
|
|
473
|
+
-------
|
|
474
|
+
DirectedGraph
|
|
475
|
+
Graph with all nodes added
|
|
476
|
+
|
|
477
|
+
Raises
|
|
478
|
+
------
|
|
479
|
+
ValueError
|
|
480
|
+
If node building fails
|
|
481
|
+
"""
|
|
482
|
+
# Import here to avoid circular dependency
|
|
483
|
+
from hexdag.core.pipeline_builder.yaml_builder import NodeEntityPlugin, YamlPipelineBuilder
|
|
484
|
+
|
|
485
|
+
# Create temporary builder for node construction
|
|
486
|
+
builder = YamlPipelineBuilder()
|
|
487
|
+
graph = DirectedGraph()
|
|
488
|
+
|
|
489
|
+
# Use NodeEntityPlugin to build each node
|
|
490
|
+
node_plugin = NodeEntityPlugin(builder)
|
|
491
|
+
|
|
492
|
+
for node_config in rendered_nodes:
|
|
493
|
+
# Validate it's a node (not another macro_invocation)
|
|
494
|
+
if node_config.get("kind") == "macro_invocation":
|
|
495
|
+
raise ValueError(
|
|
496
|
+
"YAML macros cannot contain nested macro_invocations. "
|
|
497
|
+
"Use Python macros for composition."
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
if not node_plugin.can_handle(node_config):
|
|
501
|
+
kind = node_config.get("kind", "unknown")
|
|
502
|
+
raise ValueError(f"Invalid node kind in YAML macro: {kind}")
|
|
503
|
+
|
|
504
|
+
# Build node using existing infrastructure
|
|
505
|
+
node_spec = node_plugin.build(node_config, builder, graph)
|
|
506
|
+
graph += node_spec
|
|
507
|
+
|
|
508
|
+
return graph
|
|
509
|
+
|
|
510
|
+
def __repr__(self) -> str:
|
|
511
|
+
"""String representation for debugging."""
|
|
512
|
+
config: YamlMacroConfig = self.config # type: ignore[assignment]
|
|
513
|
+
return (
|
|
514
|
+
f"YamlMacro(name='{config.macro_name}', "
|
|
515
|
+
f"parameters={len(config.parameters)}, "
|
|
516
|
+
f"nodes={len(config.nodes)})"
|
|
517
|
+
)
|