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,158 @@
|
|
|
1
|
+
"""WaveExecutor - Handles parallel execution of nodes within waves.
|
|
2
|
+
|
|
3
|
+
Extracted from Orchestrator to provide a focused component for concurrent
|
|
4
|
+
wave-based execution with semaphore-based concurrency limiting.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from hexdag.core.domain.dag import DirectedGraph
|
|
12
|
+
from hexdag.core.orchestration.events import WaveCompleted, WaveStarted
|
|
13
|
+
from hexdag.core.orchestration.models import NodeExecutionContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WaveExecutor:
|
|
17
|
+
"""Executes waves of nodes concurrently with resource management.
|
|
18
|
+
|
|
19
|
+
Responsibilities:
|
|
20
|
+
- Execute all waves in topological order
|
|
21
|
+
- Manage concurrency limits via semaphore
|
|
22
|
+
- Handle timeout for overall wave execution
|
|
23
|
+
- Emit wave-level events
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
max_concurrent_nodes : int
|
|
28
|
+
Maximum number of nodes that can execute concurrently
|
|
29
|
+
semaphore : asyncio.Semaphore | None
|
|
30
|
+
Optional semaphore for concurrency control. If None, creates one.
|
|
31
|
+
|
|
32
|
+
Examples
|
|
33
|
+
--------
|
|
34
|
+
Example usage::
|
|
35
|
+
|
|
36
|
+
executor = WaveExecutor(max_concurrent_nodes=10)
|
|
37
|
+
cancelled = await executor.execute_all_waves(
|
|
38
|
+
waves=waves,
|
|
39
|
+
node_executor_fn=orchestrator._execute_node,
|
|
40
|
+
...
|
|
41
|
+
)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
max_concurrent_nodes: int = 10,
|
|
47
|
+
semaphore: asyncio.Semaphore | None = None,
|
|
48
|
+
):
|
|
49
|
+
self.max_concurrent_nodes = max_concurrent_nodes
|
|
50
|
+
self._semaphore = semaphore or asyncio.Semaphore(max_concurrent_nodes)
|
|
51
|
+
|
|
52
|
+
async def execute_all_waves(
|
|
53
|
+
self,
|
|
54
|
+
waves: list[list[str]],
|
|
55
|
+
node_executor_fn: Any, # Callable for executing a single node
|
|
56
|
+
graph: DirectedGraph,
|
|
57
|
+
node_results: dict[str, Any],
|
|
58
|
+
initial_input: Any,
|
|
59
|
+
context: NodeExecutionContext,
|
|
60
|
+
coordinator: Any, # ExecutionCoordinator
|
|
61
|
+
timeout: float | None,
|
|
62
|
+
validate: bool,
|
|
63
|
+
**kwargs: Any,
|
|
64
|
+
) -> bool:
|
|
65
|
+
"""Execute all waves with optional timeout."""
|
|
66
|
+
from hexdag.core.context import get_observer_manager
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
async with asyncio.timeout(timeout):
|
|
70
|
+
for wave_idx, wave in enumerate(waves, 1):
|
|
71
|
+
wave_start_time = time.time()
|
|
72
|
+
|
|
73
|
+
wave_event = WaveStarted(wave_index=wave_idx, nodes=wave)
|
|
74
|
+
await coordinator.notify_observer(get_observer_manager(), wave_event)
|
|
75
|
+
|
|
76
|
+
wave_results = await self._execute_wave(
|
|
77
|
+
wave=wave,
|
|
78
|
+
node_executor_fn=node_executor_fn,
|
|
79
|
+
graph=graph,
|
|
80
|
+
node_results=node_results,
|
|
81
|
+
initial_input=initial_input,
|
|
82
|
+
context=context,
|
|
83
|
+
wave_index=wave_idx,
|
|
84
|
+
validate=validate,
|
|
85
|
+
**kwargs,
|
|
86
|
+
)
|
|
87
|
+
node_results.update(wave_results)
|
|
88
|
+
|
|
89
|
+
wave_completed = WaveCompleted(
|
|
90
|
+
wave_index=wave_idx,
|
|
91
|
+
duration_ms=(time.time() - wave_start_time) * 1000,
|
|
92
|
+
)
|
|
93
|
+
await coordinator.notify_observer(get_observer_manager(), wave_completed)
|
|
94
|
+
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
except TimeoutError:
|
|
98
|
+
return True
|
|
99
|
+
|
|
100
|
+
async def _execute_wave(
|
|
101
|
+
self,
|
|
102
|
+
wave: list[str],
|
|
103
|
+
node_executor_fn: Any,
|
|
104
|
+
graph: DirectedGraph,
|
|
105
|
+
node_results: dict[str, Any],
|
|
106
|
+
initial_input: Any,
|
|
107
|
+
context: NodeExecutionContext,
|
|
108
|
+
wave_index: int,
|
|
109
|
+
validate: bool,
|
|
110
|
+
**kwargs: Any,
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
"""Execute all nodes in a wave with concurrency limiting."""
|
|
113
|
+
|
|
114
|
+
async def execute_with_semaphore(node_name: str) -> tuple[str, Any]:
|
|
115
|
+
async with self._semaphore:
|
|
116
|
+
result = await node_executor_fn(
|
|
117
|
+
node_name=node_name,
|
|
118
|
+
graph=graph,
|
|
119
|
+
node_results=node_results,
|
|
120
|
+
initial_input=initial_input,
|
|
121
|
+
context=context,
|
|
122
|
+
wave_index=wave_index,
|
|
123
|
+
validate=validate,
|
|
124
|
+
**kwargs,
|
|
125
|
+
)
|
|
126
|
+
return node_name, result
|
|
127
|
+
|
|
128
|
+
wave_size = len(wave)
|
|
129
|
+
coros: list[Any] = [None] * wave_size
|
|
130
|
+
for i, node_name in enumerate(wave):
|
|
131
|
+
coros[i] = execute_with_semaphore(node_name)
|
|
132
|
+
|
|
133
|
+
wave_outputs = await asyncio.gather(*coros, return_exceptions=True)
|
|
134
|
+
|
|
135
|
+
wave_results = {}
|
|
136
|
+
exceptions: list[tuple[str | None, Exception]] | None = None
|
|
137
|
+
|
|
138
|
+
for output in wave_outputs:
|
|
139
|
+
if isinstance(output, Exception):
|
|
140
|
+
if exceptions is None:
|
|
141
|
+
exceptions = []
|
|
142
|
+
exceptions.append((None, output))
|
|
143
|
+
elif isinstance(output, BaseException):
|
|
144
|
+
raise output
|
|
145
|
+
else:
|
|
146
|
+
node_name, result = output
|
|
147
|
+
wave_results[node_name] = result
|
|
148
|
+
|
|
149
|
+
if exceptions:
|
|
150
|
+
if len(exceptions) == 1:
|
|
151
|
+
raise exceptions[0][1]
|
|
152
|
+
exception_list = [exc for _, exc in exceptions]
|
|
153
|
+
raise ExceptionGroup(
|
|
154
|
+
f"Multiple node failures in wave {wave_index} ({len(exceptions)} nodes failed)",
|
|
155
|
+
exception_list,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return wave_results
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Constants for orchestration execution.
|
|
2
|
+
|
|
3
|
+
This module defines shared constants used between orchestrator and executors
|
|
4
|
+
to avoid magic strings and ensure consistency.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Executor context keys - used to store graph/results in ports context
|
|
8
|
+
# These are prefixed with _hexdag_ to avoid collisions with user ports
|
|
9
|
+
EXECUTOR_CONTEXT_GRAPH = "_hexdag_graph"
|
|
10
|
+
EXECUTOR_CONTEXT_NODE_RESULTS = "_hexdag_node_results"
|
|
11
|
+
EXECUTOR_CONTEXT_INITIAL_INPUT = "_hexdag_initial_input"
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"EXECUTOR_CONTEXT_GRAPH",
|
|
15
|
+
"EXECUTOR_CONTEXT_NODE_RESULTS",
|
|
16
|
+
"EXECUTOR_CONTEXT_INITIAL_INPUT",
|
|
17
|
+
]
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# 📊 Event System Architecture
|
|
2
|
+
|
|
3
|
+
> **A high-performance, production-ready event system for DAG orchestration with clean separation of observation and control.**
|
|
4
|
+
|
|
5
|
+
## 🎯 Overview
|
|
6
|
+
|
|
7
|
+
The event system provides observability and control capabilities through a dual-manager architecture that separates **read-only observation** from **execution control**. Built on hexagonal architecture principles, it enables real-time monitoring and dynamic execution control without coupling.
|
|
8
|
+
|
|
9
|
+
## 🏗️ Architecture
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
┌─────────────────────────────────────────────────────┐
|
|
13
|
+
│ Orchestrator │
|
|
14
|
+
│ │
|
|
15
|
+
│ ┌──────────────┐ ┌──────────────┐ │
|
|
16
|
+
│ │ Control │ │ Observer │ │
|
|
17
|
+
│ │ Manager │ │ Manager │ │
|
|
18
|
+
│ └──────────────┘ └──────────────┘ │
|
|
19
|
+
│ ▲ ▲ │
|
|
20
|
+
│ │ │ │
|
|
21
|
+
│ ControlResponse (fire & forget) │
|
|
22
|
+
│ │ │ │
|
|
23
|
+
└─────────┼─────────────────────────────┼─────────────┘
|
|
24
|
+
│ │
|
|
25
|
+
┌─────▼──────┐ ┌──────▼──────┐
|
|
26
|
+
│ Control │ │ Observer │
|
|
27
|
+
│ Handlers │ │ Functions │
|
|
28
|
+
└────────────┘ └─────────────┘
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Dual-Manager System
|
|
32
|
+
|
|
33
|
+
#### **ObserverManager** - Fire-and-Forget Telemetry
|
|
34
|
+
*"Tell me what happened, but don't interfere"*
|
|
35
|
+
|
|
36
|
+
- **Purpose**: Logging, metrics, monitoring, alerts, audit trails
|
|
37
|
+
- **Behavior**: Async fire-and-forget with concurrent execution
|
|
38
|
+
- **Fault Isolation**: Observer failures never crash the pipeline
|
|
39
|
+
- **Event Filtering**: Subscribe only to relevant event types
|
|
40
|
+
- **Performance**: ThreadPoolExecutor for sync functions, semaphore-based concurrency
|
|
41
|
+
|
|
42
|
+
#### **ControlManager** - Policy Enforcement
|
|
43
|
+
*"Check if this should proceed and how"*
|
|
44
|
+
|
|
45
|
+
- **Purpose**: Retry policies, circuit breakers, rate limiting, authorization
|
|
46
|
+
- **Behavior**: Priority-based execution with veto pattern
|
|
47
|
+
- **Control Signals**: PROCEED, SKIP, RETRY, FALLBACK, FAIL, ERROR
|
|
48
|
+
- **Event Filtering**: Process only relevant events for ~90% efficiency gain
|
|
49
|
+
- **Performance**: Direct heap iteration O(n), lazy deletion with cleanup
|
|
50
|
+
|
|
51
|
+
## 📁 Module Structure
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
events/
|
|
55
|
+
├── events.py # Pure event data classes
|
|
56
|
+
├── observer_manager.py # Read-only observation
|
|
57
|
+
├── control_manager.py # Execution control policies
|
|
58
|
+
├── models.py # Protocols and base classes
|
|
59
|
+
├── config.py # Configuration and null implementations
|
|
60
|
+
└── README.md # This file
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Component Details
|
|
64
|
+
|
|
65
|
+
#### **events.py** - Event Data Classes
|
|
66
|
+
Pure immutable data classes representing pipeline events:
|
|
67
|
+
```python
|
|
68
|
+
NodeStarted, NodeCompleted, NodeFailed # Node lifecycle
|
|
69
|
+
WaveStarted, WaveCompleted # Wave execution
|
|
70
|
+
PipelineStarted, PipelineCompleted # Pipeline lifecycle
|
|
71
|
+
ToolCalled, ToolCompleted # Tool usage
|
|
72
|
+
LLMPromptSent, LLMResponseReceived # LLM interactions
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
#### **models.py** - Shared Interfaces
|
|
76
|
+
```python
|
|
77
|
+
Observer # Protocol for observers
|
|
78
|
+
ControlHandler # Protocol for control handlers
|
|
79
|
+
BaseEventManager # Common manager functionality
|
|
80
|
+
ExecutionContext # Runtime context with retry tracking
|
|
81
|
+
ControlResponse # Response with signal and data
|
|
82
|
+
ControlSignal # Enum: PROCEED, SKIP, RETRY, etc.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 🚀 Usage Examples
|
|
86
|
+
|
|
87
|
+
### Observer Pattern - Metrics Collection
|
|
88
|
+
```python
|
|
89
|
+
from hexdag.builtin.events import ObserverManager, NodeStarted
|
|
90
|
+
|
|
91
|
+
# Create observer manager with concurrency control
|
|
92
|
+
observer_manager = ObserverManager(
|
|
93
|
+
max_concurrent_observers=10,
|
|
94
|
+
observer_timeout=5.0,
|
|
95
|
+
max_sync_workers=4
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Register a metrics observer for specific events
|
|
99
|
+
async def metrics_observer(event):
|
|
100
|
+
# Record metrics to Prometheus/DataDog/etc
|
|
101
|
+
if isinstance(event, NodeStarted):
|
|
102
|
+
metrics.increment(f"node.{event.name}.started")
|
|
103
|
+
|
|
104
|
+
observer_manager.register(
|
|
105
|
+
metrics_observer,
|
|
106
|
+
event_types=[NodeStarted, NodeCompleted, NodeFailed] # Filter
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Fire and forget - non-blocking
|
|
110
|
+
await observer_manager.notify(NodeStarted(name="processor", wave_index=1))
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Control Pattern - Retry Policy
|
|
114
|
+
```python
|
|
115
|
+
from hexdag.builtin.events import (
|
|
116
|
+
ControlManager, ControlResponse, ControlSignal, NodeFailed
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
control_manager = ControlManager()
|
|
120
|
+
|
|
121
|
+
# High-priority retry policy
|
|
122
|
+
async def retry_policy(event, context):
|
|
123
|
+
if isinstance(event, NodeFailed):
|
|
124
|
+
if context.attempt < 3:
|
|
125
|
+
return ControlResponse(
|
|
126
|
+
signal=ControlSignal.RETRY,
|
|
127
|
+
data={"delay": 1.0 * context.attempt} # Exponential backoff
|
|
128
|
+
)
|
|
129
|
+
else:
|
|
130
|
+
return ControlResponse(signal=ControlSignal.FAIL)
|
|
131
|
+
return ControlResponse() # PROCEED by default
|
|
132
|
+
|
|
133
|
+
control_manager.register(
|
|
134
|
+
retry_policy,
|
|
135
|
+
priority=10, # High priority (lower = higher)
|
|
136
|
+
name="retry_policy",
|
|
137
|
+
event_types=[NodeFailed] # Only handle failures
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Check event against all policies
|
|
141
|
+
response = await control_manager.check(event, context)
|
|
142
|
+
if response.signal == ControlSignal.RETRY:
|
|
143
|
+
# Orchestrator handles retry with delay
|
|
144
|
+
pass
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Event Type Filtering
|
|
148
|
+
```python
|
|
149
|
+
# Observer only for tool events - 90% fewer invocations
|
|
150
|
+
observer_manager.register(
|
|
151
|
+
tool_observer,
|
|
152
|
+
event_types=[ToolCalled, ToolCompleted]
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Control handler only for nodes
|
|
156
|
+
control_manager.register(
|
|
157
|
+
node_handler,
|
|
158
|
+
priority=50,
|
|
159
|
+
event_types=[NodeStarted, NodeCompleted, NodeFailed]
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Universal observer sees everything (no filter)
|
|
163
|
+
observer_manager.register(audit_observer)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## ⚡ Performance Optimizations
|
|
167
|
+
|
|
168
|
+
### 1. **Efficient Priority Queue**
|
|
169
|
+
```python
|
|
170
|
+
# Before: O(n log n) sorting
|
|
171
|
+
for handler in sorted(handlers, key=lambda h: h.priority):
|
|
172
|
+
...
|
|
173
|
+
|
|
174
|
+
# After: O(n) direct heap iteration
|
|
175
|
+
for entry in self._handler_heap: # Already ordered
|
|
176
|
+
...
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 2. **Resource Reuse**
|
|
180
|
+
```python
|
|
181
|
+
# Before: Create semaphore per event
|
|
182
|
+
async def notify(self, event):
|
|
183
|
+
semaphore = asyncio.Semaphore(self._max_concurrent)
|
|
184
|
+
|
|
185
|
+
# After: Create once, reuse
|
|
186
|
+
def __init__(self):
|
|
187
|
+
self._semaphore = asyncio.Semaphore(max_concurrent)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 3. **Consolidated Storage**
|
|
191
|
+
```python
|
|
192
|
+
# Before: Triple storage
|
|
193
|
+
self._handlers = {}
|
|
194
|
+
self._priorities = {}
|
|
195
|
+
self._event_filters = {}
|
|
196
|
+
|
|
197
|
+
# After: Single HandlerEntry dataclass
|
|
198
|
+
@dataclass
|
|
199
|
+
class HandlerEntry:
|
|
200
|
+
priority: int
|
|
201
|
+
handler: ControlHandler
|
|
202
|
+
event_types: set[Type] | None
|
|
203
|
+
metadata: HandlerMetadata
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 4. **Benchmark Results**
|
|
207
|
+
```
|
|
208
|
+
ControlManager: 100 handlers, 1000 checks → 0.04ms per check
|
|
209
|
+
ObserverManager: 50 observers with filtering → 12ms per notification
|
|
210
|
+
Event filtering: ~90% reduction in unnecessary processing
|
|
211
|
+
Sync functions: Non-blocking with ThreadPoolExecutor
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## 🧪 Testing
|
|
215
|
+
|
|
216
|
+
Comprehensive test coverage with 40+ tests:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# Run all event system tests
|
|
220
|
+
uv run pytest tests/hexdag/core/application/events/ -v
|
|
221
|
+
|
|
222
|
+
# Test files:
|
|
223
|
+
test_observer_manager.py # 13 tests - observation and filtering
|
|
224
|
+
test_control_manager.py # 13 tests - control flow and policies
|
|
225
|
+
test_events.py # 14 tests - event data classes
|
|
226
|
+
test_performance.py # 5 tests - performance benchmarks
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Coverage:
|
|
230
|
+
- `control_manager.py`: 79% coverage
|
|
231
|
+
- `observer_manager.py`: 89% coverage
|
|
232
|
+
- `events.py`: 100% coverage
|
|
233
|
+
|
|
234
|
+
## 🔄 Migration Guide
|
|
235
|
+
|
|
236
|
+
### From Old EventBus (10 files → 5 files)
|
|
237
|
+
```python
|
|
238
|
+
# Old: Mixed concerns in EventBus
|
|
239
|
+
event_bus = EventBus()
|
|
240
|
+
event_bus.register_handler(handler)
|
|
241
|
+
event_bus.emit(event)
|
|
242
|
+
|
|
243
|
+
# New: Separated by purpose
|
|
244
|
+
# For monitoring/telemetry
|
|
245
|
+
observer_manager = ObserverManager()
|
|
246
|
+
observer_manager.register(observer)
|
|
247
|
+
await observer_manager.notify(event)
|
|
248
|
+
|
|
249
|
+
# For control/policies
|
|
250
|
+
control_manager = ControlManager()
|
|
251
|
+
control_manager.register(handler, priority=10)
|
|
252
|
+
response = await control_manager.check(event, context)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Adding Event Filtering (New Feature)
|
|
256
|
+
```python
|
|
257
|
+
# Register for specific events only
|
|
258
|
+
manager.register(
|
|
259
|
+
handler,
|
|
260
|
+
event_types=[NodeStarted, NodeCompleted] # O(1) filtering
|
|
261
|
+
)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## 🎯 Design Principles
|
|
265
|
+
|
|
266
|
+
### Hexagonal Architecture
|
|
267
|
+
- **Ports**: Manager interfaces define contracts
|
|
268
|
+
- **Adapters**: Handlers/observers are pluggable implementations
|
|
269
|
+
- **Domain Isolation**: Events are pure data, no behavior
|
|
270
|
+
|
|
271
|
+
### Event-Driven Patterns
|
|
272
|
+
- **Observer Pattern**: Decoupled monitoring
|
|
273
|
+
- **Chain of Responsibility**: Priority-based control
|
|
274
|
+
- **Veto Pattern**: First non-PROCEED wins
|
|
275
|
+
- **Fire-and-Forget**: Async observation
|
|
276
|
+
|
|
277
|
+
### Production Readiness
|
|
278
|
+
- **Fault Isolation**: Failures don't cascade
|
|
279
|
+
- **Graceful Degradation**: Critical vs non-critical handlers
|
|
280
|
+
- **Timeout Protection**: Bounded execution time
|
|
281
|
+
- **Resource Management**: Proper cleanup, no leaks
|
|
282
|
+
|
|
283
|
+
## 🚦 Control Signals
|
|
284
|
+
|
|
285
|
+
| Signal | Purpose | Example Use Case |
|
|
286
|
+
|--------|---------|-----------------|
|
|
287
|
+
| PROCEED | Continue normally | Default - no intervention |
|
|
288
|
+
| SKIP | Skip this operation | Feature flags, A/B testing |
|
|
289
|
+
| RETRY | Retry with optional delay | Transient failures |
|
|
290
|
+
| FALLBACK | Use alternative value | Circuit breaker open |
|
|
291
|
+
| FAIL | Stop execution | Critical validation failure |
|
|
292
|
+
| ERROR | Critical handler error | Infrastructure issue |
|
|
293
|
+
|
|
294
|
+
## 🔮 Future Enhancements
|
|
295
|
+
|
|
296
|
+
- [ ] **Event Replay** - Debugging with event history
|
|
297
|
+
- [ ] **Event Persistence** - Durable event storage
|
|
298
|
+
- [ ] **Distributed Streaming** - Kafka/Pulsar integration
|
|
299
|
+
- [ ] **WebSocket Broadcasting** - Real-time UI updates
|
|
300
|
+
- [ ] **Event Dashboard** - Visualization and analytics
|
|
301
|
+
- [ ] **Hierarchical Events** - Topic-based routing
|
|
302
|
+
- [ ] **Event Schemas** - OpenAPI/AsyncAPI specs
|
|
303
|
+
|
|
304
|
+
## 📚 Related Documentation
|
|
305
|
+
|
|
306
|
+
- [Orchestrator Integration](../orchestrator.py) - How events integrate with DAG execution
|
|
307
|
+
- [Test Examples](../../../../../tests/hexdag/core/application/events/) - Comprehensive test suite
|
|
308
|
+
- [Performance Tests](../../../../../tests/hexdag/core/application/events/test_performance.py) - Benchmark validations
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
*Built for production, optimized for performance, designed for flexibility.*
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Event system for the Hex-DAG framework.
|
|
2
|
+
|
|
3
|
+
Clean, simplified event system with clear separation of concerns:
|
|
4
|
+
- events.py: Event data classes (just data, no behavior)
|
|
5
|
+
- observers/: Observer implementations for monitoring and observability
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Event classes
|
|
9
|
+
from .events import (
|
|
10
|
+
CheckpointRestored,
|
|
11
|
+
CheckpointSaved,
|
|
12
|
+
Event,
|
|
13
|
+
HealthCheckCompleted,
|
|
14
|
+
LLMPromptSent,
|
|
15
|
+
LLMResponseReceived,
|
|
16
|
+
NodeCancelled,
|
|
17
|
+
NodeCompleted,
|
|
18
|
+
NodeFailed,
|
|
19
|
+
NodeSkipped,
|
|
20
|
+
NodeStarted,
|
|
21
|
+
PipelineCancelled,
|
|
22
|
+
PipelineCompleted,
|
|
23
|
+
PipelineStarted,
|
|
24
|
+
PolicyEvaluated,
|
|
25
|
+
PolicyFallback,
|
|
26
|
+
PolicyRetry,
|
|
27
|
+
PolicySkipped,
|
|
28
|
+
PolicyTriggered,
|
|
29
|
+
ToolCalled,
|
|
30
|
+
ToolCompleted,
|
|
31
|
+
WaveCompleted,
|
|
32
|
+
WaveStarted,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Observer implementations
|
|
36
|
+
from .observers import (
|
|
37
|
+
AlertingObserver,
|
|
38
|
+
DataQualityObserver,
|
|
39
|
+
ExecutionTracerObserver,
|
|
40
|
+
PerformanceMetricsObserver,
|
|
41
|
+
ResourceMonitorObserver,
|
|
42
|
+
SimpleLoggingObserver,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Event taxonomy - grouped event types for observer filtering
|
|
46
|
+
NODE_LIFECYCLE_EVENTS = (NodeStarted, NodeCompleted, NodeFailed, NodeCancelled, NodeSkipped)
|
|
47
|
+
WAVE_EVENTS = (WaveStarted, WaveCompleted)
|
|
48
|
+
PIPELINE_EVENTS = (PipelineStarted, PipelineCompleted, PipelineCancelled)
|
|
49
|
+
LLM_EVENTS = (LLMPromptSent, LLMResponseReceived)
|
|
50
|
+
TOOL_EVENTS = (ToolCalled, ToolCompleted)
|
|
51
|
+
POLICY_EVENTS = (PolicyEvaluated, PolicyTriggered, PolicyRetry, PolicySkipped, PolicyFallback)
|
|
52
|
+
CHECKPOINT_EVENTS = (CheckpointSaved, CheckpointRestored)
|
|
53
|
+
HEALTH_EVENTS = (HealthCheckCompleted,)
|
|
54
|
+
|
|
55
|
+
# Commonly used combinations
|
|
56
|
+
ALL_NODE_EVENTS = NODE_LIFECYCLE_EVENTS + WAVE_EVENTS
|
|
57
|
+
ALL_EXECUTION_EVENTS = NODE_LIFECYCLE_EVENTS + WAVE_EVENTS + PIPELINE_EVENTS
|
|
58
|
+
ALL_MONITORING_EVENTS = NODE_LIFECYCLE_EVENTS + LLM_EVENTS + TOOL_EVENTS + POLICY_EVENTS
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
# Events
|
|
62
|
+
"Event",
|
|
63
|
+
"NodeStarted",
|
|
64
|
+
"NodeCompleted",
|
|
65
|
+
"NodeFailed",
|
|
66
|
+
"NodeCancelled",
|
|
67
|
+
"NodeSkipped",
|
|
68
|
+
"WaveStarted",
|
|
69
|
+
"WaveCompleted",
|
|
70
|
+
"PipelineStarted",
|
|
71
|
+
"PipelineCompleted",
|
|
72
|
+
"PipelineCancelled",
|
|
73
|
+
"LLMPromptSent",
|
|
74
|
+
"LLMResponseReceived",
|
|
75
|
+
"ToolCalled",
|
|
76
|
+
"ToolCompleted",
|
|
77
|
+
"PolicyEvaluated",
|
|
78
|
+
"PolicyTriggered",
|
|
79
|
+
"PolicySkipped",
|
|
80
|
+
"PolicyFallback",
|
|
81
|
+
"PolicyRetry",
|
|
82
|
+
"CheckpointSaved",
|
|
83
|
+
"CheckpointRestored",
|
|
84
|
+
"HealthCheckCompleted",
|
|
85
|
+
# Core Observers
|
|
86
|
+
"PerformanceMetricsObserver",
|
|
87
|
+
"AlertingObserver",
|
|
88
|
+
"ExecutionTracerObserver",
|
|
89
|
+
"SimpleLoggingObserver",
|
|
90
|
+
"ResourceMonitorObserver",
|
|
91
|
+
"DataQualityObserver",
|
|
92
|
+
# Event Taxonomy
|
|
93
|
+
"NODE_LIFECYCLE_EVENTS",
|
|
94
|
+
"WAVE_EVENTS",
|
|
95
|
+
"PIPELINE_EVENTS",
|
|
96
|
+
"LLM_EVENTS",
|
|
97
|
+
"TOOL_EVENTS",
|
|
98
|
+
"POLICY_EVENTS",
|
|
99
|
+
"CHECKPOINT_EVENTS",
|
|
100
|
+
"HEALTH_EVENTS",
|
|
101
|
+
"ALL_NODE_EVENTS",
|
|
102
|
+
"ALL_EXECUTION_EVENTS",
|
|
103
|
+
"ALL_MONITORING_EVENTS",
|
|
104
|
+
]
|