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,418 @@
|
|
|
1
|
+
"""Models for orchestration state and checkpoints.
|
|
2
|
+
|
|
3
|
+
This module contains models for representing orchestrator execution
|
|
4
|
+
state, execution context, and human-in-the-loop approval requests.
|
|
5
|
+
It also includes port configuration models for managing per-node and
|
|
6
|
+
per-type port customization.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, ConfigDict
|
|
15
|
+
|
|
16
|
+
from hexdag.core.exceptions import ValidationError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class OrchestratorConfig:
|
|
21
|
+
"""Configuration for orchestrator behavior.
|
|
22
|
+
|
|
23
|
+
This immutable configuration object centralizes all orchestrator settings,
|
|
24
|
+
making it easier to pass configurations around and test different settings.
|
|
25
|
+
|
|
26
|
+
Attributes
|
|
27
|
+
----------
|
|
28
|
+
max_concurrent_nodes : int, default=10
|
|
29
|
+
Maximum number of nodes to execute concurrently in a wave.
|
|
30
|
+
Controls parallelism and resource usage.
|
|
31
|
+
strict_validation : bool, default=False
|
|
32
|
+
If True, raise errors on validation failures.
|
|
33
|
+
If False, log warnings and continue execution.
|
|
34
|
+
default_node_timeout : float | None, default=None
|
|
35
|
+
Default timeout in seconds for node execution.
|
|
36
|
+
None means no timeout. Can be overridden per-node.
|
|
37
|
+
|
|
38
|
+
Examples
|
|
39
|
+
--------
|
|
40
|
+
Example usage::
|
|
41
|
+
|
|
42
|
+
config = OrchestratorConfig(
|
|
43
|
+
max_concurrent_nodes=5,
|
|
44
|
+
strict_validation=True,
|
|
45
|
+
default_node_timeout=30.0
|
|
46
|
+
)
|
|
47
|
+
orchestrator = Orchestrator(config=config)
|
|
48
|
+
|
|
49
|
+
# Or use defaults
|
|
50
|
+
config = OrchestratorConfig()
|
|
51
|
+
config.max_concurrent_nodes
|
|
52
|
+
10
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
max_concurrent_nodes: int = 10
|
|
56
|
+
strict_validation: bool = False
|
|
57
|
+
default_node_timeout: float | None = None
|
|
58
|
+
|
|
59
|
+
def __post_init__(self) -> None:
|
|
60
|
+
"""Validate configuration parameters."""
|
|
61
|
+
if self.max_concurrent_nodes <= 0:
|
|
62
|
+
raise ValidationError(
|
|
63
|
+
"max_concurrent_nodes", "must be positive", self.max_concurrent_nodes
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if self.default_node_timeout is not None and self.default_node_timeout <= 0:
|
|
67
|
+
raise ValidationError(
|
|
68
|
+
"default_node_timeout", "must be positive or None", self.default_node_timeout
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class CheckpointState(BaseModel):
|
|
73
|
+
"""Complete state for checkpoint/resume.
|
|
74
|
+
|
|
75
|
+
Saves everything needed to resume a DAG execution from where it left off,
|
|
76
|
+
including the full graph structure (handles both static and dynamic DAGs).
|
|
77
|
+
|
|
78
|
+
Attributes
|
|
79
|
+
----------
|
|
80
|
+
run_id : str
|
|
81
|
+
Unique identifier for this execution run
|
|
82
|
+
dag_id : str
|
|
83
|
+
Stable identifier for the DAG (e.g., YAML file path, function name)
|
|
84
|
+
graph_snapshot : dict[str, Any]
|
|
85
|
+
Serialized DirectedGraph structure
|
|
86
|
+
Format: {"nodes": {...}, "edges": [...]}
|
|
87
|
+
initial_input : Any
|
|
88
|
+
Initial input data passed to the DAG
|
|
89
|
+
node_results : dict[str, Any]
|
|
90
|
+
Results from completed nodes (node_id -> output)
|
|
91
|
+
completed_node_ids : list[str]
|
|
92
|
+
Ordered list of completed node IDs (preserves execution order)
|
|
93
|
+
failed_node_ids : list[str]
|
|
94
|
+
List of node IDs that failed (for retry/debugging)
|
|
95
|
+
created_at : datetime
|
|
96
|
+
When execution started
|
|
97
|
+
updated_at : datetime
|
|
98
|
+
Last checkpoint save time
|
|
99
|
+
metadata : dict[str, Any]
|
|
100
|
+
Optional metadata (custom fields, tags, etc.)
|
|
101
|
+
|
|
102
|
+
Notes
|
|
103
|
+
-----
|
|
104
|
+
To resume execution:
|
|
105
|
+
1. Load CheckpointState by run_id
|
|
106
|
+
2. Deserialize graph_snapshot to DirectedGraph
|
|
107
|
+
3. Filter out completed nodes using completed_node_ids
|
|
108
|
+
4. Resume execution with filtered graph and node_results
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
112
|
+
|
|
113
|
+
run_id: str
|
|
114
|
+
dag_id: str
|
|
115
|
+
graph_snapshot: dict[str, Any] # Always save the graph
|
|
116
|
+
initial_input: Any
|
|
117
|
+
node_results: dict[str, Any] # node_id -> output
|
|
118
|
+
completed_node_ids: list[str] # Ordered
|
|
119
|
+
failed_node_ids: list[str] = []
|
|
120
|
+
created_at: datetime
|
|
121
|
+
updated_at: datetime
|
|
122
|
+
metadata: dict[str, Any] = {}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(slots=True)
|
|
126
|
+
class NodeExecutionContext:
|
|
127
|
+
"""Lightweight context tracking current execution position.
|
|
128
|
+
|
|
129
|
+
This flows through the execution pipeline and is NOT persisted in checkpoints.
|
|
130
|
+
It's only for tracking where we are during live execution (which node, wave, attempt).
|
|
131
|
+
|
|
132
|
+
Attributes
|
|
133
|
+
----------
|
|
134
|
+
dag_id : str
|
|
135
|
+
Identifier for the DAG being executed
|
|
136
|
+
node_id : str | None
|
|
137
|
+
Current node being executed (None for DAG-level operations)
|
|
138
|
+
wave_index : int
|
|
139
|
+
Index of the current execution wave (for parallel execution tracking)
|
|
140
|
+
attempt : int
|
|
141
|
+
Attempt number (for retry scenarios)
|
|
142
|
+
metadata : dict[str, Any]
|
|
143
|
+
Additional metadata that can be attached to the context
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
dag_id: str
|
|
147
|
+
node_id: str | None = None
|
|
148
|
+
wave_index: int = 0
|
|
149
|
+
attempt: int = 1
|
|
150
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
151
|
+
|
|
152
|
+
def with_node(self, node_id: str, wave_index: int) -> "NodeExecutionContext":
|
|
153
|
+
"""Create new context for a specific node execution.
|
|
154
|
+
|
|
155
|
+
Args
|
|
156
|
+
----
|
|
157
|
+
node_id: The ID of the node being executed
|
|
158
|
+
wave_index: The wave index for parallel execution tracking
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
NodeExecutionContext
|
|
163
|
+
New context with updated node and wave information
|
|
164
|
+
"""
|
|
165
|
+
return NodeExecutionContext(
|
|
166
|
+
dag_id=self.dag_id,
|
|
167
|
+
node_id=node_id,
|
|
168
|
+
wave_index=wave_index,
|
|
169
|
+
attempt=self.attempt,
|
|
170
|
+
metadata=self.metadata.copy(),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def with_attempt(self, attempt: int) -> "NodeExecutionContext":
|
|
174
|
+
"""Create new context with updated attempt number.
|
|
175
|
+
|
|
176
|
+
Args
|
|
177
|
+
----
|
|
178
|
+
attempt: The attempt number (for retry scenarios)
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
NodeExecutionContext
|
|
183
|
+
New context with updated attempt number
|
|
184
|
+
"""
|
|
185
|
+
return NodeExecutionContext(
|
|
186
|
+
dag_id=self.dag_id,
|
|
187
|
+
node_id=self.node_id,
|
|
188
|
+
wave_index=self.wave_index,
|
|
189
|
+
attempt=attempt,
|
|
190
|
+
metadata=self.metadata.copy(),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass(frozen=True, slots=True)
|
|
195
|
+
class PortConfig:
|
|
196
|
+
"""Configuration for a single port instance.
|
|
197
|
+
|
|
198
|
+
A PortConfig wraps a port implementation with optional metadata,
|
|
199
|
+
allowing for fine-grained control over port behavior per node.
|
|
200
|
+
|
|
201
|
+
Attributes
|
|
202
|
+
----------
|
|
203
|
+
port : Any
|
|
204
|
+
The port implementation instance (e.g., LLM, Database, Memory adapter)
|
|
205
|
+
metadata : Mapping[str, Any] | None
|
|
206
|
+
Optional metadata for the port (e.g., timeouts, retry settings, rate limits)
|
|
207
|
+
|
|
208
|
+
Examples
|
|
209
|
+
--------
|
|
210
|
+
Example usage::
|
|
211
|
+
|
|
212
|
+
from hexdag.builtin.adapters.mock import MockLLM
|
|
213
|
+
config = PortConfig(
|
|
214
|
+
port=MockLLM(),
|
|
215
|
+
metadata={"timeout": 30, "max_retries": 3}
|
|
216
|
+
)
|
|
217
|
+
config.port
|
|
218
|
+
<MockLLM object>
|
|
219
|
+
config.get_metadata()
|
|
220
|
+
{'timeout': 30, 'max_retries': 3}
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
port: Any
|
|
224
|
+
metadata: Mapping[str, Any] | None = None
|
|
225
|
+
|
|
226
|
+
def __post_init__(self) -> None:
|
|
227
|
+
"""Ensure metadata is immutable if provided."""
|
|
228
|
+
if self.metadata is not None:
|
|
229
|
+
object.__setattr__(self, "metadata", tuple(self.metadata.items()))
|
|
230
|
+
|
|
231
|
+
def get_metadata(self) -> dict[str, Any]:
|
|
232
|
+
"""Get metadata as a dictionary.
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
dict[str, Any]
|
|
237
|
+
Metadata dictionary (empty if no metadata)
|
|
238
|
+
"""
|
|
239
|
+
if self.metadata is None:
|
|
240
|
+
return {}
|
|
241
|
+
return dict(self.metadata)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass(frozen=True, slots=True)
|
|
245
|
+
class PortsConfiguration:
|
|
246
|
+
"""Complete port configuration with inheritance and overrides.
|
|
247
|
+
|
|
248
|
+
This model supports three levels of port configuration with clear inheritance:
|
|
249
|
+
1. **Global defaults** - Apply to all nodes unless overridden
|
|
250
|
+
2. **Per-type defaults** - Apply to all nodes of a specific type (e.g., "agent", "llm")
|
|
251
|
+
3. **Per-node overrides** - Apply to specific nodes by name
|
|
252
|
+
|
|
253
|
+
**Resolution order**: per-node > per-type > global defaults
|
|
254
|
+
|
|
255
|
+
Attributes
|
|
256
|
+
----------
|
|
257
|
+
global_ports : Mapping[str, PortConfig] | None
|
|
258
|
+
Default ports for all nodes
|
|
259
|
+
type_ports : Mapping[str, Mapping[str, PortConfig]] | None
|
|
260
|
+
Ports per node type, keyed by type name (e.g., {"agent": {"llm": config}})
|
|
261
|
+
node_ports : Mapping[str, Mapping[str, PortConfig]] | None
|
|
262
|
+
Ports per specific node name (e.g., {"researcher": {"llm": config}})
|
|
263
|
+
|
|
264
|
+
Examples
|
|
265
|
+
--------
|
|
266
|
+
Example usage::
|
|
267
|
+
|
|
268
|
+
from hexdag.builtin.adapters.mock import MockLLM
|
|
269
|
+
from hexdag.builtin.adapters.openai import OpenAIAdapter
|
|
270
|
+
from hexdag.builtin.adapters.anthropic import AnthropicAdapter
|
|
271
|
+
|
|
272
|
+
# Global default: All nodes use MockLLM
|
|
273
|
+
config = PortsConfiguration(
|
|
274
|
+
global_ports={"llm": PortConfig(MockLLM())},
|
|
275
|
+
type_ports={
|
|
276
|
+
# Override for all "agent" type nodes
|
|
277
|
+
"agent": {"llm": PortConfig(OpenAIAdapter(model="gpt-4"))}
|
|
278
|
+
},
|
|
279
|
+
node_ports={
|
|
280
|
+
# Override for specific "researcher" node
|
|
281
|
+
"researcher": {"llm": PortConfig(AnthropicAdapter(model="claude-3"))}
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Resolution for different nodes:
|
|
286
|
+
# - "researcher" node: AnthropicAdapter (per-node override)
|
|
287
|
+
researcher_ports = config.resolve_ports("researcher", "agent")
|
|
288
|
+
assert isinstance(researcher_ports["llm"].port, AnthropicAdapter)
|
|
289
|
+
|
|
290
|
+
# - Other "agent" nodes: OpenAIAdapter (per-type default)
|
|
291
|
+
agent_ports = config.resolve_ports("analyzer", "agent")
|
|
292
|
+
assert isinstance(agent_ports["llm"].port, OpenAIAdapter)
|
|
293
|
+
|
|
294
|
+
# - Other nodes: MockLLM (global default)
|
|
295
|
+
function_ports = config.resolve_ports("transformer", "function")
|
|
296
|
+
assert isinstance(function_ports["llm"].port, MockLLM)
|
|
297
|
+
|
|
298
|
+
Notes
|
|
299
|
+
-----
|
|
300
|
+
This design enables:
|
|
301
|
+
- **Cost optimization**: Use cheaper models for simple nodes, expensive ones for complex tasks
|
|
302
|
+
- **Performance tuning**: Different timeout/retry settings per node type
|
|
303
|
+
- **Testing flexibility**: Mock some nodes, use real adapters for others
|
|
304
|
+
- **Multi-tenant support**: Different credentials per node/type
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
global_ports: Mapping[str, PortConfig] | None = None
|
|
308
|
+
type_ports: Mapping[str, Mapping[str, PortConfig]] | None = None
|
|
309
|
+
node_ports: Mapping[str, Mapping[str, PortConfig]] | None = None
|
|
310
|
+
|
|
311
|
+
def __post_init__(self) -> None:
|
|
312
|
+
"""Ensure all mappings are immutable."""
|
|
313
|
+
if self.global_ports is not None:
|
|
314
|
+
object.__setattr__(self, "global_ports", tuple(self.global_ports.items()))
|
|
315
|
+
if self.type_ports is not None:
|
|
316
|
+
type_items = tuple(
|
|
317
|
+
(node_type, tuple(ports.items())) for node_type, ports in self.type_ports.items()
|
|
318
|
+
)
|
|
319
|
+
object.__setattr__(self, "type_ports", type_items)
|
|
320
|
+
if self.node_ports is not None:
|
|
321
|
+
node_items = tuple(
|
|
322
|
+
(node_name, tuple(ports.items())) for node_name, ports in self.node_ports.items()
|
|
323
|
+
)
|
|
324
|
+
object.__setattr__(self, "node_ports", node_items)
|
|
325
|
+
|
|
326
|
+
def resolve_ports(self, node_name: str, node_type: str | None = None) -> dict[str, PortConfig]:
|
|
327
|
+
"""Resolve ports for a specific node following inheritance rules.
|
|
328
|
+
|
|
329
|
+
Combines ports from all three levels (global, type, node) with proper
|
|
330
|
+
precedence: per-node > per-type > global defaults.
|
|
331
|
+
|
|
332
|
+
Parameters
|
|
333
|
+
----------
|
|
334
|
+
node_name : str
|
|
335
|
+
Name of the node to resolve ports for
|
|
336
|
+
node_type : str | None
|
|
337
|
+
Type of the node (e.g., "llm", "agent", "function", "loop")
|
|
338
|
+
|
|
339
|
+
Returns
|
|
340
|
+
-------
|
|
341
|
+
dict[str, PortConfig]
|
|
342
|
+
Resolved ports for the node with PortConfig wrappers
|
|
343
|
+
|
|
344
|
+
Examples
|
|
345
|
+
--------
|
|
346
|
+
Example usage::
|
|
347
|
+
|
|
348
|
+
config = PortsConfiguration(
|
|
349
|
+
global_ports={"llm": PortConfig(MockLLM())},
|
|
350
|
+
type_ports={"agent": {"llm": PortConfig(OpenAIAdapter())}},
|
|
351
|
+
node_ports={"researcher": {"llm": PortConfig(AnthropicAdapter())}}
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Researcher node gets Anthropic (per-node override)
|
|
355
|
+
researcher_ports = config.resolve_ports("researcher", "agent")
|
|
356
|
+
assert isinstance(researcher_ports["llm"].port, AnthropicAdapter)
|
|
357
|
+
|
|
358
|
+
# Other agent nodes get OpenAI (per-type default)
|
|
359
|
+
agent_ports = config.resolve_ports("analyzer", "agent")
|
|
360
|
+
assert isinstance(agent_ports["llm"].port, OpenAIAdapter)
|
|
361
|
+
|
|
362
|
+
# Function nodes get Mock (global default)
|
|
363
|
+
function_ports = config.resolve_ports("transformer", "function")
|
|
364
|
+
assert isinstance(function_ports["llm"].port, MockLLM)
|
|
365
|
+
"""
|
|
366
|
+
result: dict[str, PortConfig] = {}
|
|
367
|
+
|
|
368
|
+
# 1. Start with global defaults (lowest priority)
|
|
369
|
+
if self.global_ports is not None:
|
|
370
|
+
result.update(dict(self.global_ports))
|
|
371
|
+
|
|
372
|
+
# 2. Apply per-type defaults (overrides global)
|
|
373
|
+
if self.type_ports is not None and node_type is not None:
|
|
374
|
+
type_dict = dict(self.type_ports)
|
|
375
|
+
if node_type in type_dict:
|
|
376
|
+
result.update(dict(type_dict[node_type]))
|
|
377
|
+
|
|
378
|
+
# 3. Apply per-node overrides (highest priority)
|
|
379
|
+
if self.node_ports is not None:
|
|
380
|
+
node_dict = dict(self.node_ports)
|
|
381
|
+
if node_name in node_dict:
|
|
382
|
+
result.update(dict(node_dict[node_name]))
|
|
383
|
+
|
|
384
|
+
return result
|
|
385
|
+
|
|
386
|
+
def to_flat_dict(self, node_name: str, node_type: str | None = None) -> dict[str, Any]:
|
|
387
|
+
"""Convert resolved ports to flat dictionary of port instances.
|
|
388
|
+
|
|
389
|
+
This extracts the actual port instances from PortConfig wrappers,
|
|
390
|
+
providing backward compatibility with the current orchestrator
|
|
391
|
+
interface that expects `dict[str, Any]` ports.
|
|
392
|
+
|
|
393
|
+
Parameters
|
|
394
|
+
----------
|
|
395
|
+
node_name : str
|
|
396
|
+
Name of the node
|
|
397
|
+
node_type : str | None
|
|
398
|
+
Type of the node
|
|
399
|
+
|
|
400
|
+
Returns
|
|
401
|
+
-------
|
|
402
|
+
dict[str, Any]
|
|
403
|
+
Dictionary mapping port names to port instances (unwrapped)
|
|
404
|
+
|
|
405
|
+
Examples
|
|
406
|
+
--------
|
|
407
|
+
Example usage::
|
|
408
|
+
|
|
409
|
+
config = PortsConfiguration(
|
|
410
|
+
global_ports={"llm": PortConfig(MockLLM())}
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
ports = config.to_flat_dict("my_node")
|
|
414
|
+
assert "llm" in ports
|
|
415
|
+
assert isinstance(ports["llm"], MockLLM) # Unwrapped
|
|
416
|
+
"""
|
|
417
|
+
resolved = self.resolve_ports(node_name, node_type)
|
|
418
|
+
return {port_name: config.port for port_name, config in resolved.items()}
|