mcp-mesh 0.7.20__py3-none-any.whl → 0.8.0__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.
Files changed (124) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/dependency_injector.py +13 -15
  3. _mcp_mesh/engine/http_wrapper.py +69 -10
  4. _mcp_mesh/engine/mesh_llm_agent.py +29 -10
  5. _mcp_mesh/engine/mesh_llm_agent_injector.py +77 -41
  6. _mcp_mesh/engine/provider_handlers/__init__.py +14 -1
  7. _mcp_mesh/engine/provider_handlers/base_provider_handler.py +114 -8
  8. _mcp_mesh/engine/provider_handlers/claude_handler.py +15 -57
  9. _mcp_mesh/engine/provider_handlers/gemini_handler.py +181 -0
  10. _mcp_mesh/engine/provider_handlers/openai_handler.py +8 -63
  11. _mcp_mesh/engine/provider_handlers/provider_handler_registry.py +16 -10
  12. _mcp_mesh/engine/response_parser.py +61 -15
  13. _mcp_mesh/engine/signature_analyzer.py +58 -68
  14. _mcp_mesh/engine/unified_mcp_proxy.py +19 -35
  15. _mcp_mesh/pipeline/__init__.py +9 -20
  16. _mcp_mesh/pipeline/api_heartbeat/__init__.py +12 -7
  17. _mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +23 -49
  18. _mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +429 -0
  19. _mcp_mesh/pipeline/api_startup/api_pipeline.py +7 -9
  20. _mcp_mesh/pipeline/api_startup/api_server_setup.py +91 -70
  21. _mcp_mesh/pipeline/api_startup/fastapi_discovery.py +22 -23
  22. _mcp_mesh/pipeline/api_startup/middleware_integration.py +32 -24
  23. _mcp_mesh/pipeline/api_startup/route_collection.py +2 -4
  24. _mcp_mesh/pipeline/mcp_heartbeat/__init__.py +5 -17
  25. _mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +710 -0
  26. _mcp_mesh/pipeline/mcp_startup/__init__.py +2 -5
  27. _mcp_mesh/pipeline/mcp_startup/configuration.py +1 -1
  28. _mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +31 -8
  29. _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
  30. _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +23 -11
  31. _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +3 -8
  32. _mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -2
  33. _mcp_mesh/reload.py +1 -3
  34. _mcp_mesh/shared/__init__.py +2 -8
  35. _mcp_mesh/shared/config_resolver.py +124 -80
  36. _mcp_mesh/shared/defaults.py +89 -14
  37. _mcp_mesh/shared/fastapi_middleware_manager.py +149 -91
  38. _mcp_mesh/shared/host_resolver.py +8 -46
  39. _mcp_mesh/shared/server_discovery.py +115 -86
  40. _mcp_mesh/shared/simple_shutdown.py +44 -86
  41. _mcp_mesh/tracing/execution_tracer.py +2 -6
  42. _mcp_mesh/tracing/redis_metadata_publisher.py +24 -79
  43. _mcp_mesh/tracing/trace_context_helper.py +3 -13
  44. _mcp_mesh/tracing/utils.py +29 -15
  45. _mcp_mesh/utils/fastmcp_schema_extractor.py +5 -4
  46. {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/METADATA +7 -5
  47. mcp_mesh-0.8.0.dist-info/RECORD +85 -0
  48. mesh/__init__.py +12 -1
  49. mesh/decorators.py +248 -33
  50. mesh/helpers.py +52 -0
  51. mesh/types.py +40 -13
  52. _mcp_mesh/generated/.openapi-generator/FILES +0 -50
  53. _mcp_mesh/generated/.openapi-generator/VERSION +0 -1
  54. _mcp_mesh/generated/.openapi-generator-ignore +0 -15
  55. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +0 -90
  56. _mcp_mesh/generated/mcp_mesh_registry_client/api/__init__.py +0 -6
  57. _mcp_mesh/generated/mcp_mesh_registry_client/api/agents_api.py +0 -1088
  58. _mcp_mesh/generated/mcp_mesh_registry_client/api/health_api.py +0 -764
  59. _mcp_mesh/generated/mcp_mesh_registry_client/api/tracing_api.py +0 -303
  60. _mcp_mesh/generated/mcp_mesh_registry_client/api_client.py +0 -798
  61. _mcp_mesh/generated/mcp_mesh_registry_client/api_response.py +0 -21
  62. _mcp_mesh/generated/mcp_mesh_registry_client/configuration.py +0 -577
  63. _mcp_mesh/generated/mcp_mesh_registry_client/exceptions.py +0 -217
  64. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +0 -55
  65. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_info.py +0 -158
  66. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata.py +0 -126
  67. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner.py +0 -139
  68. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_metadata_dependencies_inner_one_of.py +0 -92
  69. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration.py +0 -103
  70. _mcp_mesh/generated/mcp_mesh_registry_client/models/agent_registration_metadata.py +0 -136
  71. _mcp_mesh/generated/mcp_mesh_registry_client/models/agents_list_response.py +0 -100
  72. _mcp_mesh/generated/mcp_mesh_registry_client/models/capability_info.py +0 -107
  73. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_metadata.py +0 -112
  74. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_agent_request.py +0 -103
  75. _mcp_mesh/generated/mcp_mesh_registry_client/models/decorator_info.py +0 -105
  76. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_info.py +0 -103
  77. _mcp_mesh/generated/mcp_mesh_registry_client/models/dependency_resolution_info.py +0 -106
  78. _mcp_mesh/generated/mcp_mesh_registry_client/models/error_response.py +0 -91
  79. _mcp_mesh/generated/mcp_mesh_registry_client/models/health_response.py +0 -103
  80. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request.py +0 -101
  81. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_request_metadata.py +0 -111
  82. _mcp_mesh/generated/mcp_mesh_registry_client/models/heartbeat_response.py +0 -117
  83. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider.py +0 -93
  84. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_provider_resolution_info.py +0 -106
  85. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +0 -109
  86. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +0 -139
  87. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +0 -91
  88. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +0 -101
  89. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_resolution_info.py +0 -120
  90. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_register_metadata.py +0 -112
  91. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +0 -129
  92. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +0 -153
  93. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response_dependencies_resolved_value_inner.py +0 -101
  94. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_dependency_registration.py +0 -93
  95. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_register_metadata.py +0 -107
  96. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +0 -117
  97. _mcp_mesh/generated/mcp_mesh_registry_client/models/registration_response.py +0 -119
  98. _mcp_mesh/generated/mcp_mesh_registry_client/models/resolved_llm_provider.py +0 -110
  99. _mcp_mesh/generated/mcp_mesh_registry_client/models/rich_dependency.py +0 -93
  100. _mcp_mesh/generated/mcp_mesh_registry_client/models/root_response.py +0 -92
  101. _mcp_mesh/generated/mcp_mesh_registry_client/models/standardized_dependency.py +0 -93
  102. _mcp_mesh/generated/mcp_mesh_registry_client/models/trace_event.py +0 -106
  103. _mcp_mesh/generated/mcp_mesh_registry_client/py.typed +0 -0
  104. _mcp_mesh/generated/mcp_mesh_registry_client/rest.py +0 -259
  105. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +0 -418
  106. _mcp_mesh/pipeline/api_heartbeat/api_fast_heartbeat_check.py +0 -117
  107. _mcp_mesh/pipeline/api_heartbeat/api_health_check.py +0 -140
  108. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_orchestrator.py +0 -247
  109. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_pipeline.py +0 -311
  110. _mcp_mesh/pipeline/api_heartbeat/api_heartbeat_send.py +0 -386
  111. _mcp_mesh/pipeline/api_heartbeat/api_registry_connection.py +0 -104
  112. _mcp_mesh/pipeline/mcp_heartbeat/dependency_resolution.py +0 -396
  113. _mcp_mesh/pipeline/mcp_heartbeat/fast_heartbeat_check.py +0 -116
  114. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_orchestrator.py +0 -311
  115. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +0 -282
  116. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_send.py +0 -98
  117. _mcp_mesh/pipeline/mcp_heartbeat/lifespan_integration.py +0 -84
  118. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +0 -264
  119. _mcp_mesh/pipeline/mcp_heartbeat/registry_connection.py +0 -79
  120. _mcp_mesh/pipeline/shared/registry_connection.py +0 -80
  121. _mcp_mesh/shared/registry_client_wrapper.py +0 -515
  122. mcp_mesh-0.7.20.dist-info/RECORD +0 -152
  123. {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/WHEEL +0 -0
  124. {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,418 +0,0 @@
1
- """
2
- API dependency resolution step for API heartbeat pipeline.
3
-
4
- Handles processing dependency resolution from registry response and
5
- updating the dependency injection system for FastAPI route handlers.
6
- """
7
-
8
- import json
9
- import logging
10
- from typing import Any
11
-
12
- from ...engine.dependency_injector import get_global_injector
13
- from ..shared import PipelineResult, PipelineStatus, PipelineStep
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
- # Global state for dependency hash tracking across heartbeat cycles
18
- _last_api_dependency_hash = None
19
-
20
-
21
- class APIDependencyResolutionStep(PipelineStep):
22
- """
23
- Processes dependency resolution from registry response for API services.
24
-
25
- Takes the dependencies_resolved data from the heartbeat response
26
- and updates dependency injection for FastAPI route handlers.
27
-
28
- Similar to MCP dependency resolution but adapted for:
29
- - FastAPI route handlers instead of MCP tools
30
- - Single "api_endpoint_handler" function instead of multiple tools
31
- - Route-level dependency mapping instead of tool-level mapping
32
- """
33
-
34
- def __init__(self):
35
- super().__init__(
36
- name="api-dependency-resolution",
37
- required=False, # Optional - can work without dependencies
38
- description="Process dependency resolution for API route handlers",
39
- )
40
-
41
- async def execute(self, context: dict[str, Any]) -> PipelineResult:
42
- """Process dependency resolution with hash-based change detection."""
43
- self.logger.debug("Processing API dependency resolution...")
44
-
45
- result = PipelineResult(message="API dependency resolution processed")
46
-
47
- try:
48
- # Get heartbeat response and registry wrapper
49
- heartbeat_response = context.get("heartbeat_response", {})
50
- registry_wrapper = context.get("registry_wrapper")
51
-
52
- if not heartbeat_response or not registry_wrapper:
53
- result.status = PipelineStatus.SUCCESS
54
- result.message = (
55
- "No heartbeat response or registry wrapper - completed successfully"
56
- )
57
- self.logger.debug(
58
- "No heartbeat response to process - this is normal for API services"
59
- )
60
- return result
61
-
62
- # Use the same hash-based change detection pattern as MCP
63
- await self.process_heartbeat_response_for_api_rewiring(heartbeat_response)
64
-
65
- # For context consistency, extract dependency count
66
- dependencies_resolved = registry_wrapper.parse_tool_dependencies(
67
- heartbeat_response
68
- )
69
- dependency_count = sum(
70
- len(deps) if isinstance(deps, list) else 0
71
- for deps in dependencies_resolved.values()
72
- )
73
-
74
- # Store processed dependencies info for context
75
- result.add_context("dependency_count", dependency_count)
76
- result.add_context("dependencies_resolved", dependencies_resolved)
77
-
78
- result.message = (
79
- "API dependency resolution completed (efficient hash-based)"
80
- )
81
-
82
- if dependency_count > 0:
83
- self.logger.info(f"🔗 Dependencies resolved: {dependency_count} items")
84
-
85
- # Log function registry status for debugging
86
- injector = get_global_injector()
87
- function_count = len(injector._function_registry)
88
- self.logger.debug(f"Function registry contains {function_count} functions")
89
-
90
- except Exception as e:
91
- result.status = PipelineStatus.FAILED
92
- result.message = f"API dependency resolution failed: {e}"
93
- result.add_error(str(e))
94
- self.logger.error(f"❌ API dependency resolution failed: {e}")
95
-
96
- return result
97
-
98
- def _extract_dependency_state(
99
- self, heartbeat_response: dict[str, Any]
100
- ) -> dict[str, list[dict[str, Any]]]:
101
- """Extract dependency state structure from heartbeat response.
102
-
103
- Preserves array structure and order from registry to support multiple
104
- dependencies with the same capability name (e.g., different versions/tags).
105
-
106
- For API services, dependencies are typically under a single function
107
- (usually "api_endpoint_handler") but we still follow the same pattern.
108
-
109
- Returns:
110
- {function_name: [{capability, endpoint, function_name, status, agent_id, kwargs}, ...]}
111
- """
112
- state = {}
113
- dependencies_resolved = heartbeat_response.get("dependencies_resolved", {})
114
-
115
- for function_name, dependency_list in dependencies_resolved.items():
116
- if not isinstance(dependency_list, list):
117
- continue
118
-
119
- state[function_name] = []
120
- for dep_resolution in dependency_list:
121
- if (
122
- not isinstance(dep_resolution, dict)
123
- or "capability" not in dep_resolution
124
- ):
125
- continue
126
-
127
- # Preserve array structure to maintain order and support duplicate capabilities
128
- state[function_name].append(
129
- {
130
- "capability": dep_resolution["capability"],
131
- "endpoint": dep_resolution.get("endpoint", ""),
132
- "function_name": dep_resolution.get("function_name", ""),
133
- "status": dep_resolution.get("status", ""),
134
- "agent_id": dep_resolution.get("agent_id", ""),
135
- "kwargs": dep_resolution.get("kwargs", {}),
136
- }
137
- )
138
-
139
- return state
140
-
141
- def _hash_dependency_state(self, state: dict) -> str:
142
- """Create hash of dependency state structure."""
143
- import hashlib
144
-
145
- # Convert to sorted JSON string for consistent hashing
146
- state_json = json.dumps(state, sort_keys=True)
147
- return hashlib.sha256(state_json.encode()).hexdigest()[
148
- :16
149
- ] # First 16 chars for readability
150
-
151
- async def process_heartbeat_response_for_api_rewiring(
152
- self, heartbeat_response: dict[str, Any]
153
- ) -> None:
154
- """Process heartbeat response to update API route dependency injection.
155
-
156
- Uses hash-based comparison to efficiently detect when ANY dependency changes
157
- and then updates ALL affected route handlers in one operation.
158
-
159
- Resilience logic (same as MCP):
160
- - No response (connection error, 5xx) → Skip entirely (keep existing wiring)
161
- - 2xx response with empty dependencies → Unwire all dependencies
162
- - 2xx response with partial dependencies → Update to match registry exactly
163
- """
164
- try:
165
- if not heartbeat_response:
166
- # No response from registry (connection error, timeout, 5xx)
167
- # → Skip entirely for resilience (keep existing dependencies)
168
- self.logger.debug(
169
- "No heartbeat response - skipping API rewiring for resilience"
170
- )
171
- return
172
-
173
- # Extract current dependency state structure
174
- current_state = self._extract_dependency_state(heartbeat_response)
175
-
176
- # IMPORTANT: Empty state from successful response means "unwire everything"
177
- # This is different from "no response" which means "keep existing for resilience"
178
-
179
- # Hash the current state (including empty state)
180
- current_hash = self._hash_dependency_state(current_state)
181
-
182
- # Compare with previous state (use global variable with API-specific name)
183
- global _last_api_dependency_hash
184
- if current_hash == _last_api_dependency_hash:
185
- self.logger.debug(
186
- f"🔄 API dependency state unchanged (hash: {current_hash}), skipping rewiring"
187
- )
188
- return
189
-
190
- # State changed - determine what changed
191
- function_count = len(current_state)
192
- total_deps = sum(len(deps) for deps in current_state.values())
193
-
194
- if _last_api_dependency_hash is None:
195
- if function_count > 0:
196
- self.logger.debug(
197
- f"Initial API dependency state detected: {function_count} functions, {total_deps} dependencies"
198
- )
199
- else:
200
- self.logger.debug(
201
- "Initial API dependency state detected: no dependencies"
202
- )
203
- else:
204
- self.logger.debug(
205
- f"API dependency state changed (hash: {_last_api_dependency_hash} → {current_hash})"
206
- )
207
- if function_count > 0:
208
- self.logger.debug(
209
- f"Updating API dependencies for {function_count} functions ({total_deps} total dependencies)"
210
- )
211
- else:
212
- self.logger.debug(
213
- "Registry reports no API dependencies - unwiring all existing dependencies"
214
- )
215
-
216
- # Import here to avoid circular imports
217
- from ...engine.dependency_injector import get_global_injector
218
-
219
- injector = get_global_injector()
220
-
221
- # Step 1: Collect all dependency keys (func_id:dep_index) that should exist
222
- # Map tool names to func_ids first
223
- from ...engine.decorator_registry import DecoratorRegistry
224
-
225
- tool_name_to_func_id = {}
226
- mesh_tools = DecoratorRegistry.get_mesh_tools()
227
- for tool_name, decorated_func in mesh_tools.items():
228
- func = decorated_func.function
229
- func_id = f"{func.__module__}.{func.__qualname__}"
230
- tool_name_to_func_id[tool_name] = func_id
231
-
232
- target_dependency_keys = set()
233
- for function_name, dependency_list in current_state.items():
234
- # Map tool name to func_id
235
- func_id = tool_name_to_func_id.get(function_name, function_name)
236
- for dep_index in range(len(dependency_list)):
237
- dep_key = f"{func_id}:dep_{dep_index}"
238
- target_dependency_keys.add(dep_key)
239
-
240
- # Step 2: Find existing dependency keys that need to be removed (unwired)
241
- # This handles the case where registry stops reporting some dependencies
242
- existing_dependency_keys = (
243
- set(injector._dependencies.keys())
244
- if hasattr(injector, "_dependencies")
245
- else set()
246
- )
247
- keys_to_remove = existing_dependency_keys - target_dependency_keys
248
-
249
- unwired_count = 0
250
- for dep_key in keys_to_remove:
251
- await injector.unregister_dependency(dep_key)
252
- unwired_count += 1
253
- self.logger.debug(
254
- f"Unwired API dependency '{dep_key}' (no longer reported by registry)"
255
- )
256
-
257
- # Step 3: Apply all dependency updates using positional indexing
258
- updated_count = 0
259
- for function_name, dependency_list in current_state.items():
260
- # Check if function_name is a route path (METHOD:path format)
261
- # Route paths contain "/" and look like "GET:/api/v1/benchmark-services"
262
- is_route_path = "/" in function_name
263
-
264
- # Map tool name to func_id (using mapping from Step 1)
265
- # For route paths, use the route_id directly as it won't be in tool_name_to_func_id
266
- func_id = tool_name_to_func_id.get(function_name, function_name)
267
-
268
- # Get route wrapper if this is a route path
269
- route_wrapper_info = None
270
- if is_route_path:
271
- route_wrapper_info = DecoratorRegistry.get_route_wrapper(
272
- function_name
273
- )
274
- if not route_wrapper_info:
275
- self.logger.warning(
276
- f"No route wrapper found for '{function_name}' - dependency injection may fail"
277
- )
278
-
279
- for dep_index, dep_info in enumerate(dependency_list):
280
- status = dep_info["status"]
281
- endpoint = dep_info["endpoint"]
282
- dep_function_name = dep_info["function_name"]
283
- capability = dep_info["capability"]
284
- kwargs_config = dep_info.get("kwargs", {})
285
-
286
- if status == "available" and endpoint and dep_function_name:
287
- # Import here to avoid circular imports
288
- import os
289
-
290
- from ...engine.self_dependency_proxy import SelfDependencyProxy
291
- from ...engine.unified_mcp_proxy import EnhancedUnifiedMCPProxy
292
-
293
- # Get current agent ID for self-dependency detection
294
- current_agent_id = None
295
- try:
296
- from ...engine.decorator_registry import DecoratorRegistry
297
-
298
- config = DecoratorRegistry.get_resolved_agent_config()
299
- current_agent_id = config["agent_id"]
300
- except Exception:
301
- # For API services, try environment variable fallback
302
- current_agent_id = os.getenv("MCP_MESH_AGENT_ID")
303
-
304
- target_agent_id = dep_info.get("agent_id")
305
-
306
- # Determine if this is a self-dependency (less common for API services)
307
- is_self_dependency = (
308
- current_agent_id
309
- and target_agent_id
310
- and current_agent_id == target_agent_id
311
- )
312
-
313
- if is_self_dependency:
314
- # Create self-dependency proxy with WRAPPER function (not original)
315
- # The wrapper has dependency injection logic, so calling it ensures
316
- # the target function's dependencies are also injected properly.
317
- wrapper_func = None
318
- if dep_function_name in mesh_tools:
319
- wrapper_func = mesh_tools[dep_function_name].function
320
-
321
- if wrapper_func:
322
- new_proxy = SelfDependencyProxy(
323
- wrapper_func, dep_function_name
324
- )
325
- self.logger.debug(
326
- f"API SELF-DEPENDENCY: Using wrapper for '{capability}' "
327
- f"(local call with full DI support)"
328
- )
329
- else:
330
- # Fallback to original function if wrapper not found
331
- original_func = injector.find_original_function(
332
- dep_function_name
333
- )
334
- if original_func:
335
- new_proxy = SelfDependencyProxy(
336
- original_func, dep_function_name
337
- )
338
- self.logger.warning(
339
- f"⚠️ API SELF-DEPENDENCY: Using original function for '{capability}' "
340
- f"(wrapper not found, DI may not work for nested deps)"
341
- )
342
- else:
343
- self.logger.warning(
344
- f"⚠️ API SELF-DEPENDENCY: Cannot create SelfDependencyProxy for '{capability}', "
345
- f"falling back to HTTP (may cause issues)"
346
- )
347
- # Fall back to unified proxy (same as cross-service)
348
- new_proxy = EnhancedUnifiedMCPProxy(
349
- endpoint,
350
- dep_function_name,
351
- kwargs_config=kwargs_config,
352
- )
353
- else:
354
- # Create cross-service proxy using unified proxy (same as MCP pipeline)
355
- new_proxy = EnhancedUnifiedMCPProxy(
356
- endpoint,
357
- dep_function_name,
358
- kwargs_config=kwargs_config,
359
- )
360
-
361
- # For route paths, directly update the wrapper's dependencies
362
- # This bypasses the injector key-based lookup which doesn't work for routes
363
- if route_wrapper_info:
364
- wrapper = route_wrapper_info.get("wrapper")
365
- if wrapper and hasattr(wrapper, "_mesh_update_dependency"):
366
- wrapper._mesh_update_dependency(dep_index, new_proxy)
367
- updated_count += 1
368
- self.logger.debug(
369
- f"Updated route dependency '{capability}' at position {dep_index} "
370
- f"→ {endpoint}/{dep_function_name} for route '{function_name}'"
371
- )
372
- else:
373
- self.logger.warning(
374
- f"Route wrapper for '{function_name}' doesn't have _mesh_update_dependency method"
375
- )
376
- else:
377
- # Fallback: Register with composite key using func_id for MCP tools
378
- dep_key = f"{func_id}:dep_{dep_index}"
379
- await injector.register_dependency(dep_key, new_proxy)
380
- updated_count += 1
381
-
382
- # Log which functions will be affected
383
- affected_functions = injector._dependency_mapping.get(
384
- dep_key, set()
385
- )
386
- self.logger.debug(
387
- f"Functions affected by '{capability}' at position {dep_index}: {list(affected_functions)}"
388
- )
389
-
390
- self.logger.debug(
391
- f"Updated API dependency '{capability}' at position {dep_index} → {endpoint}/{dep_function_name}"
392
- )
393
- self.logger.debug(
394
- f"Registered dependency '{capability}' at position {dep_index} with key '{dep_key}' (func_id: {func_id})"
395
- )
396
- else:
397
- if status != "available":
398
- self.logger.debug(
399
- f"API dependency '{capability}' at position {dep_index} not available: {status}"
400
- )
401
- else:
402
- self.logger.warning(
403
- f"Cannot update API dependency '{capability}' at position {dep_index}: missing endpoint or function_name"
404
- )
405
-
406
- # Store new hash for next comparison (use global variable)
407
- _last_api_dependency_hash = current_hash
408
-
409
- if unwired_count > 0 or updated_count > 0:
410
- self.logger.debug(
411
- f"API dependency sync: unwired={unwired_count}, updated={updated_count} (hash: {current_hash})"
412
- )
413
-
414
- except Exception as e:
415
- self.logger.error(
416
- f"❌ Failed to process API heartbeat response for rewiring: {e}"
417
- )
418
- # Don't raise - this should not break the heartbeat loop
@@ -1,117 +0,0 @@
1
- """
2
- Fast Heartbeat Check Step for API heartbeat pipeline.
3
-
4
- Performs lightweight HEAD requests to registry for fast topology change detection
5
- before expensive full POST heartbeat operations for FastAPI services.
6
- """
7
-
8
- import logging
9
- from typing import Any
10
-
11
- from ...shared.fast_heartbeat_status import FastHeartbeatStatus, FastHeartbeatStatusUtil
12
- from ..shared.base_step import PipelineStep
13
- from ..shared.pipeline_types import PipelineResult
14
-
15
- logger = logging.getLogger(__name__)
16
-
17
-
18
- class APIFastHeartbeatStep(PipelineStep):
19
- """
20
- Fast heartbeat check step for API services optimization and resilience.
21
-
22
- Performs lightweight HEAD request to registry to check for topology changes
23
- before deciding whether to execute expensive full POST heartbeat for API services.
24
-
25
- Stores semantic status in context for pipeline conditional execution.
26
- """
27
-
28
- def __init__(self):
29
- super().__init__(
30
- name="api-fast-heartbeat-check",
31
- required=True,
32
- description="Lightweight HEAD request for fast topology change detection (API services)",
33
- )
34
-
35
- async def execute(self, context: dict[str, Any]) -> PipelineResult:
36
- """
37
- Execute fast heartbeat check for API service and store semantic status.
38
-
39
- Args:
40
- context: Pipeline context containing service_id/agent_id and registry_wrapper
41
-
42
- Returns:
43
- PipelineResult with fast_heartbeat_status in context
44
- """
45
- self.logger.trace("Starting API fast heartbeat check...")
46
-
47
- result = PipelineResult(message="API fast heartbeat check completed")
48
-
49
- try:
50
- # Validate required context - API services use service_id or agent_id
51
- service_id = context.get("service_id") or context.get("agent_id")
52
- registry_wrapper = context.get("registry_wrapper")
53
-
54
- if not service_id:
55
- raise ValueError("service_id or agent_id is required in context")
56
-
57
- if not registry_wrapper:
58
- raise ValueError("registry_wrapper is required in context")
59
-
60
- self.logger.trace(
61
- f"🚀 Performing API fast heartbeat check for service '{service_id}'"
62
- )
63
-
64
- # Perform fast heartbeat check using same method as MCP agents
65
- status = await registry_wrapper.check_fast_heartbeat(service_id)
66
-
67
- # Store semantic status in context
68
- result.add_context("fast_heartbeat_status", status)
69
-
70
- # Set appropriate message based on status
71
- action_description = FastHeartbeatStatusUtil.get_action_description(status)
72
- result.message = f"API fast heartbeat check: {action_description}"
73
-
74
- # Log status and action with API-specific messaging
75
- if status == FastHeartbeatStatus.NO_CHANGES:
76
- self.logger.info(
77
- f"✅ API fast heartbeat: No changes detected for service '{service_id}'"
78
- )
79
- elif status == FastHeartbeatStatus.TOPOLOGY_CHANGED:
80
- self.logger.info(
81
- f"🔄 API fast heartbeat: Topology changed for service '{service_id}' - full refresh needed"
82
- )
83
- elif status == FastHeartbeatStatus.AGENT_UNKNOWN:
84
- self.logger.info(
85
- f"❓ API fast heartbeat: Service '{service_id}' unknown - re-registration needed"
86
- )
87
- elif status == FastHeartbeatStatus.REGISTRY_ERROR:
88
- self.logger.warning(
89
- f"⚠️ API fast heartbeat: Registry error for service '{service_id}' - skipping for resilience"
90
- )
91
- elif status == FastHeartbeatStatus.NETWORK_ERROR:
92
- self.logger.warning(
93
- f"⚠️ API fast heartbeat: Network error for service '{service_id}' - skipping for resilience"
94
- )
95
-
96
- except Exception as e:
97
- # Convert any exception to NETWORK_ERROR for resilient handling
98
- status = FastHeartbeatStatusUtil.from_exception(e)
99
- result.add_context("fast_heartbeat_status", status)
100
-
101
- action_description = FastHeartbeatStatusUtil.get_action_description(status)
102
- result.message = f"API fast heartbeat check: {action_description}"
103
-
104
- self.logger.warning(
105
- f"⚠️ API fast heartbeat check failed for service '{service_id}': {e}"
106
- )
107
- self.logger.debug(f"Exception details: {e}", exc_info=True)
108
-
109
- # Step succeeds but sets error status for pipeline decision
110
- # This ensures pipeline can handle errors gracefully
111
-
112
- # Always preserve existing context
113
- for key, value in context.items():
114
- if key not in result.context:
115
- result.add_context(key, value)
116
-
117
- return result
@@ -1,140 +0,0 @@
1
- """
2
- API health check step for API heartbeat pipeline.
3
-
4
- Validates FastAPI application health status and endpoint availability
5
- for heartbeat reporting to the registry.
6
- """
7
-
8
- import logging
9
- from typing import Any
10
-
11
- from ..shared.base_step import PipelineStep
12
- from ..shared.pipeline_types import PipelineResult
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- class APIHealthCheckStep(PipelineStep):
18
- """
19
- Check FastAPI application health status.
20
-
21
- Validates that the FastAPI application is running properly
22
- and endpoints are accessible for dependency injection.
23
- """
24
-
25
- def __init__(self, required: bool = True):
26
- super().__init__(
27
- name="api-health-check",
28
- required=required,
29
- )
30
-
31
- async def execute(self, context: dict[str, Any]) -> PipelineResult:
32
- """
33
- Check FastAPI application health status.
34
-
35
- Args:
36
- context: Pipeline context containing fastapi_app and service info
37
-
38
- Returns:
39
- PipelineResult with health_status in context
40
- """
41
- self.logger.info("🏥 [DEBUG] Checking FastAPI application health status")
42
-
43
- try:
44
- # Get FastAPI app from context
45
- fastapi_app = context.get("fastapi_app")
46
- service_id = context.get("service_id") or context.get("agent_id", "unknown")
47
-
48
- if not fastapi_app:
49
- error_msg = "No FastAPI application found in context for health check"
50
- self.logger.error(f"❌ {error_msg}")
51
-
52
- from ..shared.pipeline_types import PipelineStatus
53
- result = PipelineResult(
54
- status=PipelineStatus.FAILED,
55
- message=error_msg,
56
- context=context
57
- )
58
- result.add_error(error_msg)
59
- return result
60
-
61
- # Check FastAPI app basic properties
62
- app_title = getattr(fastapi_app, "title", "Unknown API")
63
- app_version = getattr(fastapi_app, "version", "1.0.0")
64
-
65
- # Count available routes with dependency injection
66
- routes_with_mesh = self._count_mesh_routes(fastapi_app)
67
- total_routes = len(getattr(fastapi_app, "routes", []))
68
-
69
- self.logger.debug(
70
- f"🔍 FastAPI app health: {app_title} v{app_version}, "
71
- f"{routes_with_mesh}/{total_routes} routes with mesh injection"
72
- )
73
-
74
- # Build health status for API service
75
- # For API services, we create a simplified health status dict instead of using
76
- # the strict HealthStatus model which requires capabilities (designed for MCP agents)
77
- from datetime import UTC, datetime
78
-
79
- health_status_dict = {
80
- "agent_name": service_id,
81
- "status": "healthy",
82
- "timestamp": datetime.now(UTC).isoformat(),
83
- "version": app_version,
84
- "metadata": {
85
- "service_type": "api",
86
- "app_title": app_title,
87
- "app_version": app_version,
88
- "routes_total": total_routes,
89
- "routes_with_mesh": routes_with_mesh,
90
- "health_check_timestamp": datetime.now(UTC).isoformat(),
91
- }
92
- }
93
-
94
- self.logger.info(
95
- f"🏥 API health check passed: {app_title} v{app_version} "
96
- f"({routes_with_mesh} mesh routes)"
97
- )
98
-
99
- return PipelineResult(
100
- message=f"API health check passed for {app_title}",
101
- context={
102
- "health_status": health_status_dict,
103
- "app_title": app_title,
104
- "app_version": app_version,
105
- "routes_total": total_routes,
106
- "routes_with_mesh": routes_with_mesh,
107
- }
108
- )
109
-
110
- except Exception as e:
111
- error_msg = f"API health check failed: {e}"
112
- self.logger.error(f"❌ {error_msg}")
113
-
114
- from ..shared.pipeline_types import PipelineStatus
115
- result = PipelineResult(
116
- status=PipelineStatus.FAILED,
117
- message=error_msg,
118
- context=context
119
- )
120
- result.add_error(str(e))
121
- return result
122
-
123
- def _count_mesh_routes(self, fastapi_app: Any) -> int:
124
- """Count routes that have mesh dependency injection applied."""
125
- try:
126
- mesh_routes = 0
127
- routes = getattr(fastapi_app, "routes", [])
128
-
129
- for route in routes:
130
- # Check if route has dependency injection wrapper
131
- endpoint = getattr(route, "endpoint", None)
132
- if endpoint and hasattr(endpoint, "__wrapped__"):
133
- # This indicates our dependency injection wrapper
134
- mesh_routes += 1
135
-
136
- return mesh_routes
137
-
138
- except Exception as e:
139
- self.logger.warning(f"⚠️ Could not count mesh routes: {e}")
140
- return 0