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,294 @@
|
|
|
1
|
+
"""Configurable interface and base class for plugins and adapters.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
1. ConfigurableComponent - Protocol for components with configuration
|
|
5
|
+
2. ConfigurableAdapter - Base class that implements the protocol and eliminates boilerplate
|
|
6
|
+
3. ConfigurableNode - Base class for node factories with configuration
|
|
7
|
+
4. ConfigurablePolicy - Base class for policies with configuration
|
|
8
|
+
5. ConfigurableMacro - Base class for macros with configuration
|
|
9
|
+
6. SecretField - Helper for declaring secret configuration fields
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any, Protocol, runtime_checkable
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, ConfigDict, Field # noqa: TC002
|
|
17
|
+
|
|
18
|
+
from hexdag.core.logging import get_logger
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _extract_config_fields(kwargs: dict[str, Any], config_class: type[BaseModel]) -> dict[str, Any]:
|
|
24
|
+
"""Extract config fields from kwargs that match the config class schema.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
kwargs : dict[str, Any]
|
|
29
|
+
Keyword arguments containing config values
|
|
30
|
+
config_class : type[BaseModel]
|
|
31
|
+
Pydantic model class defining the config schema
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
dict[str, Any]
|
|
36
|
+
Dictionary containing only the fields defined in config_class
|
|
37
|
+
"""
|
|
38
|
+
return {
|
|
39
|
+
field_name: kwargs[field_name]
|
|
40
|
+
for field_name in config_class.model_fields
|
|
41
|
+
if field_name in kwargs
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Legacy Config base classes removed - use simplified decorator pattern instead
|
|
46
|
+
# See CLAUDE.md and SIMPLIFIED_PATTERN.md for migration guide
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MacroConfig(BaseModel):
|
|
50
|
+
"""Base configuration class for all macros.
|
|
51
|
+
|
|
52
|
+
Macros should define a nested Config class inheriting from this.
|
|
53
|
+
This enables:
|
|
54
|
+
- Type-safe configuration
|
|
55
|
+
- YAML schema generation
|
|
56
|
+
- Runtime validation
|
|
57
|
+
- Expansion strategy configuration
|
|
58
|
+
|
|
59
|
+
Examples
|
|
60
|
+
--------
|
|
61
|
+
Define a macro configuration class::
|
|
62
|
+
|
|
63
|
+
class ResearchMacroConfig(MacroConfig):
|
|
64
|
+
depth: int = 3
|
|
65
|
+
enable_synthesis: bool = True
|
|
66
|
+
|
|
67
|
+
config = ResearchMacroConfig(depth=5)
|
|
68
|
+
assert config.depth == 5
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
model_config = ConfigDict(frozen=True)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def SecretField(
|
|
75
|
+
env_var: str,
|
|
76
|
+
memory_key: str | None = None,
|
|
77
|
+
default: Any = None,
|
|
78
|
+
description: str | None = None,
|
|
79
|
+
**field_kwargs: Any,
|
|
80
|
+
) -> Any:
|
|
81
|
+
"""Create a secret field with auto-resolution from Memory or environment.
|
|
82
|
+
|
|
83
|
+
This helper marks a Pydantic field as a secret, which enables:
|
|
84
|
+
1. Auto-hiding in logs/repr (uses Pydantic SecretStr)
|
|
85
|
+
2. Auto-resolution from Memory (secret:KEY) via orchestrator
|
|
86
|
+
3. Fallback to environment variable if not in Memory
|
|
87
|
+
4. Clear documentation of secret requirements
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
env_var : str
|
|
92
|
+
Environment variable name to read from (e.g., "OPENAI_API_KEY")
|
|
93
|
+
memory_key : str | None, optional
|
|
94
|
+
Key to use in Memory port (defaults to env_var).
|
|
95
|
+
Will be prefixed with "secret:" automatically.
|
|
96
|
+
default : Any, optional
|
|
97
|
+
Default value if not found (default: None)
|
|
98
|
+
description : str | None, optional
|
|
99
|
+
Field description for schema documentation
|
|
100
|
+
**field_kwargs : Any
|
|
101
|
+
Additional Pydantic Field() parameters
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
Any
|
|
106
|
+
Pydantic Field with secret metadata (typed as Any for use in assignments)
|
|
107
|
+
|
|
108
|
+
Examples
|
|
109
|
+
--------
|
|
110
|
+
>>> from pydantic import BaseModel
|
|
111
|
+
>>> class MyAdapterConfig(BaseModel):
|
|
112
|
+
... api_key: SecretStr | None = SecretField(
|
|
113
|
+
... env_var="OPENAI_API_KEY",
|
|
114
|
+
... description="OpenAI API key"
|
|
115
|
+
... )
|
|
116
|
+
... timeout: float = 30.0
|
|
117
|
+
"""
|
|
118
|
+
return Field(
|
|
119
|
+
default=default,
|
|
120
|
+
description=description,
|
|
121
|
+
json_schema_extra={
|
|
122
|
+
"secret": True,
|
|
123
|
+
"env_var": env_var,
|
|
124
|
+
"memory_key": memory_key or env_var,
|
|
125
|
+
},
|
|
126
|
+
**field_kwargs,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@runtime_checkable
|
|
131
|
+
class ConfigurableComponent(Protocol):
|
|
132
|
+
"""Protocol for components that support configuration.
|
|
133
|
+
|
|
134
|
+
Any adapter or plugin that implements this protocol will be
|
|
135
|
+
automatically discoverable by the CLI config generation system.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def get_config_class(cls) -> type[BaseModel]:
|
|
140
|
+
"""Return the Pydantic model class that defines configuration schema.
|
|
141
|
+
|
|
142
|
+
Returns
|
|
143
|
+
-------
|
|
144
|
+
type[BaseModel]
|
|
145
|
+
A Pydantic model class with field definitions, defaults, and validation
|
|
146
|
+
"""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ConfigurableMacro:
|
|
151
|
+
"""Base class for macros with configuration support.
|
|
152
|
+
|
|
153
|
+
Macros are pipeline templates that expand into DirectedGraph subgraphs.
|
|
154
|
+
They are first-class registry components like nodes, adapters, and policies.
|
|
155
|
+
|
|
156
|
+
Key Concepts
|
|
157
|
+
------------
|
|
158
|
+
- Macros live at a higher abstraction level than nodes
|
|
159
|
+
- Nodes are atomic operations (single LLM call, single function)
|
|
160
|
+
- Macros are compositions (multi-step workflows)
|
|
161
|
+
- Macros expand to graphs of nodes at build time or runtime
|
|
162
|
+
|
|
163
|
+
Architecture
|
|
164
|
+
------------
|
|
165
|
+
ConfigurableMacro provides:
|
|
166
|
+
1. Type-safe configuration via MacroConfig subclasses
|
|
167
|
+
2. Consistent expansion interface via expand() method
|
|
168
|
+
3. Registry integration via @macro decorator
|
|
169
|
+
4. Support for both static and dynamic expansion strategies
|
|
170
|
+
|
|
171
|
+
Subclasses must:
|
|
172
|
+
- Define a nested Config class inheriting from MacroConfig
|
|
173
|
+
- Implement expand() method that returns DirectedGraph
|
|
174
|
+
|
|
175
|
+
Examples
|
|
176
|
+
--------
|
|
177
|
+
>>> from hexdag.core.configurable import ConfigurableMacro, MacroConfig
|
|
178
|
+
>>> from hexdag.core.domain.dag import DirectedGraph
|
|
179
|
+
>>>
|
|
180
|
+
>>> class ResearchMacroConfig(MacroConfig):
|
|
181
|
+
... depth: int = 3
|
|
182
|
+
... enable_synthesis: bool = True
|
|
183
|
+
>>>
|
|
184
|
+
>>> class ResearchMacro(ConfigurableMacro):
|
|
185
|
+
... Config = ResearchMacroConfig
|
|
186
|
+
...
|
|
187
|
+
... def expand(self, instance_name, inputs, dependencies):
|
|
188
|
+
... # Build and return DirectedGraph
|
|
189
|
+
... return DirectedGraph([...])
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
Config: type[MacroConfig]
|
|
193
|
+
|
|
194
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
195
|
+
"""Initialize macro with configuration.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
**kwargs : Any
|
|
200
|
+
Configuration options matching Config schema fields
|
|
201
|
+
"""
|
|
202
|
+
if not hasattr(self.__class__, "Config"):
|
|
203
|
+
raise AttributeError(
|
|
204
|
+
f"{self.__class__.__name__} must define a nested Config class (MacroConfig)"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
config_data = _extract_config_fields(kwargs, self.Config)
|
|
208
|
+
|
|
209
|
+
self.config = self.Config(**config_data)
|
|
210
|
+
self._extra_kwargs = {k: v for k, v in kwargs.items() if k not in self.Config.model_fields}
|
|
211
|
+
|
|
212
|
+
def expand(
|
|
213
|
+
self,
|
|
214
|
+
instance_name: str,
|
|
215
|
+
inputs: dict[str, Any],
|
|
216
|
+
dependencies: list[str],
|
|
217
|
+
) -> Any: # Returns DirectedGraph but avoid circular import
|
|
218
|
+
"""Expand macro into a concrete subgraph.
|
|
219
|
+
|
|
220
|
+
This is the core method that subclasses must implement.
|
|
221
|
+
It transforms the macro template into an actual DirectedGraph
|
|
222
|
+
with concrete nodes.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
instance_name : str
|
|
227
|
+
Unique name for this macro instance (used as prefix for generated nodes).
|
|
228
|
+
Example: "deep_research" → generates "deep_research_step_1", etc.
|
|
229
|
+
inputs : dict[str, Any]
|
|
230
|
+
Input values to bind to macro parameters.
|
|
231
|
+
Example: {"topic": "AI safety", "depth": 5}
|
|
232
|
+
dependencies : list[str]
|
|
233
|
+
External node names that this macro instance depends on.
|
|
234
|
+
The macro's entry nodes will be connected to these.
|
|
235
|
+
Example: ["query_parser", "validator"]
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
DirectedGraph
|
|
240
|
+
Subgraph containing the expanded nodes with proper dependencies
|
|
241
|
+
|
|
242
|
+
Raises
|
|
243
|
+
------
|
|
244
|
+
NotImplementedError
|
|
245
|
+
If subclass doesn't implement this method
|
|
246
|
+
ValueError
|
|
247
|
+
If inputs don't match macro parameter requirements
|
|
248
|
+
"""
|
|
249
|
+
raise NotImplementedError(f"{self.__class__.__name__} must implement expand() method")
|
|
250
|
+
|
|
251
|
+
def validate_inputs(
|
|
252
|
+
self, inputs: dict[str, Any], required: list[str], optional: dict[str, Any]
|
|
253
|
+
) -> dict[str, Any]:
|
|
254
|
+
"""Validate and normalize macro inputs.
|
|
255
|
+
|
|
256
|
+
Helper method for subclasses to validate inputs against requirements.
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
inputs : dict[str, Any]
|
|
261
|
+
Provided input values
|
|
262
|
+
required : list[str]
|
|
263
|
+
Names of required input parameters
|
|
264
|
+
optional : dict[str, Any]
|
|
265
|
+
Optional parameters with their default values
|
|
266
|
+
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
dict[str, Any]
|
|
270
|
+
Validated and normalized inputs (with defaults applied)
|
|
271
|
+
|
|
272
|
+
Raises
|
|
273
|
+
------
|
|
274
|
+
ValueError
|
|
275
|
+
If required inputs are missing
|
|
276
|
+
"""
|
|
277
|
+
# Check required inputs
|
|
278
|
+
if missing := [name for name in required if name not in inputs]:
|
|
279
|
+
raise ValueError(
|
|
280
|
+
f"Missing required inputs for {self.__class__.__name__}: {', '.join(missing)}"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Apply defaults for optional inputs
|
|
284
|
+
return {**optional, **inputs}
|
|
285
|
+
|
|
286
|
+
@classmethod
|
|
287
|
+
def get_config_class(cls) -> type[BaseModel]:
|
|
288
|
+
"""Get the configuration model class."""
|
|
289
|
+
return cls.Config
|
|
290
|
+
|
|
291
|
+
def __repr__(self) -> str:
|
|
292
|
+
"""Readable representation for debugging."""
|
|
293
|
+
config_dict = self.config.model_dump() if hasattr(self.config, "model_dump") else {}
|
|
294
|
+
return f"{self.__class__.__name__}(config={config_dict})"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Execution context for async-safe cross-cutting concerns."""
|
|
2
|
+
|
|
3
|
+
from hexdag.core.context.execution_context import (
|
|
4
|
+
ExecutionContext,
|
|
5
|
+
clear_execution_context,
|
|
6
|
+
get_current_graph,
|
|
7
|
+
get_current_node_name,
|
|
8
|
+
get_node_results,
|
|
9
|
+
get_observer_manager,
|
|
10
|
+
get_port,
|
|
11
|
+
get_ports,
|
|
12
|
+
get_run_id,
|
|
13
|
+
set_current_graph,
|
|
14
|
+
set_current_node_name,
|
|
15
|
+
set_node_results,
|
|
16
|
+
set_observer_manager,
|
|
17
|
+
set_ports,
|
|
18
|
+
set_run_id,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"ExecutionContext",
|
|
23
|
+
"clear_execution_context",
|
|
24
|
+
"get_current_graph",
|
|
25
|
+
"get_current_node_name",
|
|
26
|
+
"get_node_results",
|
|
27
|
+
"get_observer_manager",
|
|
28
|
+
"get_port",
|
|
29
|
+
"get_ports",
|
|
30
|
+
"get_run_id",
|
|
31
|
+
"set_current_graph",
|
|
32
|
+
"set_current_node_name",
|
|
33
|
+
"set_node_results",
|
|
34
|
+
"set_observer_manager",
|
|
35
|
+
"set_ports",
|
|
36
|
+
"set_run_id",
|
|
37
|
+
]
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Execution context for orchestrator components.
|
|
2
|
+
|
|
3
|
+
This module provides async-safe context management for cross-cutting concerns
|
|
4
|
+
like observer management. This eliminates parameter drilling and provides a
|
|
5
|
+
clean way to access these services throughout the execution call stack.
|
|
6
|
+
|
|
7
|
+
The context is automatically propagated through async call chains, making
|
|
8
|
+
observer_manager and other orchestration services available to all components
|
|
9
|
+
without explicit parameter passing.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from contextvars import ContextVar
|
|
15
|
+
from types import MappingProxyType
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from hexdag.core.ports.observer_manager import ObserverManagerPort
|
|
20
|
+
|
|
21
|
+
# Context variables for orchestrator components (async-safe)
|
|
22
|
+
_observer_manager_context: ContextVar[ObserverManagerPort | None] = ContextVar(
|
|
23
|
+
"observer_manager", default=None
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_run_id_context: ContextVar[str | None] = ContextVar("run_id", default=None)
|
|
27
|
+
|
|
28
|
+
# Ports stored as immutable MappingProxyType to prevent race conditions in concurrent execution
|
|
29
|
+
_ports_context: ContextVar[MappingProxyType[str, Any] | None] = ContextVar("ports", default=None)
|
|
30
|
+
|
|
31
|
+
# Dynamic graph context - for runtime expansion support
|
|
32
|
+
_current_graph_context: ContextVar[Any | None] = ContextVar(
|
|
33
|
+
"current_graph", default=None
|
|
34
|
+
) # Any to avoid circular import
|
|
35
|
+
|
|
36
|
+
# Node results context - for accessing intermediate results during execution
|
|
37
|
+
_node_results_context: ContextVar[dict[str, Any] | None] = ContextVar("node_results", default=None)
|
|
38
|
+
|
|
39
|
+
# Current node name context - for event emission with proper node attribution
|
|
40
|
+
_current_node_name_context: ContextVar[str | None] = ContextVar("current_node_name", default=None)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ============================================================================
|
|
44
|
+
# Observer Manager Context
|
|
45
|
+
# ============================================================================
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def set_observer_manager(manager: ObserverManagerPort | None) -> None:
|
|
49
|
+
"""Set observer manager for current async execution context.
|
|
50
|
+
|
|
51
|
+
This should be called by the orchestrator at the start of DAG execution.
|
|
52
|
+
All components within this context will automatically have access to
|
|
53
|
+
the observer manager for event emission.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
manager : ObserverManagerPort | None
|
|
58
|
+
Observer manager instance, or None to clear context
|
|
59
|
+
|
|
60
|
+
Examples
|
|
61
|
+
--------
|
|
62
|
+
Example usage::
|
|
63
|
+
|
|
64
|
+
# In orchestrator
|
|
65
|
+
observer_manager = ports.get("observer_manager")
|
|
66
|
+
set_observer_manager(observer_manager)
|
|
67
|
+
# Now all components can emit events
|
|
68
|
+
"""
|
|
69
|
+
_observer_manager_context.set(manager)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_observer_manager() -> ObserverManagerPort | None:
|
|
73
|
+
"""Get observer manager from current async execution context.
|
|
74
|
+
|
|
75
|
+
This is called by components to access the observer manager for event
|
|
76
|
+
emission. Returns None if not set or not in orchestrator context.
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
ObserverManagerPort | None
|
|
81
|
+
Current observer manager, or None if not in orchestrator context
|
|
82
|
+
|
|
83
|
+
Examples
|
|
84
|
+
--------
|
|
85
|
+
Example usage::
|
|
86
|
+
|
|
87
|
+
# In any component
|
|
88
|
+
if (observer_manager := get_observer_manager()):
|
|
89
|
+
await observer_manager.notify(NodeStarted(...))
|
|
90
|
+
"""
|
|
91
|
+
return _observer_manager_context.get()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ============================================================================
|
|
95
|
+
# Run ID Context
|
|
96
|
+
# ============================================================================
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def set_run_id(run_id: str | None) -> None:
|
|
100
|
+
"""Set run ID for current async execution context.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
run_id : str | None
|
|
105
|
+
Unique run identifier for this execution
|
|
106
|
+
"""
|
|
107
|
+
_run_id_context.set(run_id)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_run_id() -> str | None:
|
|
111
|
+
"""Get run ID from current async execution context.
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
str | None
|
|
116
|
+
Current run ID, or None if not in orchestrator context
|
|
117
|
+
"""
|
|
118
|
+
return _run_id_context.get()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ============================================================================
|
|
122
|
+
# Ports Context
|
|
123
|
+
# ============================================================================
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def set_ports(ports: dict[str, Any] | None) -> None:
|
|
127
|
+
"""Set ports dict for current async execution context.
|
|
128
|
+
|
|
129
|
+
This allows adapters and components to access ports without explicit passing.
|
|
130
|
+
|
|
131
|
+
IMPORTANT: Ports are stored as immutable MappingProxyType to prevent race
|
|
132
|
+
conditions when multiple nodes execute concurrently.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
ports : dict[str, Any] | None
|
|
137
|
+
Ports dictionary containing all available adapters and services
|
|
138
|
+
"""
|
|
139
|
+
# Wrap in MappingProxyType to make immutable and prevent concurrent modification
|
|
140
|
+
_ports_context.set(MappingProxyType(ports) if ports else None)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_ports() -> MappingProxyType[str, Any] | None:
|
|
144
|
+
"""Get immutable ports mapping from current async execution context.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
MappingProxyType[str, Any] | None
|
|
149
|
+
Immutable view of ports dictionary, or None if not in orchestrator context
|
|
150
|
+
"""
|
|
151
|
+
return _ports_context.get()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_port(port_name: str) -> Any:
|
|
155
|
+
"""Get a specific port from current async execution context.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
port_name : str
|
|
160
|
+
Name of the port to retrieve (e.g., "llm", "database", "memory")
|
|
161
|
+
|
|
162
|
+
Returns
|
|
163
|
+
-------
|
|
164
|
+
Any
|
|
165
|
+
The port adapter, or None if not found or not in orchestrator context
|
|
166
|
+
|
|
167
|
+
Examples
|
|
168
|
+
--------
|
|
169
|
+
Example usage::
|
|
170
|
+
|
|
171
|
+
# In any component
|
|
172
|
+
if (llm := get_port("llm")):
|
|
173
|
+
response = await llm.aresponse(messages)
|
|
174
|
+
"""
|
|
175
|
+
ports = _ports_context.get()
|
|
176
|
+
if ports is None:
|
|
177
|
+
return None
|
|
178
|
+
return ports.get(port_name)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ============================================================================
|
|
182
|
+
# Current Node Name Context
|
|
183
|
+
# ============================================================================
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def set_current_node_name(node_name: str | None) -> None:
|
|
187
|
+
"""Set current node name for event attribution.
|
|
188
|
+
|
|
189
|
+
Parameters
|
|
190
|
+
----------
|
|
191
|
+
node_name : str | None
|
|
192
|
+
Name of the currently executing node, or None to clear
|
|
193
|
+
"""
|
|
194
|
+
_current_node_name_context.set(node_name)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def get_current_node_name() -> str | None:
|
|
198
|
+
"""Get current node name from execution context.
|
|
199
|
+
|
|
200
|
+
Returns
|
|
201
|
+
-------
|
|
202
|
+
str | None
|
|
203
|
+
Name of currently executing node, or None if not set
|
|
204
|
+
"""
|
|
205
|
+
return _current_node_name_context.get()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ============================================================================
|
|
209
|
+
# Batch Context Management
|
|
210
|
+
# ============================================================================
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ExecutionContext:
|
|
214
|
+
"""Context manager for setting up orchestrator execution context.
|
|
215
|
+
|
|
216
|
+
This provides a clean way to set all execution context variables at once
|
|
217
|
+
using a context manager pattern.
|
|
218
|
+
|
|
219
|
+
Important
|
|
220
|
+
---------
|
|
221
|
+
The context is automatically cleaned up on exit via __aexit__. This means:
|
|
222
|
+
|
|
223
|
+
1. **All async operations (observers, hooks) MUST complete before context exit**
|
|
224
|
+
2. Observer notifications should always use `await` before exiting context
|
|
225
|
+
3. Post-DAG hooks must execute INSIDE the context, not after
|
|
226
|
+
4. Do not create fire-and-forget tasks that outlive the context
|
|
227
|
+
|
|
228
|
+
The orchestrator correctly handles this by awaiting all notifications and
|
|
229
|
+
executing post-hooks inside the context block before cleanup.
|
|
230
|
+
|
|
231
|
+
Examples
|
|
232
|
+
--------
|
|
233
|
+
Example usage::
|
|
234
|
+
|
|
235
|
+
async with ExecutionContext(
|
|
236
|
+
observer_manager=observer,
|
|
237
|
+
run_id="run-123",
|
|
238
|
+
ports=all_ports
|
|
239
|
+
):
|
|
240
|
+
# All components can access context
|
|
241
|
+
result = await execute_dag(dag, inputs)
|
|
242
|
+
# IMPORTANT: All observer notifications and hooks complete here
|
|
243
|
+
# Context cleaned up here - observers no longer accessible
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
def __init__(
|
|
247
|
+
self,
|
|
248
|
+
observer_manager: ObserverManagerPort | None = None,
|
|
249
|
+
run_id: str | None = None,
|
|
250
|
+
ports: dict[str, Any] | None = None,
|
|
251
|
+
):
|
|
252
|
+
"""Initialize execution context.
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
observer_manager : ObserverManagerPort | None
|
|
257
|
+
Observer manager for event emission
|
|
258
|
+
run_id : str | None
|
|
259
|
+
Unique run identifier
|
|
260
|
+
ports : dict[str, Any] | None
|
|
261
|
+
Ports dictionary with all adapters
|
|
262
|
+
"""
|
|
263
|
+
self.observer_manager = observer_manager
|
|
264
|
+
self.run_id = run_id
|
|
265
|
+
self.ports = ports
|
|
266
|
+
|
|
267
|
+
def __enter__(self) -> ExecutionContext:
|
|
268
|
+
"""Set up execution context (sync context manager)."""
|
|
269
|
+
set_observer_manager(self.observer_manager)
|
|
270
|
+
set_run_id(self.run_id)
|
|
271
|
+
set_ports(self.ports)
|
|
272
|
+
return self
|
|
273
|
+
|
|
274
|
+
def __exit__(self, _exc_type: Any, _exc_val: Any, _exc_tb: Any) -> None:
|
|
275
|
+
"""Clean up execution context (sync context manager)."""
|
|
276
|
+
clear_execution_context()
|
|
277
|
+
|
|
278
|
+
async def __aenter__(self) -> ExecutionContext:
|
|
279
|
+
"""Set up execution context (async context manager)."""
|
|
280
|
+
set_observer_manager(self.observer_manager)
|
|
281
|
+
set_run_id(self.run_id)
|
|
282
|
+
set_ports(self.ports)
|
|
283
|
+
return self
|
|
284
|
+
|
|
285
|
+
async def __aexit__(self, _exc_type: Any, _exc_val: Any, _exc_tb: Any) -> None:
|
|
286
|
+
"""Clean up execution context (async context manager)."""
|
|
287
|
+
clear_execution_context()
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ============================================================================
|
|
291
|
+
# Cleanup
|
|
292
|
+
# ============================================================================
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def clear_execution_context() -> None:
|
|
296
|
+
"""Clear all execution context variables.
|
|
297
|
+
|
|
298
|
+
Useful for cleanup after orchestrator execution or in tests.
|
|
299
|
+
"""
|
|
300
|
+
_observer_manager_context.set(None)
|
|
301
|
+
_run_id_context.set(None)
|
|
302
|
+
_ports_context.set(None)
|
|
303
|
+
_current_graph_context.set(None)
|
|
304
|
+
_node_results_context.set(None)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ============================================================================
|
|
308
|
+
# Dynamic Graph Context (for runtime expansion)
|
|
309
|
+
# ============================================================================
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def set_current_graph(graph: Any) -> None:
|
|
313
|
+
"""Set current graph for dynamic expansion.
|
|
314
|
+
|
|
315
|
+
This allows expander nodes to access and modify the graph during execution.
|
|
316
|
+
Only used when executing DynamicDirectedGraph.
|
|
317
|
+
|
|
318
|
+
Parameters
|
|
319
|
+
----------
|
|
320
|
+
graph : Any
|
|
321
|
+
The current graph being executed (typically DynamicDirectedGraph)
|
|
322
|
+
"""
|
|
323
|
+
_current_graph_context.set(graph)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def get_current_graph() -> Any | None:
|
|
327
|
+
"""Get current graph from execution context.
|
|
328
|
+
|
|
329
|
+
Used by expander nodes to inject new nodes during runtime.
|
|
330
|
+
|
|
331
|
+
Returns
|
|
332
|
+
-------
|
|
333
|
+
Any | None
|
|
334
|
+
Current graph, or None if not in dynamic execution context
|
|
335
|
+
|
|
336
|
+
Examples
|
|
337
|
+
--------
|
|
338
|
+
>>> # In an expander node
|
|
339
|
+
>>> graph = get_current_graph()
|
|
340
|
+
>>> if graph and hasattr(graph, 'add'):
|
|
341
|
+
... new_node = create_next_step()
|
|
342
|
+
... graph.add(new_node)
|
|
343
|
+
"""
|
|
344
|
+
return _current_graph_context.get()
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def set_node_results(results: dict[str, Any]) -> None:
|
|
348
|
+
"""Set accumulated node results for dynamic expansion.
|
|
349
|
+
|
|
350
|
+
This allows expander nodes to inspect previous results when deciding
|
|
351
|
+
whether to expand the graph.
|
|
352
|
+
|
|
353
|
+
Parameters
|
|
354
|
+
----------
|
|
355
|
+
results : dict[str, Any]
|
|
356
|
+
Dictionary mapping node names to their execution results
|
|
357
|
+
"""
|
|
358
|
+
_node_results_context.set(results)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def get_node_results() -> dict[str, Any] | None:
|
|
362
|
+
"""Get accumulated node results from execution context.
|
|
363
|
+
|
|
364
|
+
Returns
|
|
365
|
+
-------
|
|
366
|
+
dict[str, Any] | None
|
|
367
|
+
Node results dictionary, or None if not in execution context
|
|
368
|
+
|
|
369
|
+
Examples
|
|
370
|
+
--------
|
|
371
|
+
>>> # In an expander node
|
|
372
|
+
>>> results = get_node_results()
|
|
373
|
+
>>> if results:
|
|
374
|
+
... previous_step = results.get("step_1")
|
|
375
|
+
... if should_continue(previous_step):
|
|
376
|
+
... inject_next_step()
|
|
377
|
+
"""
|
|
378
|
+
return _node_results_context.get()
|