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,481 @@
|
|
|
1
|
+
"""BodyExecutor - Shared body execution logic for ControlFlowNode.
|
|
2
|
+
|
|
3
|
+
This module provides unified body execution for control flow nodes,
|
|
4
|
+
supporting four execution modes:
|
|
5
|
+
1. Module path string - Resolve and call function by path
|
|
6
|
+
2. !py inline Python - Compile and execute inline function
|
|
7
|
+
3. Inline nodes - Build and execute sub-DAG
|
|
8
|
+
4. Pipeline reference - Load and execute external pipeline
|
|
9
|
+
|
|
10
|
+
Examples
|
|
11
|
+
--------
|
|
12
|
+
Basic usage::
|
|
13
|
+
|
|
14
|
+
executor = BodyExecutor()
|
|
15
|
+
|
|
16
|
+
# Execute function by module path
|
|
17
|
+
result = await executor.execute(
|
|
18
|
+
body="myapp.process_item",
|
|
19
|
+
input_data={"item": data},
|
|
20
|
+
context=ctx,
|
|
21
|
+
ports={"database": db_adapter},
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Execute inline nodes
|
|
25
|
+
result = await executor.execute(
|
|
26
|
+
body=[
|
|
27
|
+
{"kind": "expression_node", "metadata": {"name": "step1"}, "spec": {...}},
|
|
28
|
+
{"kind": "llm_node", "metadata": {"name": "step2"}, "spec": {...}},
|
|
29
|
+
],
|
|
30
|
+
input_data={"item": data},
|
|
31
|
+
context=ctx,
|
|
32
|
+
ports={"llm": llm_adapter},
|
|
33
|
+
)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import asyncio
|
|
37
|
+
from collections.abc import Callable
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
from hexdag.core.logging import get_logger
|
|
42
|
+
from hexdag.core.orchestration.models import NodeExecutionContext
|
|
43
|
+
from hexdag.core.resolver import resolve_function
|
|
44
|
+
|
|
45
|
+
logger = get_logger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BodyExecutorError(Exception):
|
|
49
|
+
"""Error during body execution."""
|
|
50
|
+
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BodyExecutor:
|
|
55
|
+
"""Execute node body in function, inline-nodes, !py, or pipeline mode.
|
|
56
|
+
|
|
57
|
+
This class provides a unified interface for executing different body types
|
|
58
|
+
within control flow nodes (while, for-each, times, if-else, switch).
|
|
59
|
+
|
|
60
|
+
The BodyExecutor can be configured with orchestrator settings to ensure
|
|
61
|
+
sub-DAGs and pipelines execute with consistent configuration.
|
|
62
|
+
|
|
63
|
+
Attributes
|
|
64
|
+
----------
|
|
65
|
+
base_path : Path
|
|
66
|
+
Base directory for resolving pipeline references
|
|
67
|
+
max_concurrent_nodes : int
|
|
68
|
+
Maximum concurrent nodes for sub-orchestrators
|
|
69
|
+
strict_validation : bool
|
|
70
|
+
Whether to use strict validation in sub-orchestrators
|
|
71
|
+
default_node_timeout : float | None
|
|
72
|
+
Default timeout for nodes in sub-orchestrators
|
|
73
|
+
|
|
74
|
+
Examples
|
|
75
|
+
--------
|
|
76
|
+
Execute a function by module path::
|
|
77
|
+
|
|
78
|
+
executor = BodyExecutor()
|
|
79
|
+
result = await executor.execute(
|
|
80
|
+
body="myapp.process",
|
|
81
|
+
input_data={"x": 1},
|
|
82
|
+
context=ctx,
|
|
83
|
+
ports={},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
Execute inline Python function (from !py tag)::
|
|
87
|
+
|
|
88
|
+
async def my_func(item, index, state, **ports):
|
|
89
|
+
return item * 2
|
|
90
|
+
|
|
91
|
+
result = await executor.execute(
|
|
92
|
+
body=my_func, # Compiled from !py tag
|
|
93
|
+
input_data={"item": 5, "index": 0, "state": {}},
|
|
94
|
+
context=ctx,
|
|
95
|
+
ports={},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
With orchestrator configuration::
|
|
99
|
+
|
|
100
|
+
executor = BodyExecutor(
|
|
101
|
+
base_path=Path("./pipelines"),
|
|
102
|
+
max_concurrent_nodes=5,
|
|
103
|
+
strict_validation=True,
|
|
104
|
+
)
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
base_path: Path | None = None,
|
|
110
|
+
max_concurrent_nodes: int = 10,
|
|
111
|
+
strict_validation: bool = False,
|
|
112
|
+
default_node_timeout: float | None = None,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Initialize BodyExecutor.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
base_path : Path | None
|
|
119
|
+
Base directory for resolving pipeline references.
|
|
120
|
+
Defaults to current working directory.
|
|
121
|
+
max_concurrent_nodes : int
|
|
122
|
+
Maximum concurrent nodes for sub-orchestrators (default: 10)
|
|
123
|
+
strict_validation : bool
|
|
124
|
+
Whether to use strict validation in sub-orchestrators (default: False)
|
|
125
|
+
default_node_timeout : float | None
|
|
126
|
+
Default timeout for nodes in sub-orchestrators (default: None)
|
|
127
|
+
"""
|
|
128
|
+
self.base_path = base_path or Path.cwd()
|
|
129
|
+
self.max_concurrent_nodes = max_concurrent_nodes
|
|
130
|
+
self.strict_validation = strict_validation
|
|
131
|
+
self.default_node_timeout = default_node_timeout
|
|
132
|
+
|
|
133
|
+
async def execute(
|
|
134
|
+
self,
|
|
135
|
+
body: str | list[dict[str, Any]] | Callable[..., Any] | None,
|
|
136
|
+
body_pipeline: str | None,
|
|
137
|
+
input_data: dict[str, Any],
|
|
138
|
+
context: NodeExecutionContext,
|
|
139
|
+
ports: dict[str, Any],
|
|
140
|
+
iteration_context: dict[str, Any] | None = None,
|
|
141
|
+
) -> Any:
|
|
142
|
+
"""Execute body and return result.
|
|
143
|
+
|
|
144
|
+
Dispatches to the appropriate execution method based on body type:
|
|
145
|
+
- str: Module path string → _execute_function
|
|
146
|
+
- Callable: !py compiled function → _execute_py_function
|
|
147
|
+
- list[dict]: Inline nodes → _execute_inline_nodes
|
|
148
|
+
- body_pipeline: Pipeline reference → _execute_pipeline
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
body : str | list[dict] | Callable | None
|
|
153
|
+
The body specification. Can be:
|
|
154
|
+
- Module path string (e.g., "myapp.process")
|
|
155
|
+
- Callable (compiled from !py tag)
|
|
156
|
+
- List of node configs (inline nodes/sub-DAG)
|
|
157
|
+
body_pipeline : str | None
|
|
158
|
+
Path to external pipeline YAML file
|
|
159
|
+
input_data : dict[str, Any]
|
|
160
|
+
Input data for the body execution
|
|
161
|
+
context : NodeExecutionContext
|
|
162
|
+
Execution context with metadata
|
|
163
|
+
ports : dict[str, Any]
|
|
164
|
+
Injected ports (llm, database, memory, etc.)
|
|
165
|
+
iteration_context : dict[str, Any] | None
|
|
166
|
+
Additional context for iterations ($item, $index, $total, state)
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
Any
|
|
171
|
+
Result from body execution
|
|
172
|
+
|
|
173
|
+
Raises
|
|
174
|
+
------
|
|
175
|
+
BodyExecutorError
|
|
176
|
+
If body execution fails or no body is specified
|
|
177
|
+
"""
|
|
178
|
+
# Merge iteration context with input data
|
|
179
|
+
exec_context = dict(input_data)
|
|
180
|
+
if iteration_context:
|
|
181
|
+
exec_context.update(iteration_context)
|
|
182
|
+
|
|
183
|
+
if body_pipeline:
|
|
184
|
+
return await self._execute_pipeline(body_pipeline, exec_context, context, ports)
|
|
185
|
+
if isinstance(body, list):
|
|
186
|
+
return await self._execute_inline_nodes(body, exec_context, context, ports)
|
|
187
|
+
if isinstance(body, str):
|
|
188
|
+
return await self._execute_function(body, exec_context, context, ports)
|
|
189
|
+
if callable(body):
|
|
190
|
+
# Callable passed directly (either !py compiled function or regular callable)
|
|
191
|
+
# We call with (input_data, **ports) to match function_node convention
|
|
192
|
+
return await self._execute_callable(body, exec_context, context, ports)
|
|
193
|
+
raise BodyExecutorError(
|
|
194
|
+
"No body specified. Provide 'body' (function path, callable, or inline nodes) "
|
|
195
|
+
"or 'body_pipeline' (pipeline reference)."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
async def _execute_function(
|
|
199
|
+
self,
|
|
200
|
+
body: str,
|
|
201
|
+
input_data: dict[str, Any],
|
|
202
|
+
context: NodeExecutionContext,
|
|
203
|
+
ports: dict[str, Any],
|
|
204
|
+
) -> Any:
|
|
205
|
+
"""Execute body as module path function.
|
|
206
|
+
|
|
207
|
+
Parameters
|
|
208
|
+
----------
|
|
209
|
+
body : str
|
|
210
|
+
Module path to function (e.g., "myapp.process")
|
|
211
|
+
input_data : dict[str, Any]
|
|
212
|
+
Input data to pass to function
|
|
213
|
+
context : NodeExecutionContext
|
|
214
|
+
Execution context
|
|
215
|
+
ports : dict[str, Any]
|
|
216
|
+
Injected ports
|
|
217
|
+
|
|
218
|
+
Returns
|
|
219
|
+
-------
|
|
220
|
+
Any
|
|
221
|
+
Function result
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
func = resolve_function(body)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
raise BodyExecutorError(f"Failed to resolve function '{body}': {e}") from e
|
|
227
|
+
|
|
228
|
+
logger.debug(
|
|
229
|
+
"Executing function body",
|
|
230
|
+
function=body,
|
|
231
|
+
node_id=context.node_id,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
# Call function with input_data and ports
|
|
236
|
+
# Support both sync and async functions
|
|
237
|
+
result = func(input_data, **ports)
|
|
238
|
+
if asyncio.iscoroutine(result):
|
|
239
|
+
result = await result
|
|
240
|
+
return result
|
|
241
|
+
except Exception as e:
|
|
242
|
+
raise BodyExecutorError(f"Function '{body}' execution failed: {e}") from e
|
|
243
|
+
|
|
244
|
+
async def _execute_py_function(
|
|
245
|
+
self,
|
|
246
|
+
body: Callable[..., Any],
|
|
247
|
+
input_data: dict[str, Any],
|
|
248
|
+
context: NodeExecutionContext,
|
|
249
|
+
ports: dict[str, Any],
|
|
250
|
+
) -> Any:
|
|
251
|
+
"""Execute !py compiled inline Python function.
|
|
252
|
+
|
|
253
|
+
The function is expected to have signature:
|
|
254
|
+
async def process(item, index, state, **ports) -> Any
|
|
255
|
+
|
|
256
|
+
But we support flexible signatures - pass what's available.
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
body : Callable
|
|
261
|
+
Compiled Python function from !py tag
|
|
262
|
+
input_data : dict[str, Any]
|
|
263
|
+
Input data containing item, index, state, etc.
|
|
264
|
+
context : NodeExecutionContext
|
|
265
|
+
Execution context
|
|
266
|
+
ports : dict[str, Any]
|
|
267
|
+
Injected ports
|
|
268
|
+
|
|
269
|
+
Returns
|
|
270
|
+
-------
|
|
271
|
+
Any
|
|
272
|
+
Function result
|
|
273
|
+
"""
|
|
274
|
+
logger.debug(
|
|
275
|
+
"Executing !py inline function",
|
|
276
|
+
function_name=getattr(body, "__name__", "<anonymous>"),
|
|
277
|
+
node_id=context.node_id,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
# Extract standard parameters from input_data
|
|
282
|
+
item = input_data.get("$item", input_data.get("item"))
|
|
283
|
+
index = input_data.get("$index", input_data.get("index", 0))
|
|
284
|
+
state = input_data.get("state", {})
|
|
285
|
+
|
|
286
|
+
# Call with positional args (item, index, state) and keyword args (ports)
|
|
287
|
+
result = body(item, index, state, **ports)
|
|
288
|
+
if asyncio.iscoroutine(result):
|
|
289
|
+
result = await result
|
|
290
|
+
return result
|
|
291
|
+
except Exception as e:
|
|
292
|
+
func_name = getattr(body, "__name__", "<anonymous>")
|
|
293
|
+
raise BodyExecutorError(f"!py function '{func_name}' execution failed: {e}") from e
|
|
294
|
+
|
|
295
|
+
async def _execute_callable(
|
|
296
|
+
self,
|
|
297
|
+
body: Callable[..., Any],
|
|
298
|
+
input_data: dict[str, Any],
|
|
299
|
+
context: NodeExecutionContext,
|
|
300
|
+
ports: dict[str, Any],
|
|
301
|
+
) -> Any:
|
|
302
|
+
"""Execute a callable body with function_node-style signature.
|
|
303
|
+
|
|
304
|
+
This method handles regular callable bodies passed directly to
|
|
305
|
+
ControlFlowNode. It uses the (input_data, **ports) convention
|
|
306
|
+
matching function_node behavior.
|
|
307
|
+
|
|
308
|
+
For !py compiled functions that need (item, index, state, **ports),
|
|
309
|
+
use _execute_py_function instead.
|
|
310
|
+
|
|
311
|
+
Parameters
|
|
312
|
+
----------
|
|
313
|
+
body : Callable
|
|
314
|
+
A callable function
|
|
315
|
+
input_data : dict[str, Any]
|
|
316
|
+
Input data to pass as first argument
|
|
317
|
+
context : NodeExecutionContext
|
|
318
|
+
Execution context
|
|
319
|
+
ports : dict[str, Any]
|
|
320
|
+
Injected ports
|
|
321
|
+
|
|
322
|
+
Returns
|
|
323
|
+
-------
|
|
324
|
+
Any
|
|
325
|
+
Function result
|
|
326
|
+
"""
|
|
327
|
+
logger.debug(
|
|
328
|
+
"Executing callable body",
|
|
329
|
+
function_name=getattr(body, "__name__", "<anonymous>"),
|
|
330
|
+
node_id=context.node_id,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
# Call with (input_data, **ports) - matching function_node convention
|
|
335
|
+
result = body(input_data, **ports)
|
|
336
|
+
if asyncio.iscoroutine(result):
|
|
337
|
+
result = await result
|
|
338
|
+
return result
|
|
339
|
+
except Exception as e:
|
|
340
|
+
func_name = getattr(body, "__name__", "<anonymous>")
|
|
341
|
+
raise BodyExecutorError(f"Callable '{func_name}' execution failed: {e}") from e
|
|
342
|
+
|
|
343
|
+
async def _execute_inline_nodes(
|
|
344
|
+
self,
|
|
345
|
+
body: list[dict[str, Any]],
|
|
346
|
+
input_data: dict[str, Any],
|
|
347
|
+
context: NodeExecutionContext,
|
|
348
|
+
ports: dict[str, Any],
|
|
349
|
+
) -> Any:
|
|
350
|
+
"""Execute inline nodes as sub-DAG.
|
|
351
|
+
|
|
352
|
+
Builds a temporary DirectedGraph from the inline node configs,
|
|
353
|
+
creates a sub-orchestrator, and executes it.
|
|
354
|
+
|
|
355
|
+
Parameters
|
|
356
|
+
----------
|
|
357
|
+
body : list[dict[str, Any]]
|
|
358
|
+
List of node configuration dicts
|
|
359
|
+
input_data : dict[str, Any]
|
|
360
|
+
Input data for the sub-DAG
|
|
361
|
+
context : NodeExecutionContext
|
|
362
|
+
Execution context
|
|
363
|
+
ports : dict[str, Any]
|
|
364
|
+
Injected ports
|
|
365
|
+
|
|
366
|
+
Returns
|
|
367
|
+
-------
|
|
368
|
+
Any
|
|
369
|
+
Result from the last non-skipped node in the sub-DAG
|
|
370
|
+
"""
|
|
371
|
+
# Import here to avoid circular dependency
|
|
372
|
+
from hexdag.core.domain.dag import DirectedGraph
|
|
373
|
+
from hexdag.core.orchestration.orchestrator import Orchestrator
|
|
374
|
+
from hexdag.core.pipeline_builder import YamlPipelineBuilder
|
|
375
|
+
|
|
376
|
+
logger.debug(
|
|
377
|
+
"Executing inline nodes",
|
|
378
|
+
node_count=len(body),
|
|
379
|
+
node_id=context.node_id,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Build sub-graph from inline nodes
|
|
383
|
+
builder = YamlPipelineBuilder(base_path=self.base_path)
|
|
384
|
+
sub_graph = DirectedGraph()
|
|
385
|
+
|
|
386
|
+
for node_config in body:
|
|
387
|
+
# Find appropriate plugin to build node
|
|
388
|
+
for plugin in builder.entity_plugins:
|
|
389
|
+
if plugin.can_handle(node_config):
|
|
390
|
+
result = plugin.build(node_config, builder, sub_graph)
|
|
391
|
+
if result is not None:
|
|
392
|
+
sub_graph += result
|
|
393
|
+
break
|
|
394
|
+
|
|
395
|
+
if not sub_graph.nodes:
|
|
396
|
+
logger.warning("Inline body has no nodes", node_id=context.node_id)
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
# Create sub-orchestrator with inherited configuration
|
|
400
|
+
sub_orchestrator = Orchestrator(
|
|
401
|
+
max_concurrent_nodes=self.max_concurrent_nodes,
|
|
402
|
+
strict_validation=self.strict_validation,
|
|
403
|
+
default_node_timeout=self.default_node_timeout,
|
|
404
|
+
ports=ports,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
run_result = await sub_orchestrator.run(sub_graph, input_data, validate=False)
|
|
409
|
+
except Exception as e:
|
|
410
|
+
raise BodyExecutorError(f"Inline nodes execution failed: {e}") from e
|
|
411
|
+
|
|
412
|
+
# Return the result from the last non-skipped node
|
|
413
|
+
# Find nodes in topological order and get last result
|
|
414
|
+
# Note: orchestrator.run() returns dict[str, Any]
|
|
415
|
+
for node_name in reversed(list(sub_graph.nodes.keys())):
|
|
416
|
+
if node_name in run_result:
|
|
417
|
+
node_result = run_result[node_name]
|
|
418
|
+
if isinstance(node_result, dict) and node_result.get("_skipped"):
|
|
419
|
+
continue
|
|
420
|
+
return node_result
|
|
421
|
+
|
|
422
|
+
return run_result
|
|
423
|
+
|
|
424
|
+
async def _execute_pipeline(
|
|
425
|
+
self,
|
|
426
|
+
body_pipeline: str,
|
|
427
|
+
input_data: dict[str, Any],
|
|
428
|
+
context: NodeExecutionContext,
|
|
429
|
+
ports: dict[str, Any],
|
|
430
|
+
) -> Any:
|
|
431
|
+
"""Execute external pipeline from YAML file.
|
|
432
|
+
|
|
433
|
+
Parameters
|
|
434
|
+
----------
|
|
435
|
+
body_pipeline : str
|
|
436
|
+
Path to pipeline YAML file (relative to base_path)
|
|
437
|
+
input_data : dict[str, Any]
|
|
438
|
+
Input data for the pipeline
|
|
439
|
+
context : NodeExecutionContext
|
|
440
|
+
Execution context
|
|
441
|
+
ports : dict[str, Any]
|
|
442
|
+
Injected ports
|
|
443
|
+
|
|
444
|
+
Returns
|
|
445
|
+
-------
|
|
446
|
+
Any
|
|
447
|
+
Pipeline execution result
|
|
448
|
+
"""
|
|
449
|
+
# Import here to avoid circular dependency
|
|
450
|
+
from hexdag.core.orchestration.orchestrator import Orchestrator
|
|
451
|
+
from hexdag.core.pipeline_builder import YamlPipelineBuilder
|
|
452
|
+
|
|
453
|
+
# Resolve pipeline path
|
|
454
|
+
pipeline_path = self.base_path / body_pipeline
|
|
455
|
+
if not pipeline_path.exists():
|
|
456
|
+
raise BodyExecutorError(f"Pipeline file not found: {pipeline_path}")
|
|
457
|
+
|
|
458
|
+
logger.debug(
|
|
459
|
+
"Executing pipeline body",
|
|
460
|
+
pipeline=body_pipeline,
|
|
461
|
+
node_id=context.node_id,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
# Build pipeline
|
|
466
|
+
builder = YamlPipelineBuilder(base_path=pipeline_path.parent)
|
|
467
|
+
graph, config = builder.build_from_yaml_file(str(pipeline_path))
|
|
468
|
+
|
|
469
|
+
# Create orchestrator with inherited configuration
|
|
470
|
+
orchestrator = Orchestrator(
|
|
471
|
+
max_concurrent_nodes=self.max_concurrent_nodes,
|
|
472
|
+
strict_validation=self.strict_validation,
|
|
473
|
+
default_node_timeout=self.default_node_timeout,
|
|
474
|
+
ports=ports,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Execute pipeline
|
|
478
|
+
return await orchestrator.run(graph, input_data, validate=False)
|
|
479
|
+
|
|
480
|
+
except Exception as e:
|
|
481
|
+
raise BodyExecutorError(f"Pipeline '{body_pipeline}' execution failed: {e}") from e
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Components used by the orchestrator.
|
|
2
|
+
|
|
3
|
+
This package contains reusable components that implement specific
|
|
4
|
+
responsibilities in the orchestration pipeline:
|
|
5
|
+
|
|
6
|
+
Primary Components (Recommended):
|
|
7
|
+
- ExecutionCoordinator: Unified policy evaluation, input mapping, event notification
|
|
8
|
+
- LifecycleManager: Unified pre/post execution lifecycle management
|
|
9
|
+
|
|
10
|
+
Execution Components (Keep Unchanged):
|
|
11
|
+
- NodeExecutor: Executes individual nodes with validation and timeout
|
|
12
|
+
- WaveExecutor: Executes waves of parallel nodes with concurrency control
|
|
13
|
+
- CheckpointManager: Manages checkpoint save/restore and graph filtering
|
|
14
|
+
|
|
15
|
+
Deprecated Components (Use Unified Managers Instead):
|
|
16
|
+
- PolicyCoordinator: -> Use ExecutionCoordinator
|
|
17
|
+
- InputMapper: -> Use ExecutionCoordinator
|
|
18
|
+
- HealthCheckManager: -> Use LifecycleManager
|
|
19
|
+
- SecretManager: -> Use LifecycleManager
|
|
20
|
+
- AdapterLifecycleManager: -> Use LifecycleManager
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import importlib
|
|
26
|
+
import warnings
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from hexdag.core.exceptions import OrchestratorError
|
|
30
|
+
from hexdag.core.orchestration.components.checkpoint_manager import CheckpointManager
|
|
31
|
+
from hexdag.core.orchestration.components.execution_coordinator import ExecutionCoordinator
|
|
32
|
+
from hexdag.core.orchestration.components.lifecycle_manager import LifecycleManager
|
|
33
|
+
from hexdag.core.orchestration.components.node_executor import (
|
|
34
|
+
NodeExecutionError,
|
|
35
|
+
NodeExecutor,
|
|
36
|
+
NodeTimeoutError,
|
|
37
|
+
)
|
|
38
|
+
from hexdag.core.orchestration.components.wave_executor import WaveExecutor
|
|
39
|
+
|
|
40
|
+
# Deprecated classes - lazy loaded via __getattr__
|
|
41
|
+
_DEPRECATED_MAPPING: dict[str, tuple[str, str, str]] = {
|
|
42
|
+
"PolicyCoordinator": (
|
|
43
|
+
"hexdag.core.orchestration.components.policy_coordinator",
|
|
44
|
+
"PolicyCoordinator",
|
|
45
|
+
"ExecutionCoordinator",
|
|
46
|
+
),
|
|
47
|
+
"InputMapper": (
|
|
48
|
+
"hexdag.core.orchestration.components.input_mapper",
|
|
49
|
+
"InputMapper",
|
|
50
|
+
"ExecutionCoordinator",
|
|
51
|
+
),
|
|
52
|
+
"HealthCheckManager": (
|
|
53
|
+
"hexdag.core.orchestration.components.health_check_manager",
|
|
54
|
+
"HealthCheckManager",
|
|
55
|
+
"LifecycleManager",
|
|
56
|
+
),
|
|
57
|
+
"SecretManager": (
|
|
58
|
+
"hexdag.core.orchestration.components.secret_manager",
|
|
59
|
+
"SecretManager",
|
|
60
|
+
"LifecycleManager",
|
|
61
|
+
),
|
|
62
|
+
"AdapterLifecycleManager": (
|
|
63
|
+
"hexdag.core.orchestration.components.adapter_lifecycle_manager",
|
|
64
|
+
"AdapterLifecycleManager",
|
|
65
|
+
"LifecycleManager",
|
|
66
|
+
),
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def __getattr__(name: str) -> Any:
|
|
71
|
+
"""Provide deprecated imports with warnings."""
|
|
72
|
+
if name in _DEPRECATED_MAPPING:
|
|
73
|
+
module_path, class_name, replacement = _DEPRECATED_MAPPING[name]
|
|
74
|
+
warnings.warn(
|
|
75
|
+
f"{class_name} is deprecated and will be removed in a future version. "
|
|
76
|
+
f"Use {replacement} instead.",
|
|
77
|
+
DeprecationWarning,
|
|
78
|
+
stacklevel=2,
|
|
79
|
+
)
|
|
80
|
+
module = importlib.import_module(module_path)
|
|
81
|
+
return getattr(module, class_name)
|
|
82
|
+
|
|
83
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
__all__ = [
|
|
87
|
+
# Primary components (recommended)
|
|
88
|
+
"ExecutionCoordinator",
|
|
89
|
+
"LifecycleManager",
|
|
90
|
+
# Execution components (keep unchanged)
|
|
91
|
+
"CheckpointManager",
|
|
92
|
+
"NodeExecutionError",
|
|
93
|
+
"NodeExecutor",
|
|
94
|
+
"NodeTimeoutError",
|
|
95
|
+
"OrchestratorError",
|
|
96
|
+
"WaveExecutor",
|
|
97
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Adapter lifecycle manager for cleanup of adapter resources."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from hexdag.core.logging import get_logger
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from hexdag.core.ports.observer_manager import ObserverManagerPort
|
|
11
|
+
else:
|
|
12
|
+
ObserverManagerPort = Any
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
# Port names to skip during cleanup
|
|
17
|
+
MANAGER_PORT_NAMES = frozenset({"observer_manager"})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AdapterLifecycleManager:
|
|
21
|
+
"""Manages adapter lifecycle including connection cleanup.
|
|
22
|
+
|
|
23
|
+
Responsibilities:
|
|
24
|
+
- Close adapter connections (aclose, ashutdown, cleanup methods)
|
|
25
|
+
- Release resources after DAG execution
|
|
26
|
+
- Track which adapters were cleaned up
|
|
27
|
+
|
|
28
|
+
Examples
|
|
29
|
+
--------
|
|
30
|
+
Example usage::
|
|
31
|
+
|
|
32
|
+
manager = AdapterLifecycleManager()
|
|
33
|
+
result = await manager.cleanup_all_adapters(
|
|
34
|
+
ports={"llm": openai, "database": postgres},
|
|
35
|
+
observer_manager=observer
|
|
36
|
+
)
|
|
37
|
+
# {"cleaned_adapters": ["llm", "database"], "count": 2}
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Methods to try for cleanup, in order of preference
|
|
41
|
+
CLEANUP_METHODS = ["aclose", "ashutdown", "cleanup"]
|
|
42
|
+
|
|
43
|
+
async def cleanup_all_adapters(
|
|
44
|
+
self,
|
|
45
|
+
ports: dict[str, Any],
|
|
46
|
+
observer_manager: ObserverManagerPort | None,
|
|
47
|
+
) -> dict[str, Any]:
|
|
48
|
+
"""Close adapter connections and release resources.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
ports : dict[str, Any]
|
|
53
|
+
All available ports
|
|
54
|
+
observer_manager : ObserverManagerPort | None
|
|
55
|
+
Optional observer for event emission
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
dict[str, Any]
|
|
60
|
+
Cleanup results with cleaned_adapters list and count
|
|
61
|
+
|
|
62
|
+
Examples
|
|
63
|
+
--------
|
|
64
|
+
Example usage::
|
|
65
|
+
|
|
66
|
+
result = await manager.cleanup_all_adapters(
|
|
67
|
+
ports={"llm": openai, "database": postgres},
|
|
68
|
+
observer_manager=observer
|
|
69
|
+
)
|
|
70
|
+
# {"cleaned_adapters": ["llm", "database"], "count": 2}
|
|
71
|
+
"""
|
|
72
|
+
cleaned_adapters = []
|
|
73
|
+
|
|
74
|
+
for port_name, adapter in ports.items():
|
|
75
|
+
# Skip manager ports
|
|
76
|
+
if port_name in MANAGER_PORT_NAMES:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
# Try each cleanup method in order
|
|
80
|
+
if await self._cleanup_single_adapter(port_name, adapter):
|
|
81
|
+
cleaned_adapters.append(port_name)
|
|
82
|
+
|
|
83
|
+
return {"cleaned_adapters": cleaned_adapters, "count": len(cleaned_adapters)}
|
|
84
|
+
|
|
85
|
+
async def _cleanup_single_adapter(self, port_name: str, adapter: Any) -> bool:
|
|
86
|
+
"""Attempt to clean up a single adapter.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
port_name : str
|
|
91
|
+
Name of the port
|
|
92
|
+
adapter : Any
|
|
93
|
+
Adapter instance
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
bool
|
|
98
|
+
True if cleanup succeeded, False otherwise
|
|
99
|
+
"""
|
|
100
|
+
for method_name in self.CLEANUP_METHODS:
|
|
101
|
+
if hasattr(adapter, method_name) and callable(getattr(adapter, method_name)):
|
|
102
|
+
cleanup_method = getattr(adapter, method_name)
|
|
103
|
+
try:
|
|
104
|
+
logger.debug(f"Cleaning up adapter '{port_name}' via {method_name}()")
|
|
105
|
+
await cleanup_method()
|
|
106
|
+
logger.info(f"✅ Cleaned up adapter: {port_name}")
|
|
107
|
+
return True # Only call first matching cleanup method
|
|
108
|
+
except (RuntimeError, ValueError, TypeError, ConnectionError, OSError) as e:
|
|
109
|
+
# Expected cleanup errors - log but don't crash
|
|
110
|
+
logger.warning(f"Cleanup failed for {port_name}: {e}")
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
return False
|