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,134 @@
|
|
|
1
|
+
"""CheckpointManager component for orchestrator state persistence.
|
|
2
|
+
|
|
3
|
+
Storage-agnostic checkpoint manager using Memory Port for maximum flexibility.
|
|
4
|
+
Supports any backend: SQL databases, files (JSON/YAML), Redis, S3, etc.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from hexdag.core.domain.dag import DirectedGraph, NodeSpec
|
|
8
|
+
from hexdag.core.orchestration.models import CheckpointState
|
|
9
|
+
from hexdag.core.ports.memory import Memory
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CheckpointManager:
|
|
13
|
+
"""Manages orchestrator checkpoints using Memory Port abstraction.
|
|
14
|
+
|
|
15
|
+
This implementation is storage-agnostic and works with any Memory backend:
|
|
16
|
+
- SQL databases (via SQLiteMemoryAdapter)
|
|
17
|
+
- File storage (JSON, YAML, pickle via FileMemoryAdapter)
|
|
18
|
+
- In-memory storage (for testing)
|
|
19
|
+
- Redis, S3, etc.
|
|
20
|
+
|
|
21
|
+
Responsibilities:
|
|
22
|
+
- Save/restore execution state
|
|
23
|
+
- Filter graphs for resume
|
|
24
|
+
- Automatic serialization via Pydantic
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
storage : Memory
|
|
29
|
+
Memory port implementation for storage backend
|
|
30
|
+
key_prefix : str, default="checkpoint:"
|
|
31
|
+
Prefix for checkpoint keys (useful for namespacing)
|
|
32
|
+
auto_checkpoint : bool, default=True
|
|
33
|
+
Auto-save after nodes complete
|
|
34
|
+
|
|
35
|
+
Examples
|
|
36
|
+
--------
|
|
37
|
+
In-memory storage (testing)::
|
|
38
|
+
|
|
39
|
+
storage = InMemoryMemory()
|
|
40
|
+
mgr = CheckpointManager(storage=storage)
|
|
41
|
+
await mgr.save(state)
|
|
42
|
+
restored = await mgr.load("run-123")
|
|
43
|
+
|
|
44
|
+
File-based storage (production)::
|
|
45
|
+
|
|
46
|
+
storage = FileMemoryAdapter(base_path="./checkpoints", format="json")
|
|
47
|
+
mgr = CheckpointManager(storage=storage)
|
|
48
|
+
|
|
49
|
+
Database storage (enterprise)::
|
|
50
|
+
|
|
51
|
+
db = SQLiteAdapter(db_path="hexdag.db")
|
|
52
|
+
storage = SQLiteMemoryAdapter(database=db)
|
|
53
|
+
mgr = CheckpointManager(storage=storage)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
storage: Memory,
|
|
59
|
+
key_prefix: str = "checkpoint:",
|
|
60
|
+
auto_checkpoint: bool = True,
|
|
61
|
+
):
|
|
62
|
+
self.storage = storage
|
|
63
|
+
self.key_prefix = key_prefix
|
|
64
|
+
self.auto_checkpoint = auto_checkpoint
|
|
65
|
+
|
|
66
|
+
def _make_key(self, run_id: str) -> str:
|
|
67
|
+
"""Generate storage key for a run_id."""
|
|
68
|
+
return f"{self.key_prefix}{run_id}"
|
|
69
|
+
|
|
70
|
+
async def save(self, state: CheckpointState) -> None:
|
|
71
|
+
"""Save checkpoint state.
|
|
72
|
+
|
|
73
|
+
Uses Pydantic's model_dump_json() for automatic serialization.
|
|
74
|
+
All complex types (datetime, nested models) are handled automatically.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
state : CheckpointState
|
|
79
|
+
Complete checkpoint state to persist
|
|
80
|
+
"""
|
|
81
|
+
key = self._make_key(state.run_id)
|
|
82
|
+
# Pydantic handles all serialization including datetime, nested models, etc.
|
|
83
|
+
serialized = state.model_dump_json()
|
|
84
|
+
await self.storage.aset(key, serialized)
|
|
85
|
+
|
|
86
|
+
async def load(self, run_id: str) -> CheckpointState | None:
|
|
87
|
+
"""Load checkpoint state by run_id.
|
|
88
|
+
|
|
89
|
+
Uses Pydantic's model_validate_json() for automatic deserialization.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
run_id : str
|
|
94
|
+
Run identifier to load
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
CheckpointState | None
|
|
99
|
+
Restored checkpoint state, or None if not found
|
|
100
|
+
"""
|
|
101
|
+
key = self._make_key(run_id)
|
|
102
|
+
serialized = await self.storage.aget(key)
|
|
103
|
+
|
|
104
|
+
if serialized is None:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
# Pydantic handles all deserialization and validation
|
|
108
|
+
return CheckpointState.model_validate_json(serialized)
|
|
109
|
+
|
|
110
|
+
def filter_completed(self, graph: DirectedGraph, completed: set[str]) -> DirectedGraph:
|
|
111
|
+
"""Create graph with only pending nodes.
|
|
112
|
+
|
|
113
|
+
Parameters
|
|
114
|
+
----------
|
|
115
|
+
graph : DirectedGraph
|
|
116
|
+
Original DAG
|
|
117
|
+
completed : set[str]
|
|
118
|
+
Set of completed node names
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
DirectedGraph
|
|
123
|
+
New graph with only pending nodes and updated dependencies
|
|
124
|
+
"""
|
|
125
|
+
pending = DirectedGraph()
|
|
126
|
+
for spec in graph: # Using iterator instead of .nodes.items()
|
|
127
|
+
if spec.name not in completed:
|
|
128
|
+
pending += NodeSpec( # Using += operator instead of .add()
|
|
129
|
+
name=spec.name,
|
|
130
|
+
fn=spec.fn,
|
|
131
|
+
deps=frozenset(d for d in spec.deps if d not in completed),
|
|
132
|
+
timeout=spec.timeout,
|
|
133
|
+
)
|
|
134
|
+
return pending
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Execution coordinator for observer notifications and input mapping.
|
|
2
|
+
|
|
3
|
+
This module provides execution coordination functionality:
|
|
4
|
+
|
|
5
|
+
- Observer notifications during execution
|
|
6
|
+
- Input preparation and dependency mapping
|
|
7
|
+
- Input mapping transformation (including $input syntax)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from hexdag.core.ports.observer_manager import ObserverManagerPort
|
|
14
|
+
else:
|
|
15
|
+
ObserverManagerPort = Any
|
|
16
|
+
|
|
17
|
+
from hexdag.core.domain.dag import NodeSpec
|
|
18
|
+
from hexdag.core.logging import get_logger
|
|
19
|
+
|
|
20
|
+
__all__ = ["ExecutionCoordinator"]
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExecutionCoordinator:
|
|
26
|
+
"""Coordinates execution context: observer notifications and input mapping.
|
|
27
|
+
|
|
28
|
+
This component handles two responsibilities:
|
|
29
|
+
|
|
30
|
+
1. **Observer Notifications**: Notifying observers of events during DAG execution.
|
|
31
|
+
|
|
32
|
+
2. **Input Mapping**: Preparing input data for nodes based on their dependencies.
|
|
33
|
+
Uses a smart mapping strategy:
|
|
34
|
+
- No dependencies → initial input
|
|
35
|
+
- Single dependency → pass through that result
|
|
36
|
+
- Multiple dependencies → dict of results
|
|
37
|
+
|
|
38
|
+
Examples
|
|
39
|
+
--------
|
|
40
|
+
Basic usage::
|
|
41
|
+
|
|
42
|
+
coordinator = ExecutionCoordinator()
|
|
43
|
+
|
|
44
|
+
# Notify observer of an event
|
|
45
|
+
await coordinator.notify_observer(observer_manager, NodeStarted(...))
|
|
46
|
+
|
|
47
|
+
# Prepare input for a node
|
|
48
|
+
input_data = coordinator.prepare_node_input(
|
|
49
|
+
node_spec, node_results, initial_input
|
|
50
|
+
)
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
# ========================================================================
|
|
54
|
+
# Observer Notifications (from PolicyCoordinator)
|
|
55
|
+
# ========================================================================
|
|
56
|
+
|
|
57
|
+
async def notify_observer(
|
|
58
|
+
self, observer_manager: ObserverManagerPort | None, event: Any
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Notify observer manager of an event if it exists.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
observer_manager : ObserverManagerPort | None
|
|
65
|
+
Observer manager to notify (None if no observer configured)
|
|
66
|
+
event : Any
|
|
67
|
+
Event to send (typically NodeStarted, NodeCompleted, etc.)
|
|
68
|
+
|
|
69
|
+
Examples
|
|
70
|
+
--------
|
|
71
|
+
>>> from hexdag.core.orchestration.events import NodeStarted
|
|
72
|
+
>>> event = NodeStarted(name="my_node", wave_index=0)
|
|
73
|
+
>>> await coordinator.notify_observer(observer_manager, event) # doctest: +SKIP
|
|
74
|
+
"""
|
|
75
|
+
if observer_manager:
|
|
76
|
+
await observer_manager.notify(event)
|
|
77
|
+
|
|
78
|
+
# ========================================================================
|
|
79
|
+
# Input Mapping
|
|
80
|
+
# ========================================================================
|
|
81
|
+
|
|
82
|
+
def prepare_node_input(
|
|
83
|
+
self, node_spec: NodeSpec, node_results: dict[str, Any], initial_input: Any
|
|
84
|
+
) -> Any:
|
|
85
|
+
"""Prepare input data for node execution with simplified data mapping.
|
|
86
|
+
|
|
87
|
+
The mapping strategy is:
|
|
88
|
+
1. **No dependencies** → initial_input (entry point)
|
|
89
|
+
2. **Single dependency** → results[dependency_name] (pass-through)
|
|
90
|
+
3. **Multiple dependencies** → {dep1: result1, dep2: result2, ...} (namespace)
|
|
91
|
+
|
|
92
|
+
This approach balances simplicity (pass-through for single deps) with
|
|
93
|
+
clarity (named dict for multiple deps).
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
node_spec : NodeSpec
|
|
98
|
+
Node specification containing dependencies
|
|
99
|
+
node_results : dict[str, Any]
|
|
100
|
+
Results from previously executed nodes
|
|
101
|
+
initial_input : Any
|
|
102
|
+
Initial input data for the pipeline
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
Any
|
|
107
|
+
Prepared input data for the node:
|
|
108
|
+
- initial_input if no dependencies
|
|
109
|
+
- dependency result if single dependency
|
|
110
|
+
- dict of dependency results if multiple dependencies
|
|
111
|
+
|
|
112
|
+
Examples
|
|
113
|
+
--------
|
|
114
|
+
>>> coordinator = ExecutionCoordinator()
|
|
115
|
+
>>>
|
|
116
|
+
>>> # No dependencies - gets initial input
|
|
117
|
+
>>> # start_input = coordinator.prepare_node_input(
|
|
118
|
+
>>> # NodeSpec("start", lambda x: x.upper()),
|
|
119
|
+
>>> # node_results={},
|
|
120
|
+
>>> # initial_input="hello"
|
|
121
|
+
>>> # )
|
|
122
|
+
>>> # start_input == "hello"
|
|
123
|
+
>>>
|
|
124
|
+
>>> # Single dependency - gets that result directly
|
|
125
|
+
>>> # process_input = coordinator.prepare_node_input(
|
|
126
|
+
>>> # NodeSpec("process", lambda x: x + "!", deps={"start"}),
|
|
127
|
+
>>> # node_results={"start": "HELLO"},
|
|
128
|
+
>>> # initial_input="hello"
|
|
129
|
+
>>> # )
|
|
130
|
+
>>> # process_input == "HELLO"
|
|
131
|
+
|
|
132
|
+
Notes
|
|
133
|
+
-----
|
|
134
|
+
The multi-dependency dict preserves node names as keys, making it clear
|
|
135
|
+
where each piece of data came from. This is especially useful for
|
|
136
|
+
debugging and for nodes that need to treat different dependencies
|
|
137
|
+
differently.
|
|
138
|
+
|
|
139
|
+
If the node has an ``input_mapping`` in its params, the prepared input
|
|
140
|
+
will be transformed according to the mapping. This supports:
|
|
141
|
+
- ``$input.field`` - Reference the initial pipeline input
|
|
142
|
+
- ``node_name.field`` - Reference a specific dependency's output
|
|
143
|
+
"""
|
|
144
|
+
# Prepare base input from dependencies
|
|
145
|
+
if not node_spec.deps:
|
|
146
|
+
base_input = initial_input
|
|
147
|
+
elif len(node_spec.deps) == 1:
|
|
148
|
+
dep_name = next(iter(node_spec.deps))
|
|
149
|
+
base_input = node_results.get(dep_name, initial_input)
|
|
150
|
+
else:
|
|
151
|
+
# Multiple dependencies - preserve namespace structure
|
|
152
|
+
base_input = {}
|
|
153
|
+
for dep_name in node_spec.deps:
|
|
154
|
+
if dep_name in node_results:
|
|
155
|
+
base_input[dep_name] = node_results[dep_name]
|
|
156
|
+
|
|
157
|
+
# Apply input_mapping if present in node params
|
|
158
|
+
input_mapping = node_spec.params.get("input_mapping") if node_spec.params else None
|
|
159
|
+
if input_mapping:
|
|
160
|
+
return self._apply_input_mapping(base_input, input_mapping, initial_input, node_results)
|
|
161
|
+
|
|
162
|
+
return base_input
|
|
163
|
+
|
|
164
|
+
def _is_expression(self, source: str) -> bool:
|
|
165
|
+
"""Check if a source string is an expression (contains function calls or operators).
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
source : str
|
|
170
|
+
The source string to check
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
bool
|
|
175
|
+
True if the source appears to be an expression
|
|
176
|
+
"""
|
|
177
|
+
from hexdag.core.expression_parser import ALLOWED_FUNCTIONS
|
|
178
|
+
|
|
179
|
+
# Check for function call patterns (function_name followed by parenthesis)
|
|
180
|
+
for func_name in ALLOWED_FUNCTIONS:
|
|
181
|
+
if f"{func_name}(" in source:
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
# Check for arithmetic/comparison operators (but not dots which are field paths)
|
|
185
|
+
# Be careful not to match operators in simple field paths
|
|
186
|
+
expression_indicators = [
|
|
187
|
+
"==",
|
|
188
|
+
"!=",
|
|
189
|
+
"<=",
|
|
190
|
+
">=",
|
|
191
|
+
" < ",
|
|
192
|
+
" > ",
|
|
193
|
+
" + ",
|
|
194
|
+
" - ",
|
|
195
|
+
" * ",
|
|
196
|
+
" / ",
|
|
197
|
+
" % ",
|
|
198
|
+
" and ",
|
|
199
|
+
" or ",
|
|
200
|
+
" not ",
|
|
201
|
+
" in ",
|
|
202
|
+
]
|
|
203
|
+
return any(op in source for op in expression_indicators)
|
|
204
|
+
|
|
205
|
+
def _apply_input_mapping(
|
|
206
|
+
self,
|
|
207
|
+
base_input: Any,
|
|
208
|
+
input_mapping: dict[str, str],
|
|
209
|
+
initial_input: Any,
|
|
210
|
+
node_results: dict[str, Any],
|
|
211
|
+
) -> dict[str, Any]:
|
|
212
|
+
"""Apply field mapping to transform input data.
|
|
213
|
+
|
|
214
|
+
Supports multiple syntaxes:
|
|
215
|
+
- ``$input.field`` - Extract from the initial pipeline input
|
|
216
|
+
- ``node_name.field`` - Extract from a specific node's output
|
|
217
|
+
- Expression syntax - Use allowed functions and operators
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
base_input : Any
|
|
222
|
+
The prepared input from dependencies (may be single value or dict)
|
|
223
|
+
input_mapping : dict[str, str]
|
|
224
|
+
Mapping of {target_field: "source_path"} or {target_field: "expression"}
|
|
225
|
+
initial_input : Any
|
|
226
|
+
The original pipeline input (for $input references)
|
|
227
|
+
node_results : dict[str, Any]
|
|
228
|
+
Results from all previously executed nodes
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
dict[str, Any]
|
|
233
|
+
Transformed input with mapped fields
|
|
234
|
+
|
|
235
|
+
Examples
|
|
236
|
+
--------
|
|
237
|
+
>>> coordinator = ExecutionCoordinator()
|
|
238
|
+
>>> mapping = {"load_id": "$input.load_id", "result": "analyzer.output"}
|
|
239
|
+
>>> # This would extract load_id from initial input and result from analyzer node
|
|
240
|
+
|
|
241
|
+
Expression examples::
|
|
242
|
+
|
|
243
|
+
mapping = {
|
|
244
|
+
"is_valid": "len(items) > 0",
|
|
245
|
+
"name_upper": "upper(user.name)",
|
|
246
|
+
"total": "price * quantity",
|
|
247
|
+
}
|
|
248
|
+
"""
|
|
249
|
+
from hexdag.builtin.nodes.mapped_input import FieldExtractor
|
|
250
|
+
|
|
251
|
+
result: dict[str, Any] = {}
|
|
252
|
+
|
|
253
|
+
for target_field, source_path in input_mapping.items():
|
|
254
|
+
# Check if this is an expression that needs evaluation
|
|
255
|
+
if self._is_expression(source_path):
|
|
256
|
+
value = self._evaluate_expression(
|
|
257
|
+
source_path, base_input, initial_input, node_results
|
|
258
|
+
)
|
|
259
|
+
elif source_path.startswith("$input."):
|
|
260
|
+
# Extract from initial pipeline input
|
|
261
|
+
actual_path = source_path[7:] # Remove "$input." prefix
|
|
262
|
+
if actual_path:
|
|
263
|
+
# Has a field path like "$input.my_field"
|
|
264
|
+
if isinstance(initial_input, dict):
|
|
265
|
+
value = FieldExtractor.extract(initial_input, actual_path)
|
|
266
|
+
else:
|
|
267
|
+
# Non-dict input - wrap and extract
|
|
268
|
+
value = FieldExtractor.extract({"_root": initial_input}, "_root")
|
|
269
|
+
else:
|
|
270
|
+
# Just "$input." with no field - return entire initial input
|
|
271
|
+
value = initial_input
|
|
272
|
+
elif source_path == "$input":
|
|
273
|
+
# Reference the entire initial input
|
|
274
|
+
value = initial_input
|
|
275
|
+
elif "." in source_path:
|
|
276
|
+
# Check if it's a node_name.field pattern
|
|
277
|
+
parts = source_path.split(".", 1)
|
|
278
|
+
node_name, field_path = parts[0], parts[1]
|
|
279
|
+
if node_name in node_results:
|
|
280
|
+
# Extract from specific node's result
|
|
281
|
+
value = FieldExtractor.extract(node_results[node_name], field_path)
|
|
282
|
+
else:
|
|
283
|
+
# Fall back to extracting from base_input
|
|
284
|
+
value = FieldExtractor.extract(
|
|
285
|
+
base_input if isinstance(base_input, dict) else {}, source_path
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
# Simple field name - extract from base_input
|
|
289
|
+
value = FieldExtractor.extract(
|
|
290
|
+
base_input if isinstance(base_input, dict) else {}, source_path
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if value is None:
|
|
294
|
+
logger.warning(
|
|
295
|
+
f"input_mapping: '{source_path}' resolved to None for target '{target_field}'"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
result[target_field] = value
|
|
299
|
+
|
|
300
|
+
return result
|
|
301
|
+
|
|
302
|
+
def _evaluate_expression(
|
|
303
|
+
self,
|
|
304
|
+
expression: str,
|
|
305
|
+
base_input: Any,
|
|
306
|
+
initial_input: Any,
|
|
307
|
+
node_results: dict[str, Any],
|
|
308
|
+
) -> Any:
|
|
309
|
+
"""Evaluate an expression against available data.
|
|
310
|
+
|
|
311
|
+
Parameters
|
|
312
|
+
----------
|
|
313
|
+
expression : str
|
|
314
|
+
The expression to evaluate (e.g., "len(items) > 0")
|
|
315
|
+
base_input : Any
|
|
316
|
+
The prepared input from dependencies
|
|
317
|
+
initial_input : Any
|
|
318
|
+
The original pipeline input
|
|
319
|
+
node_results : dict[str, Any]
|
|
320
|
+
Results from all previously executed nodes
|
|
321
|
+
|
|
322
|
+
Returns
|
|
323
|
+
-------
|
|
324
|
+
Any
|
|
325
|
+
The result of evaluating the expression
|
|
326
|
+
"""
|
|
327
|
+
from hexdag.core.expression_parser import ExpressionError, evaluate_expression
|
|
328
|
+
|
|
329
|
+
# Build the data context for expression evaluation
|
|
330
|
+
# Merge all available data sources into a single dict
|
|
331
|
+
data_context: dict[str, Any] = {}
|
|
332
|
+
|
|
333
|
+
# Add node results
|
|
334
|
+
data_context.update(node_results)
|
|
335
|
+
|
|
336
|
+
# Add base_input (either as-is if dict, or wrapped)
|
|
337
|
+
if isinstance(base_input, dict):
|
|
338
|
+
data_context.update(base_input)
|
|
339
|
+
elif base_input is not None:
|
|
340
|
+
data_context["_input"] = base_input
|
|
341
|
+
|
|
342
|
+
# Add initial input with $input prefix removed (accessible as 'input')
|
|
343
|
+
if isinstance(initial_input, dict):
|
|
344
|
+
data_context["input"] = initial_input
|
|
345
|
+
# Also add initial_input fields at top level for convenience
|
|
346
|
+
for key, val in initial_input.items():
|
|
347
|
+
if key not in data_context:
|
|
348
|
+
data_context[key] = val
|
|
349
|
+
elif initial_input is not None:
|
|
350
|
+
data_context["input"] = initial_input
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
# Use evaluate_expression to get the actual value, not a boolean
|
|
354
|
+
return evaluate_expression(expression, data_context, {})
|
|
355
|
+
except ExpressionError as e:
|
|
356
|
+
logger.error(f"Expression evaluation failed for '{expression}': {e}")
|
|
357
|
+
return None
|
|
358
|
+
except Exception as e:
|
|
359
|
+
logger.error(f"Unexpected error evaluating expression '{expression}': {e}")
|
|
360
|
+
return None
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Health check manager for pre-DAG adapter validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from hexdag.core.ports.observer_manager import ObserverManagerPort
|
|
9
|
+
else:
|
|
10
|
+
ObserverManagerPort = Any
|
|
11
|
+
|
|
12
|
+
from hexdag.core.logging import get_logger
|
|
13
|
+
from hexdag.core.orchestration.events import HealthCheckCompleted
|
|
14
|
+
from hexdag.core.ports.healthcheck import HealthStatus
|
|
15
|
+
from hexdag.core.protocols import HealthCheckable
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
# Constants
|
|
20
|
+
MANAGER_PORT_NAMES = frozenset({"observer_manager"})
|
|
21
|
+
LATENCY_PRECISION = 1 # Decimal places for latency display
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HealthCheckManager:
|
|
25
|
+
"""Manages health checks on adapters before DAG execution.
|
|
26
|
+
|
|
27
|
+
Responsibilities:
|
|
28
|
+
- Check adapter health via ahealth_check() method
|
|
29
|
+
- Emit HealthCheckCompleted events
|
|
30
|
+
- Determine if unhealthy adapters should block execution
|
|
31
|
+
|
|
32
|
+
Examples
|
|
33
|
+
--------
|
|
34
|
+
Example usage::
|
|
35
|
+
|
|
36
|
+
manager = HealthCheckManager(fail_fast=True, warn_only=False)
|
|
37
|
+
health_results = await manager.check_all_adapters(
|
|
38
|
+
ports={"llm": openai, "database": postgres},
|
|
39
|
+
observer_manager=observer,
|
|
40
|
+
pipeline_name="my_pipeline"
|
|
41
|
+
)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, fail_fast: bool = False, warn_only: bool = True):
|
|
45
|
+
"""Initialize health check manager.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
fail_fast : bool, default=False
|
|
50
|
+
If True, unhealthy adapters block pipeline execution
|
|
51
|
+
warn_only : bool, default=True
|
|
52
|
+
If True, log warnings for unhealthy adapters but don't block
|
|
53
|
+
"""
|
|
54
|
+
self.fail_fast = fail_fast
|
|
55
|
+
self.warn_only = warn_only
|
|
56
|
+
|
|
57
|
+
async def check_all_adapters(
|
|
58
|
+
self,
|
|
59
|
+
ports: dict[str, Any],
|
|
60
|
+
observer_manager: ObserverManagerPort | None,
|
|
61
|
+
pipeline_name: str,
|
|
62
|
+
) -> list[HealthStatus]:
|
|
63
|
+
"""Run health checks on all adapters that implement ahealth_check().
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
ports : dict[str, Any]
|
|
68
|
+
All available ports
|
|
69
|
+
observer_manager : ObserverManagerPort | None
|
|
70
|
+
Optional observer for event emission
|
|
71
|
+
pipeline_name : str
|
|
72
|
+
Name of the pipeline
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
list[HealthStatus]
|
|
77
|
+
Health status results from all adapters
|
|
78
|
+
"""
|
|
79
|
+
health_results = []
|
|
80
|
+
|
|
81
|
+
for port_name, adapter in ports.items():
|
|
82
|
+
# Skip non-adapter ports
|
|
83
|
+
if port_name in MANAGER_PORT_NAMES:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if isinstance(adapter, HealthCheckable):
|
|
87
|
+
status = await self._check_single_adapter(port_name, adapter, observer_manager)
|
|
88
|
+
health_results.append(status)
|
|
89
|
+
|
|
90
|
+
return health_results
|
|
91
|
+
|
|
92
|
+
async def _check_single_adapter(
|
|
93
|
+
self,
|
|
94
|
+
port_name: str,
|
|
95
|
+
adapter: Any,
|
|
96
|
+
observer_manager: ObserverManagerPort | None,
|
|
97
|
+
) -> HealthStatus:
|
|
98
|
+
"""Check health of a single adapter.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
port_name : str
|
|
103
|
+
Name of the port
|
|
104
|
+
adapter : Any
|
|
105
|
+
Adapter instance
|
|
106
|
+
observer_manager : ObserverManagerPort | None
|
|
107
|
+
Optional observer for event emission
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
HealthStatus
|
|
112
|
+
Health status of the adapter
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
logger.debug(f"Running health check for {port_name}")
|
|
116
|
+
health_check = adapter.ahealth_check
|
|
117
|
+
status: HealthStatus = await health_check() # pyright: ignore[reportGeneralTypeIssues]
|
|
118
|
+
status.port_name = port_name # Ensure port name is set
|
|
119
|
+
|
|
120
|
+
# Emit event
|
|
121
|
+
if observer_manager:
|
|
122
|
+
event = HealthCheckCompleted(
|
|
123
|
+
adapter_name=status.adapter_name,
|
|
124
|
+
port_name=port_name,
|
|
125
|
+
status=status,
|
|
126
|
+
)
|
|
127
|
+
await observer_manager.notify(event)
|
|
128
|
+
|
|
129
|
+
# Log result
|
|
130
|
+
self._log_health_result(port_name, status)
|
|
131
|
+
|
|
132
|
+
return status
|
|
133
|
+
|
|
134
|
+
except (RuntimeError, ConnectionError, TimeoutError, ValueError) as e:
|
|
135
|
+
# Health check errors - mark adapter as unhealthy
|
|
136
|
+
logger.error(f"Health check failed for {port_name}: {e}", exc_info=True)
|
|
137
|
+
adapter_name = getattr(adapter, "_hexdag_name", port_name)
|
|
138
|
+
return HealthStatus(
|
|
139
|
+
status="unhealthy",
|
|
140
|
+
adapter_name=adapter_name,
|
|
141
|
+
port_name=port_name,
|
|
142
|
+
error=e,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def _log_health_result(self, port_name: str, status: HealthStatus) -> None:
|
|
146
|
+
"""Log health check result.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
port_name : str
|
|
151
|
+
Name of the port
|
|
152
|
+
status : HealthStatus
|
|
153
|
+
Health status result
|
|
154
|
+
"""
|
|
155
|
+
if status.status == "healthy":
|
|
156
|
+
latency_info = (
|
|
157
|
+
f" ({status.latency_ms:.{LATENCY_PRECISION}f}ms)" if status.latency_ms else ""
|
|
158
|
+
)
|
|
159
|
+
logger.info(f"✅ {port_name} health check: {status.status}{latency_info}")
|
|
160
|
+
else:
|
|
161
|
+
logger.warning(f"⚠️ {port_name} health check: {status.status} - {status.error}")
|
|
162
|
+
|
|
163
|
+
def get_unhealthy_adapters(self, health_results: list[HealthStatus]) -> list[HealthStatus]:
|
|
164
|
+
"""Filter health results to only unhealthy adapters.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
health_results : list[HealthStatus]
|
|
169
|
+
All health check results
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
list[HealthStatus]
|
|
174
|
+
Only the unhealthy adapters
|
|
175
|
+
"""
|
|
176
|
+
return [h for h in health_results if h.status == "unhealthy"]
|