mcp-mesh 0.7.21__py3-none-any.whl → 0.8.0b1__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.
- _mcp_mesh/__init__.py +1 -1
- _mcp_mesh/engine/dependency_injector.py +4 -6
- _mcp_mesh/engine/http_wrapper.py +69 -10
- _mcp_mesh/engine/mesh_llm_agent.py +4 -7
- _mcp_mesh/engine/mesh_llm_agent_injector.py +2 -1
- _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
- _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
- _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
- _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
- _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
- _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
- _mcp_mesh/engine/response_parser.py +61 -15
- _mcp_mesh/engine/unified_mcp_proxy.py +18 -34
- _mcp_mesh/pipeline/__init__.py +9 -20
- _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
- _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
- _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +425 -0
- _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
- _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
- _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
- _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
- _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
- _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
- _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +695 -0
- _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
- _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
- _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +5 -6
- _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +21 -9
- _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
- _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
- _mcp_mesh/reload.py +1 -3
- _mcp_mesh/shared/__init__.py +2 -8
- _mcp_mesh/shared/config_resolver.py +124 -80
- _mcp_mesh/shared/defaults.py +89 -14
- _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
- _mcp_mesh/shared/host_resolver.py +8 -46
- _mcp_mesh/shared/server_discovery.py +115 -86
- _mcp_mesh/shared/simple_shutdown.py +44 -86
- _mcp_mesh/tracing/execution_tracer.py +2 -6
- _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
- _mcp_mesh/tracing/trace_context_helper.py +3 -13
- _mcp_mesh/tracing/utils.py +29 -15
- _mcp_mesh/utils/fastmcp_schema_extractor.py +2 -1
- {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/METADATA +2 -1
- mcp_mesh-0.8.0b1.dist-info/RECORD +85 -0
- mesh/__init__.py +2 -1
- mesh/decorators.py +89 -5
- _mcp_mesh/generated/.openapi-generator/FILES +0 -50
- _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
- _mcp_mesh/generated/.openapi-generator-ignore +0 -15
- _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
- _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
- _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
- _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
- _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
- _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
- _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
- _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
- _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
- _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
- _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
- _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
- _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
- _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
- _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
- _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
- _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
- _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
- _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
- _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
- _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
- _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
- _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -243
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
- _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
- _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
- _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
- _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
- _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
- _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
- _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
- _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
- _mcp_mesh/shared/registry_client_wrapper.py +0 -515
- mcp_mesh-0.7.21.dist-info/RECORD +0 -152
- {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.7.21.dist-info → mcp_mesh-0.8.0b1.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Heartbeat orchestrator for managing periodic pipeline execution.
|
|
3
|
-
|
|
4
|
-
Provides a high-level interface for executing heartbeat pipelines with proper
|
|
5
|
-
context management and error handling.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
import logging
|
|
10
|
-
from datetime import UTC, datetime
|
|
11
|
-
from typing import Any, Optional
|
|
12
|
-
|
|
13
|
-
from ...shared.support_types import HealthStatus, HealthStatusType
|
|
14
|
-
from .heartbeat_pipeline import HeartbeatPipeline
|
|
15
|
-
|
|
16
|
-
logger = logging.getLogger(__name__)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class HeartbeatOrchestrator:
|
|
20
|
-
"""
|
|
21
|
-
Orchestrates heartbeat pipeline execution for periodic registry communication.
|
|
22
|
-
|
|
23
|
-
Manages the context preparation, pipeline execution, and result processing
|
|
24
|
-
for the 30-second heartbeat cycle.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
def __init__(self):
|
|
28
|
-
self.logger = logging.getLogger(f"{__name__}.HeartbeatOrchestrator")
|
|
29
|
-
self.pipeline = HeartbeatPipeline()
|
|
30
|
-
self._heartbeat_count = 0
|
|
31
|
-
|
|
32
|
-
async def execute_heartbeat(self, agent_id: str, context: dict[str, Any]) -> bool:
|
|
33
|
-
"""
|
|
34
|
-
Execute a complete heartbeat cycle with comprehensive error handling.
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
agent_id: Agent identifier
|
|
38
|
-
context: Full pipeline context from startup
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
bool: True if heartbeat succeeded, False if failed
|
|
42
|
-
"""
|
|
43
|
-
self._heartbeat_count += 1
|
|
44
|
-
|
|
45
|
-
try:
|
|
46
|
-
|
|
47
|
-
# Prepare heartbeat context with validation
|
|
48
|
-
heartbeat_context = await self._prepare_heartbeat_context(agent_id, context)
|
|
49
|
-
|
|
50
|
-
# Validate required context before proceeding
|
|
51
|
-
if not self._validate_heartbeat_context(heartbeat_context):
|
|
52
|
-
self.logger.error(
|
|
53
|
-
f"❌ Heartbeat #{self._heartbeat_count} failed: invalid context"
|
|
54
|
-
)
|
|
55
|
-
return False
|
|
56
|
-
|
|
57
|
-
# Check if health status is unhealthy - skip heartbeat if so
|
|
58
|
-
health_status = heartbeat_context.get("health_status")
|
|
59
|
-
if health_status and health_status.status == HealthStatusType.UNHEALTHY:
|
|
60
|
-
self.logger.warning(
|
|
61
|
-
f"⚠️ Heartbeat #{self._heartbeat_count} skipped for agent '{agent_id}': Health status is UNHEALTHY"
|
|
62
|
-
)
|
|
63
|
-
self.logger.warning(f" Health checks failed: {health_status.checks}")
|
|
64
|
-
self.logger.warning(f" Errors: {health_status.errors}")
|
|
65
|
-
return False
|
|
66
|
-
|
|
67
|
-
# Log heartbeat request details for debugging
|
|
68
|
-
self._log_heartbeat_request(heartbeat_context, self._heartbeat_count)
|
|
69
|
-
|
|
70
|
-
# Execute heartbeat pipeline with timeout protection
|
|
71
|
-
self.logger.trace(
|
|
72
|
-
f"💓 Executing heartbeat #{self._heartbeat_count} for agent '{agent_id}'"
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
# Add timeout to prevent hanging heartbeats (30 seconds max)
|
|
76
|
-
import asyncio
|
|
77
|
-
|
|
78
|
-
try:
|
|
79
|
-
|
|
80
|
-
result = await asyncio.wait_for(
|
|
81
|
-
self.pipeline.execute_heartbeat_cycle(heartbeat_context),
|
|
82
|
-
timeout=30.0,
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
except TimeoutError:
|
|
86
|
-
self.logger.error(
|
|
87
|
-
f"❌ Heartbeat #{self._heartbeat_count} timed out after 30 seconds"
|
|
88
|
-
)
|
|
89
|
-
return False
|
|
90
|
-
|
|
91
|
-
# Process results
|
|
92
|
-
success = self._process_heartbeat_result(
|
|
93
|
-
result, agent_id, self._heartbeat_count
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
# Log periodic status updates
|
|
97
|
-
if self._heartbeat_count % 10 == 0:
|
|
98
|
-
elapsed_time = self._heartbeat_count * 30 # Assuming 30s interval
|
|
99
|
-
self.logger.info(
|
|
100
|
-
f"💓 Heartbeat #{self._heartbeat_count} for agent '{agent_id}' - "
|
|
101
|
-
f"running for {elapsed_time} seconds"
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
return success
|
|
105
|
-
|
|
106
|
-
except Exception as e:
|
|
107
|
-
# Log detailed error information for debugging
|
|
108
|
-
import traceback
|
|
109
|
-
|
|
110
|
-
self.logger.error(
|
|
111
|
-
f"❌ Heartbeat #{self._heartbeat_count} failed for agent '{agent_id}': {e}\n"
|
|
112
|
-
f"Traceback: {traceback.format_exc()}"
|
|
113
|
-
)
|
|
114
|
-
return False
|
|
115
|
-
|
|
116
|
-
async def _prepare_heartbeat_context(
|
|
117
|
-
self, agent_id: str, startup_context: dict[str, Any]
|
|
118
|
-
) -> dict[str, Any]:
|
|
119
|
-
"""Prepare context for heartbeat pipeline execution."""
|
|
120
|
-
|
|
121
|
-
# Build health status from startup context
|
|
122
|
-
health_status = await self._build_health_status_from_context(
|
|
123
|
-
startup_context, agent_id
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
# Prepare heartbeat-specific context - registry_wrapper will be created in pipeline
|
|
127
|
-
heartbeat_context = {
|
|
128
|
-
"agent_id": agent_id,
|
|
129
|
-
"health_status": health_status,
|
|
130
|
-
"agent_config": startup_context.get("agent_config", {}),
|
|
131
|
-
"registration_data": startup_context.get("registration_data", {}),
|
|
132
|
-
# Include other relevant context from startup
|
|
133
|
-
"fastmcp_servers": startup_context.get("fastmcp_servers", {}),
|
|
134
|
-
"capabilities": startup_context.get("capabilities", []),
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return heartbeat_context
|
|
138
|
-
|
|
139
|
-
def _validate_heartbeat_context(self, heartbeat_context: dict[str, Any]) -> bool:
|
|
140
|
-
"""Validate that heartbeat context has all required components."""
|
|
141
|
-
|
|
142
|
-
required_fields = ["agent_id", "health_status"]
|
|
143
|
-
|
|
144
|
-
for field in required_fields:
|
|
145
|
-
if field not in heartbeat_context or heartbeat_context[field] is None:
|
|
146
|
-
self.logger.error(
|
|
147
|
-
f"❌ Heartbeat context validation failed: missing '{field}'"
|
|
148
|
-
)
|
|
149
|
-
return False
|
|
150
|
-
|
|
151
|
-
# Additional validation for health_status
|
|
152
|
-
health_status = heartbeat_context.get("health_status")
|
|
153
|
-
if not hasattr(health_status, "agent_name") or not hasattr(
|
|
154
|
-
health_status, "status"
|
|
155
|
-
):
|
|
156
|
-
self.logger.error(
|
|
157
|
-
"❌ Heartbeat context validation failed: invalid health_status object"
|
|
158
|
-
)
|
|
159
|
-
return False
|
|
160
|
-
|
|
161
|
-
return True
|
|
162
|
-
|
|
163
|
-
async def _build_health_status_from_context(
|
|
164
|
-
self, startup_context: dict[str, Any], agent_id: str
|
|
165
|
-
) -> HealthStatus:
|
|
166
|
-
"""Build health status object from startup context with optional user health check."""
|
|
167
|
-
|
|
168
|
-
agent_config = startup_context.get("agent_config", {})
|
|
169
|
-
|
|
170
|
-
# Check if user provided a health_check function
|
|
171
|
-
health_check_fn = agent_config.get("health_check")
|
|
172
|
-
health_check_ttl = agent_config.get("health_check_ttl", 15)
|
|
173
|
-
|
|
174
|
-
# If health check is configured, use the cache
|
|
175
|
-
if health_check_fn:
|
|
176
|
-
from ...shared.health_check_manager import get_health_status_with_cache
|
|
177
|
-
|
|
178
|
-
return await get_health_status_with_cache(
|
|
179
|
-
agent_id=agent_id,
|
|
180
|
-
health_check_fn=health_check_fn,
|
|
181
|
-
agent_config=agent_config,
|
|
182
|
-
startup_context=startup_context,
|
|
183
|
-
ttl=health_check_ttl,
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
# No health check configured - use existing logic
|
|
187
|
-
existing_health_status = startup_context.get("health_status")
|
|
188
|
-
|
|
189
|
-
if existing_health_status:
|
|
190
|
-
# Update timestamp to current time for fresh heartbeat
|
|
191
|
-
if hasattr(existing_health_status, "timestamp"):
|
|
192
|
-
existing_health_status.timestamp = datetime.now(UTC)
|
|
193
|
-
return existing_health_status
|
|
194
|
-
|
|
195
|
-
# Build minimal health status from context if none exists
|
|
196
|
-
return HealthStatus(
|
|
197
|
-
agent_name=agent_id,
|
|
198
|
-
status=HealthStatusType.HEALTHY,
|
|
199
|
-
capabilities=agent_config.get("capabilities", []),
|
|
200
|
-
timestamp=datetime.now(UTC),
|
|
201
|
-
version=agent_config.get("version", "1.0.0"),
|
|
202
|
-
metadata=agent_config,
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
def _log_heartbeat_request(
|
|
206
|
-
self, heartbeat_context: dict[str, Any], heartbeat_count: int
|
|
207
|
-
) -> None:
|
|
208
|
-
"""Log heartbeat request details for debugging."""
|
|
209
|
-
|
|
210
|
-
health_status = heartbeat_context.get("health_status")
|
|
211
|
-
if not health_status:
|
|
212
|
-
return
|
|
213
|
-
|
|
214
|
-
# Convert health status to dict for logging
|
|
215
|
-
if hasattr(health_status, "__dict__"):
|
|
216
|
-
health_dict = {
|
|
217
|
-
"agent_name": getattr(health_status, "agent_name", "unknown"),
|
|
218
|
-
"status": (
|
|
219
|
-
getattr(health_status, "status", "healthy").value
|
|
220
|
-
if hasattr(getattr(health_status, "status", "healthy"), "value")
|
|
221
|
-
else str(getattr(health_status, "status", "healthy"))
|
|
222
|
-
),
|
|
223
|
-
"capabilities": getattr(health_status, "capabilities", []),
|
|
224
|
-
"timestamp": (
|
|
225
|
-
getattr(health_status, "timestamp", "").isoformat()
|
|
226
|
-
if hasattr(getattr(health_status, "timestamp", ""), "isoformat")
|
|
227
|
-
else str(getattr(health_status, "timestamp", ""))
|
|
228
|
-
),
|
|
229
|
-
"version": getattr(health_status, "version", "1.0.0"),
|
|
230
|
-
"metadata": getattr(health_status, "metadata", {}),
|
|
231
|
-
}
|
|
232
|
-
else:
|
|
233
|
-
health_dict = health_status
|
|
234
|
-
|
|
235
|
-
request_json = json.dumps(health_dict, indent=2, default=str)
|
|
236
|
-
|
|
237
|
-
def _process_heartbeat_result(
|
|
238
|
-
self, result: Any, agent_id: str, heartbeat_count: int
|
|
239
|
-
) -> bool:
|
|
240
|
-
"""Process heartbeat pipeline result and log appropriately."""
|
|
241
|
-
|
|
242
|
-
if result.is_success():
|
|
243
|
-
# Check fast heartbeat status to understand what happened
|
|
244
|
-
fast_heartbeat_status = result.context.get("fast_heartbeat_status")
|
|
245
|
-
heartbeat_response = result.context.get("heartbeat_response")
|
|
246
|
-
|
|
247
|
-
# Import FastHeartbeatStatus for comparison
|
|
248
|
-
from ...shared.fast_heartbeat_status import (
|
|
249
|
-
FastHeartbeatStatus,
|
|
250
|
-
FastHeartbeatStatusUtil,
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
if not fast_heartbeat_status:
|
|
254
|
-
self.logger.error(
|
|
255
|
-
f"💔 Heartbeat #{heartbeat_count} failed for agent '{agent_id}' - missing fast_heartbeat_status"
|
|
256
|
-
)
|
|
257
|
-
return False
|
|
258
|
-
|
|
259
|
-
if FastHeartbeatStatusUtil.should_skip_for_optimization(
|
|
260
|
-
fast_heartbeat_status
|
|
261
|
-
):
|
|
262
|
-
# Fast heartbeat optimization - no changes detected
|
|
263
|
-
self.logger.debug(
|
|
264
|
-
f"🚀 Heartbeat #{heartbeat_count} optimized for agent '{agent_id}' - no changes detected"
|
|
265
|
-
)
|
|
266
|
-
return True
|
|
267
|
-
elif FastHeartbeatStatusUtil.should_skip_for_resilience(
|
|
268
|
-
fast_heartbeat_status
|
|
269
|
-
):
|
|
270
|
-
# Fast heartbeat resilience - registry/network error
|
|
271
|
-
self.logger.info(
|
|
272
|
-
f"⚠️ Heartbeat #{heartbeat_count} skipped for agent '{agent_id}' - resilience mode ({fast_heartbeat_status.value})"
|
|
273
|
-
)
|
|
274
|
-
return True
|
|
275
|
-
elif FastHeartbeatStatusUtil.requires_full_heartbeat(fast_heartbeat_status):
|
|
276
|
-
# Full heartbeat was executed - check for response
|
|
277
|
-
if heartbeat_response:
|
|
278
|
-
# Log response details for debugging
|
|
279
|
-
response_json = json.dumps(
|
|
280
|
-
heartbeat_response, indent=2, default=str
|
|
281
|
-
)
|
|
282
|
-
self.logger.debug(
|
|
283
|
-
f"🔍 Heartbeat response #{heartbeat_count}:\n{response_json}"
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
# Log dependency resolution info if available
|
|
287
|
-
deps_resolved = heartbeat_response.get("dependencies_resolved", {})
|
|
288
|
-
if deps_resolved:
|
|
289
|
-
self.logger.info(
|
|
290
|
-
f"🔗 Dependencies resolved: {len(deps_resolved)} items"
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
self.logger.info(
|
|
294
|
-
f"💚 Heartbeat #{heartbeat_count} sent successfully for agent '{agent_id}' (full refresh: {fast_heartbeat_status.value})"
|
|
295
|
-
)
|
|
296
|
-
return True
|
|
297
|
-
else:
|
|
298
|
-
self.logger.warning(
|
|
299
|
-
f"💔 Heartbeat #{heartbeat_count} failed for agent '{agent_id}' - full heartbeat expected but no response"
|
|
300
|
-
)
|
|
301
|
-
return False
|
|
302
|
-
else:
|
|
303
|
-
self.logger.warning(
|
|
304
|
-
f"💔 Heartbeat #{heartbeat_count} unknown status for agent '{agent_id}': {fast_heartbeat_status}"
|
|
305
|
-
)
|
|
306
|
-
return False
|
|
307
|
-
else:
|
|
308
|
-
self.logger.warning(
|
|
309
|
-
f"💔 Heartbeat #{heartbeat_count} pipeline failed for agent '{agent_id}': {result.message}"
|
|
310
|
-
)
|
|
311
|
-
return False
|
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Heartbeat pipeline for MCP Mesh periodic operations.
|
|
3
|
-
|
|
4
|
-
Provides structured execution of heartbeat operations with proper error handling
|
|
5
|
-
and logging. Runs every 30 seconds to maintain registry communication and
|
|
6
|
-
dependency resolution.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import logging
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
from ...shared.fast_heartbeat_status import FastHeartbeatStatus, FastHeartbeatStatusUtil
|
|
13
|
-
from ..shared import PipelineResult, PipelineStatus
|
|
14
|
-
from ..shared.mesh_pipeline import MeshPipeline
|
|
15
|
-
from .dependency_resolution import DependencyResolutionStep
|
|
16
|
-
from .fast_heartbeat_check import FastHeartbeatStep
|
|
17
|
-
from .heartbeat_send import HeartbeatSendStep
|
|
18
|
-
from .llm_tools_resolution import LLMToolsResolutionStep
|
|
19
|
-
from .registry_connection import RegistryConnectionStep
|
|
20
|
-
|
|
21
|
-
logger = logging.getLogger(__name__)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class HeartbeatPipeline(MeshPipeline):
|
|
25
|
-
"""
|
|
26
|
-
Specialized pipeline for heartbeat operations with fast optimization.
|
|
27
|
-
|
|
28
|
-
Executes the five core heartbeat steps in sequence:
|
|
29
|
-
1. Registry connection preparation
|
|
30
|
-
2. Fast heartbeat check (HEAD request)
|
|
31
|
-
3. Heartbeat sending (conditional POST request)
|
|
32
|
-
4. Dependency resolution (conditional)
|
|
33
|
-
5. LLM tools resolution (conditional)
|
|
34
|
-
|
|
35
|
-
Steps 3, 4, and 5 only run if fast heartbeat indicates changes are needed.
|
|
36
|
-
Provides optimization for NO_CHANGES and resilience for error conditions.
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
def __init__(self):
|
|
40
|
-
super().__init__(name="heartbeat-pipeline")
|
|
41
|
-
self._setup_heartbeat_steps()
|
|
42
|
-
|
|
43
|
-
def _setup_heartbeat_steps(self) -> None:
|
|
44
|
-
"""Setup the heartbeat pipeline steps."""
|
|
45
|
-
steps = [
|
|
46
|
-
RegistryConnectionStep(),
|
|
47
|
-
FastHeartbeatStep(),
|
|
48
|
-
HeartbeatSendStep(required=True),
|
|
49
|
-
DependencyResolutionStep(),
|
|
50
|
-
LLMToolsResolutionStep(),
|
|
51
|
-
]
|
|
52
|
-
|
|
53
|
-
self.add_steps(steps)
|
|
54
|
-
self.logger.trace(f"Heartbeat pipeline configured with {len(steps)} steps")
|
|
55
|
-
|
|
56
|
-
async def execute_heartbeat_cycle(
|
|
57
|
-
self, heartbeat_context: dict[str, Any]
|
|
58
|
-
) -> PipelineResult:
|
|
59
|
-
"""
|
|
60
|
-
Execute a complete heartbeat cycle with enhanced error handling.
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
heartbeat_context: Context containing registry_wrapper, agent_id, health_status, etc.
|
|
64
|
-
|
|
65
|
-
Returns:
|
|
66
|
-
PipelineResult with execution status and any context updates
|
|
67
|
-
"""
|
|
68
|
-
self.logger.trace("Starting heartbeat pipeline execution")
|
|
69
|
-
|
|
70
|
-
# Initialize pipeline context with heartbeat-specific data
|
|
71
|
-
self.context.clear()
|
|
72
|
-
self.context.update(heartbeat_context)
|
|
73
|
-
|
|
74
|
-
try:
|
|
75
|
-
# Execute the pipeline with conditional logic for fast optimization
|
|
76
|
-
result = await self._execute_with_conditional_logic()
|
|
77
|
-
|
|
78
|
-
if result.is_success():
|
|
79
|
-
self.logger.trace("✅ Heartbeat pipeline completed successfully")
|
|
80
|
-
elif result.status == PipelineStatus.PARTIAL:
|
|
81
|
-
self.logger.warning(
|
|
82
|
-
f"⚠️ Heartbeat pipeline completed partially: {result.message}"
|
|
83
|
-
)
|
|
84
|
-
# Log which steps failed
|
|
85
|
-
if result.errors:
|
|
86
|
-
for error in result.errors:
|
|
87
|
-
self.logger.warning(f" - Step error: {error}")
|
|
88
|
-
else:
|
|
89
|
-
self.logger.error(f"❌ Heartbeat pipeline failed: {result.message}")
|
|
90
|
-
# Log detailed error information
|
|
91
|
-
if result.errors:
|
|
92
|
-
for error in result.errors:
|
|
93
|
-
self.logger.error(f" - Pipeline error: {error}")
|
|
94
|
-
|
|
95
|
-
return result
|
|
96
|
-
|
|
97
|
-
except Exception as e:
|
|
98
|
-
# Log detailed error information for debugging
|
|
99
|
-
import traceback
|
|
100
|
-
|
|
101
|
-
self.logger.error(
|
|
102
|
-
f"❌ Heartbeat pipeline failed with exception: {e}\n"
|
|
103
|
-
f"Context keys: {list(self.context.keys())}\n"
|
|
104
|
-
f"Traceback: {traceback.format_exc()}"
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
# Create failure result with detailed context
|
|
108
|
-
failure_result = PipelineResult(
|
|
109
|
-
status=PipelineStatus.FAILED,
|
|
110
|
-
message=f"Heartbeat pipeline exception: {str(e)[:200]}...", # Truncate long error messages
|
|
111
|
-
context=self.context,
|
|
112
|
-
)
|
|
113
|
-
failure_result.add_error(str(e))
|
|
114
|
-
|
|
115
|
-
return failure_result
|
|
116
|
-
|
|
117
|
-
async def _execute_with_conditional_logic(self) -> PipelineResult:
|
|
118
|
-
"""
|
|
119
|
-
Execute pipeline with conditional logic based on fast heartbeat status.
|
|
120
|
-
|
|
121
|
-
Always executes:
|
|
122
|
-
- RegistryConnectionStep
|
|
123
|
-
- FastHeartbeatStep
|
|
124
|
-
|
|
125
|
-
Conditionally executes based on fast heartbeat status:
|
|
126
|
-
- NO_CHANGES: Skip remaining steps (optimization)
|
|
127
|
-
- TOPOLOGY_CHANGED, AGENT_UNKNOWN: Execute all remaining steps
|
|
128
|
-
- REGISTRY_ERROR, NETWORK_ERROR: Skip remaining steps (resilience)
|
|
129
|
-
|
|
130
|
-
Returns:
|
|
131
|
-
PipelineResult with execution status and context
|
|
132
|
-
"""
|
|
133
|
-
overall_result = PipelineResult(
|
|
134
|
-
message="Heartbeat pipeline execution completed"
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
# Track which steps were executed for logging
|
|
138
|
-
executed_steps = []
|
|
139
|
-
skipped_steps = []
|
|
140
|
-
|
|
141
|
-
try:
|
|
142
|
-
# Always execute registry connection and fast heartbeat steps
|
|
143
|
-
mandatory_steps = self.steps[
|
|
144
|
-
:2
|
|
145
|
-
] # RegistryConnectionStep, FastHeartbeatStep
|
|
146
|
-
conditional_steps = self.steps[
|
|
147
|
-
2:
|
|
148
|
-
] # HeartbeatSendStep, DependencyResolutionStep, LLMToolsResolutionStep
|
|
149
|
-
|
|
150
|
-
# Execute mandatory steps
|
|
151
|
-
for step in mandatory_steps:
|
|
152
|
-
self.logger.trace(f"Executing mandatory step: {step.name}")
|
|
153
|
-
|
|
154
|
-
step_result = await step.execute(self.context)
|
|
155
|
-
executed_steps.append(step.name)
|
|
156
|
-
|
|
157
|
-
# Merge step context into pipeline context
|
|
158
|
-
self.context.update(step_result.context)
|
|
159
|
-
|
|
160
|
-
# If step fails, handle accordingly
|
|
161
|
-
if not step_result.is_success():
|
|
162
|
-
overall_result.status = PipelineStatus.FAILED
|
|
163
|
-
overall_result.message = (
|
|
164
|
-
f"Mandatory step '{step.name}' failed: {step_result.message}"
|
|
165
|
-
)
|
|
166
|
-
overall_result.add_error(
|
|
167
|
-
f"Step '{step.name}': {step_result.message}"
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
if step.required:
|
|
171
|
-
# Stop execution if required step fails
|
|
172
|
-
for key, value in self.context.items():
|
|
173
|
-
overall_result.add_context(key, value)
|
|
174
|
-
return overall_result
|
|
175
|
-
|
|
176
|
-
# Check fast heartbeat status for conditional execution
|
|
177
|
-
fast_heartbeat_status = self.context.get("fast_heartbeat_status")
|
|
178
|
-
|
|
179
|
-
if fast_heartbeat_status is None:
|
|
180
|
-
# Fast heartbeat step failed to set status - fallback to full execution
|
|
181
|
-
self.logger.warning(
|
|
182
|
-
"⚠️ Fast heartbeat status not found - falling back to full execution"
|
|
183
|
-
)
|
|
184
|
-
should_execute_remaining = True
|
|
185
|
-
reason = "fallback (missing status)"
|
|
186
|
-
elif FastHeartbeatStatusUtil.should_skip_for_optimization(
|
|
187
|
-
fast_heartbeat_status
|
|
188
|
-
):
|
|
189
|
-
# NO_CHANGES - skip for optimization
|
|
190
|
-
should_execute_remaining = False
|
|
191
|
-
reason = "optimization (no changes detected)"
|
|
192
|
-
self.logger.trace(
|
|
193
|
-
f"🚀 Skipping remaining steps for optimization: {reason}"
|
|
194
|
-
)
|
|
195
|
-
elif FastHeartbeatStatusUtil.should_skip_for_resilience(
|
|
196
|
-
fast_heartbeat_status
|
|
197
|
-
):
|
|
198
|
-
# REGISTRY_ERROR, NETWORK_ERROR - skip for resilience
|
|
199
|
-
should_execute_remaining = False
|
|
200
|
-
reason = "resilience (preserve existing state)"
|
|
201
|
-
self.logger.warning(
|
|
202
|
-
f"⚠️ Skipping remaining steps for resilience: {reason}"
|
|
203
|
-
)
|
|
204
|
-
elif FastHeartbeatStatusUtil.requires_full_heartbeat(fast_heartbeat_status):
|
|
205
|
-
# TOPOLOGY_CHANGED, AGENT_UNKNOWN - execute full pipeline
|
|
206
|
-
should_execute_remaining = True
|
|
207
|
-
reason = "changes detected or re-registration needed"
|
|
208
|
-
self.logger.info(f"🔄 Executing remaining steps: {reason}")
|
|
209
|
-
else:
|
|
210
|
-
# Unknown status - fallback to full execution
|
|
211
|
-
self.logger.warning(
|
|
212
|
-
f"⚠️ Unknown fast heartbeat status '{fast_heartbeat_status}' - falling back to full execution"
|
|
213
|
-
)
|
|
214
|
-
should_execute_remaining = True
|
|
215
|
-
reason = "fallback (unknown status)"
|
|
216
|
-
|
|
217
|
-
# Execute or skip conditional steps based on decision
|
|
218
|
-
if should_execute_remaining:
|
|
219
|
-
for step in conditional_steps:
|
|
220
|
-
self.logger.trace(f"Executing conditional step: {step.name}")
|
|
221
|
-
|
|
222
|
-
step_result = await step.execute(self.context)
|
|
223
|
-
executed_steps.append(step.name)
|
|
224
|
-
|
|
225
|
-
# Merge step context into pipeline context
|
|
226
|
-
self.context.update(step_result.context)
|
|
227
|
-
|
|
228
|
-
# Handle step failure
|
|
229
|
-
if not step_result.is_success():
|
|
230
|
-
if step.required:
|
|
231
|
-
overall_result.status = PipelineStatus.FAILED
|
|
232
|
-
overall_result.message = f"Required step '{step.name}' failed: {step_result.message}"
|
|
233
|
-
overall_result.add_error(
|
|
234
|
-
f"Step '{step.name}': {step_result.message}"
|
|
235
|
-
)
|
|
236
|
-
break
|
|
237
|
-
else:
|
|
238
|
-
# Optional step failed - mark as partial success
|
|
239
|
-
if overall_result.status == PipelineStatus.SUCCESS:
|
|
240
|
-
overall_result.status = PipelineStatus.PARTIAL
|
|
241
|
-
overall_result.add_error(
|
|
242
|
-
f"Optional step '{step.name}': {step_result.message}"
|
|
243
|
-
)
|
|
244
|
-
self.logger.warning(
|
|
245
|
-
f"⚠️ Optional step '{step.name}' failed: {step_result.message}"
|
|
246
|
-
)
|
|
247
|
-
else:
|
|
248
|
-
# Mark skipped steps
|
|
249
|
-
for step in conditional_steps:
|
|
250
|
-
skipped_steps.append(step.name)
|
|
251
|
-
|
|
252
|
-
# Set final result message
|
|
253
|
-
if executed_steps and skipped_steps:
|
|
254
|
-
overall_result.message = (
|
|
255
|
-
f"Pipeline completed with conditional execution - "
|
|
256
|
-
f"executed: {executed_steps}, skipped: {skipped_steps} ({reason})"
|
|
257
|
-
)
|
|
258
|
-
elif executed_steps:
|
|
259
|
-
overall_result.message = (
|
|
260
|
-
f"Pipeline completed - executed: {executed_steps} ({reason})"
|
|
261
|
-
)
|
|
262
|
-
else:
|
|
263
|
-
overall_result.message = (
|
|
264
|
-
f"Pipeline completed - all steps skipped ({reason})"
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
# Add final context
|
|
268
|
-
for key, value in self.context.items():
|
|
269
|
-
overall_result.add_context(key, value)
|
|
270
|
-
|
|
271
|
-
return overall_result
|
|
272
|
-
|
|
273
|
-
except Exception as e:
|
|
274
|
-
# Handle unexpected exceptions
|
|
275
|
-
overall_result.status = PipelineStatus.FAILED
|
|
276
|
-
overall_result.message = f"Pipeline execution failed with exception: {e}"
|
|
277
|
-
overall_result.add_error(str(e))
|
|
278
|
-
for key, value in self.context.items():
|
|
279
|
-
overall_result.add_context(key, value)
|
|
280
|
-
|
|
281
|
-
self.logger.error(f"❌ Conditional pipeline execution failed: {e}")
|
|
282
|
-
return overall_result
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Heartbeat sending step for MCP Mesh pipeline.
|
|
3
|
-
|
|
4
|
-
Handles sending heartbeat to the mesh registry service.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
from ..shared import PipelineResult, PipelineStatus, PipelineStep
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class HeartbeatSendStep(PipelineStep):
|
|
16
|
-
"""
|
|
17
|
-
Sends heartbeat to the mesh registry.
|
|
18
|
-
|
|
19
|
-
Performs the actual registry communication using the prepared
|
|
20
|
-
heartbeat data from previous steps.
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
def __init__(self, required: bool = True):
|
|
24
|
-
super().__init__(
|
|
25
|
-
name="heartbeat-send",
|
|
26
|
-
required=required,
|
|
27
|
-
description="Send heartbeat to mesh registry",
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
async def execute(self, context: dict[str, Any]) -> PipelineResult:
|
|
31
|
-
"""Send heartbeat to registry or print JSON in debug mode."""
|
|
32
|
-
result = PipelineResult(message="Heartbeat processed successfully")
|
|
33
|
-
|
|
34
|
-
try:
|
|
35
|
-
# Get required context
|
|
36
|
-
health_status = context.get("health_status")
|
|
37
|
-
agent_id = context.get("agent_id", "unknown-agent")
|
|
38
|
-
registration_data = context.get("registration_data")
|
|
39
|
-
|
|
40
|
-
if not health_status:
|
|
41
|
-
raise ValueError("Health status not available in context")
|
|
42
|
-
|
|
43
|
-
# Prepare heartbeat for registry
|
|
44
|
-
self.logger.trace(f"🔍 Preparing heartbeat for agent '{agent_id}'")
|
|
45
|
-
|
|
46
|
-
# Send actual HTTP request to registry
|
|
47
|
-
registry_wrapper = context.get("registry_wrapper")
|
|
48
|
-
|
|
49
|
-
if not registry_wrapper:
|
|
50
|
-
# If no registry wrapper, just log the payload and mark as successful
|
|
51
|
-
self.logger.info(
|
|
52
|
-
f"⚠️ No registry connection - would send heartbeat for agent '{agent_id}'"
|
|
53
|
-
)
|
|
54
|
-
result.add_context(
|
|
55
|
-
"heartbeat_response", {"status": "no_registry", "logged": True}
|
|
56
|
-
)
|
|
57
|
-
result.add_context("dependencies_resolved", {})
|
|
58
|
-
result.message = (
|
|
59
|
-
f"Heartbeat logged for agent '{agent_id}' (no registry)"
|
|
60
|
-
)
|
|
61
|
-
return result
|
|
62
|
-
|
|
63
|
-
self.logger.info(f"💓 Sending heartbeat for agent '{agent_id}'...")
|
|
64
|
-
|
|
65
|
-
response = await registry_wrapper.send_heartbeat_with_dependency_resolution(
|
|
66
|
-
health_status
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
if response:
|
|
70
|
-
# Store response data
|
|
71
|
-
result.add_context("heartbeat_response", response)
|
|
72
|
-
result.add_context(
|
|
73
|
-
"dependencies_resolved",
|
|
74
|
-
response.get("dependencies_resolved", {}),
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
result.message = f"Heartbeat sent successfully for agent '{agent_id}'"
|
|
78
|
-
self.logger.info(f"💚 Heartbeat successful for agent '{agent_id}'")
|
|
79
|
-
|
|
80
|
-
# Log dependency resolution info
|
|
81
|
-
deps_resolved = response.get("dependencies_resolved", {})
|
|
82
|
-
if deps_resolved:
|
|
83
|
-
self.logger.info(
|
|
84
|
-
f"🔗 Dependencies resolved: {len(deps_resolved)} items"
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
else:
|
|
88
|
-
result.status = PipelineStatus.FAILED
|
|
89
|
-
result.message = "Heartbeat failed - no response from registry"
|
|
90
|
-
self.logger.error("💔 Heartbeat failed - no response")
|
|
91
|
-
|
|
92
|
-
except Exception as e:
|
|
93
|
-
result.status = PipelineStatus.FAILED
|
|
94
|
-
result.message = f"Heartbeat processing failed: {e}"
|
|
95
|
-
result.add_error(str(e))
|
|
96
|
-
self.logger.error(f"❌ Heartbeat processing failed: {e}")
|
|
97
|
-
|
|
98
|
-
return result
|