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.
- _mcp_mesh/__init__.py +1 -1
- _mcp_mesh/engine/dependency_injector.py +13 -15
- _mcp_mesh/engine/http_wrapper.py +69 -10
- _mcp_mesh/engine/mesh_llm_agent.py +29 -10
- _mcp_mesh/engine/mesh_llm_agent_injector.py +77 -41
- _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/signature_analyzer.py +58 -68
- _mcp_mesh/engine/unified_mcp_proxy.py +19 -35
- _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 +429 -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 +710 -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 +31 -8
- _mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +6 -7
- _mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +23 -11
- _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 +5 -4
- {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/METADATA +7 -5
- mcp_mesh-0.8.0.dist-info/RECORD +85 -0
- mesh/__init__.py +12 -1
- mesh/decorators.py +248 -33
- mesh/helpers.py +52 -0
- mesh/types.py +40 -13
- _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 -247
- _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.20.dist-info/RECORD +0 -152
- {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/WHEEL +0 -0
- {mcp_mesh-0.7.20.dist-info → mcp_mesh-0.8.0.dist-info}/licenses/LICENSE +0 -0
_mcp_mesh/__init__.py
CHANGED
|
@@ -14,12 +14,10 @@ import weakref
|
|
|
14
14
|
from collections.abc import Callable
|
|
15
15
|
from typing import Any
|
|
16
16
|
|
|
17
|
-
from ..shared.logging_config import (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
)
|
|
22
|
-
from .signature_analyzer import get_mesh_agent_positions, has_llm_agent_parameter
|
|
17
|
+
from ..shared.logging_config import (format_log_value, format_result_summary,
|
|
18
|
+
get_trace_prefix)
|
|
19
|
+
from .signature_analyzer import (get_mesh_agent_positions,
|
|
20
|
+
has_llm_agent_parameter)
|
|
23
21
|
|
|
24
22
|
logger = logging.getLogger(__name__)
|
|
25
23
|
|
|
@@ -29,8 +27,8 @@ def analyze_injection_strategy(func: Callable, dependencies: list[str]) -> list[
|
|
|
29
27
|
Analyze function signature and determine injection strategy.
|
|
30
28
|
|
|
31
29
|
Rules:
|
|
32
|
-
1. Single parameter: inject regardless of typing (with warning if not
|
|
33
|
-
2. Multiple parameters: only inject into
|
|
30
|
+
1. Single parameter: inject regardless of typing (with warning if not McpMeshTool)
|
|
31
|
+
2. Multiple parameters: only inject into McpMeshTool typed parameters
|
|
34
32
|
3. Log warnings for mismatches and edge cases
|
|
35
33
|
|
|
36
34
|
Args:
|
|
@@ -62,17 +60,17 @@ def analyze_injection_strategy(func: Callable, dependencies: list[str]) -> list[
|
|
|
62
60
|
logger.warning(
|
|
63
61
|
f"Single parameter '{param_name}' in function '{func_name}' found, "
|
|
64
62
|
f"injecting {dependencies[0] if dependencies else 'dependency'} proxy "
|
|
65
|
-
f"(consider typing as
|
|
63
|
+
f"(consider typing as McpMeshTool for clarity)"
|
|
66
64
|
)
|
|
67
65
|
return [0] # Inject into the single parameter
|
|
68
66
|
|
|
69
|
-
# Multiple parameters rule: only inject into
|
|
67
|
+
# Multiple parameters rule: only inject into McpMeshTool typed parameters
|
|
70
68
|
if param_count > 1:
|
|
71
69
|
if not mesh_positions:
|
|
72
70
|
logger.warning(
|
|
73
71
|
f"⚠️ Function '{func_name}' has {param_count} parameters but none are "
|
|
74
|
-
f"typed as
|
|
75
|
-
f"Consider typing dependency parameters as
|
|
72
|
+
f"typed as McpMeshTool. Skipping injection of {len(dependencies)} dependencies. "
|
|
73
|
+
f"Consider typing dependency parameters as McpMeshTool."
|
|
76
74
|
)
|
|
77
75
|
return []
|
|
78
76
|
|
|
@@ -82,7 +80,7 @@ def analyze_injection_strategy(func: Callable, dependencies: list[str]) -> list[
|
|
|
82
80
|
excess_deps = dependencies[len(mesh_positions) :]
|
|
83
81
|
logger.warning(
|
|
84
82
|
f"Function '{func_name}' has {len(dependencies)} dependencies "
|
|
85
|
-
f"but only {len(mesh_positions)}
|
|
83
|
+
f"but only {len(mesh_positions)} McpMeshTool parameters. "
|
|
86
84
|
f"Dependencies {excess_deps} will not be injected."
|
|
87
85
|
)
|
|
88
86
|
else:
|
|
@@ -90,7 +88,7 @@ def analyze_injection_strategy(func: Callable, dependencies: list[str]) -> list[
|
|
|
90
88
|
params[pos].name for pos in mesh_positions[len(dependencies) :]
|
|
91
89
|
]
|
|
92
90
|
logger.warning(
|
|
93
|
-
f"Function '{func_name}' has {len(mesh_positions)}
|
|
91
|
+
f"Function '{func_name}' has {len(mesh_positions)} McpMeshTool parameters "
|
|
94
92
|
f"but only {len(dependencies)} dependencies declared. "
|
|
95
93
|
f"Parameters {excess_params} will remain None."
|
|
96
94
|
)
|
|
@@ -106,7 +104,7 @@ class DependencyInjector:
|
|
|
106
104
|
Manages dynamic dependency injection for mesh agents.
|
|
107
105
|
|
|
108
106
|
This class:
|
|
109
|
-
1. Maintains a registry of available dependencies (
|
|
107
|
+
1. Maintains a registry of available dependencies (McpMeshTool)
|
|
110
108
|
2. Coordinates with MeshLlmAgentInjector for LLM agent injection
|
|
111
109
|
3. Tracks which functions depend on which services
|
|
112
110
|
4. Updates function bindings when topology changes
|
_mcp_mesh/engine/http_wrapper.py
CHANGED
|
@@ -387,8 +387,13 @@ class HttpMcpWrapper:
|
|
|
387
387
|
self.logger = logger
|
|
388
388
|
|
|
389
389
|
async def dispatch(self, request: Request, call_next):
|
|
390
|
-
#
|
|
390
|
+
# Read body once for processing
|
|
391
|
+
body = await request.body()
|
|
392
|
+
modified_body = body
|
|
393
|
+
|
|
394
|
+
# Extract and set trace context from headers and arguments
|
|
391
395
|
try:
|
|
396
|
+
from ..tracing.context import TraceContext
|
|
392
397
|
from ..tracing.trace_context_helper import TraceContextHelper
|
|
393
398
|
|
|
394
399
|
# DEBUG: Log incoming headers for trace propagation debugging
|
|
@@ -399,22 +404,67 @@ class HttpMcpWrapper:
|
|
|
399
404
|
f"X-Parent-Span={parent_span_header}, path={request.url.path}"
|
|
400
405
|
)
|
|
401
406
|
|
|
402
|
-
#
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
407
|
+
# Extract trace context from both headers AND arguments
|
|
408
|
+
trace_id = trace_id_header
|
|
409
|
+
parent_span = parent_span_header
|
|
410
|
+
|
|
411
|
+
# Try extracting from JSON-RPC body arguments as fallback
|
|
412
|
+
# Also strip trace fields from arguments to avoid Pydantic validation errors
|
|
413
|
+
if body:
|
|
414
|
+
try:
|
|
415
|
+
payload = json.loads(body.decode("utf-8"))
|
|
416
|
+
if payload.get("method") == "tools/call":
|
|
417
|
+
arguments = payload.get("params", {}).get(
|
|
418
|
+
"arguments", {}
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# Extract trace context from arguments (TypeScript uses _trace_id/_parent_span)
|
|
422
|
+
if not trace_id and arguments.get("_trace_id"):
|
|
423
|
+
trace_id = arguments.get("_trace_id")
|
|
424
|
+
if not parent_span and arguments.get("_parent_span"):
|
|
425
|
+
parent_span = arguments.get("_parent_span")
|
|
426
|
+
|
|
427
|
+
# Strip trace context fields from arguments before passing to FastMCP
|
|
428
|
+
if (
|
|
429
|
+
"_trace_id" in arguments
|
|
430
|
+
or "_parent_span" in arguments
|
|
431
|
+
):
|
|
432
|
+
arguments.pop("_trace_id", None)
|
|
433
|
+
arguments.pop("_parent_span", None)
|
|
434
|
+
# Update payload with cleaned arguments
|
|
435
|
+
modified_body = json.dumps(payload).encode("utf-8")
|
|
436
|
+
self.logger.debug(
|
|
437
|
+
f"🔗 Stripped trace fields from arguments, "
|
|
438
|
+
f"trace_id={trace_id[:8] if trace_id else None}..."
|
|
439
|
+
)
|
|
440
|
+
except Exception as e:
|
|
441
|
+
self.logger.debug(
|
|
442
|
+
f"Failed to process body for trace context: {e}"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Setup trace context if we have a trace_id
|
|
446
|
+
if trace_id:
|
|
447
|
+
trace_context = {
|
|
448
|
+
"trace_id": trace_id,
|
|
449
|
+
"parent_span": parent_span,
|
|
450
|
+
}
|
|
451
|
+
TraceContextHelper.setup_request_trace_context(
|
|
452
|
+
trace_context, self.logger
|
|
406
453
|
)
|
|
407
|
-
)
|
|
408
|
-
TraceContextHelper.setup_request_trace_context(
|
|
409
|
-
trace_context, self.logger
|
|
410
|
-
)
|
|
411
454
|
except Exception as e:
|
|
412
455
|
# Never fail request due to tracing issues
|
|
413
456
|
self.logger.warning(f"Failed to set trace context: {e}")
|
|
414
457
|
pass
|
|
415
458
|
|
|
459
|
+
# Create a new request scope with the modified body
|
|
460
|
+
async def receive():
|
|
461
|
+
return {"type": "http.request", "body": modified_body}
|
|
462
|
+
|
|
463
|
+
# Update request with modified receive
|
|
464
|
+
request._receive = receive
|
|
465
|
+
|
|
416
466
|
# Extract session ID from request
|
|
417
|
-
session_id = await self.http_wrapper.
|
|
467
|
+
session_id = await self.http_wrapper._extract_session_id_from_body(body)
|
|
418
468
|
|
|
419
469
|
if session_id:
|
|
420
470
|
# Check for existing session assignment
|
|
@@ -458,6 +508,15 @@ class HttpMcpWrapper:
|
|
|
458
508
|
# Try extracting from JSON-RPC body
|
|
459
509
|
try:
|
|
460
510
|
body = await request.body()
|
|
511
|
+
return await self._extract_session_id_from_body(body)
|
|
512
|
+
except Exception:
|
|
513
|
+
pass
|
|
514
|
+
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
async def _extract_session_id_from_body(self, body: bytes) -> str:
|
|
518
|
+
"""Extract session ID from already-read request body."""
|
|
519
|
+
try:
|
|
461
520
|
if body:
|
|
462
521
|
payload = json.loads(body.decode("utf-8"))
|
|
463
522
|
if payload.get("method") == "tools/call":
|
|
@@ -636,12 +636,14 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
636
636
|
# Multi-turn conversation - use provided messages array
|
|
637
637
|
messages = message.copy()
|
|
638
638
|
|
|
639
|
-
#
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
639
|
+
# Only add/update system message if we have non-empty content
|
|
640
|
+
# (Claude API rejects empty system messages - though decorator provides default)
|
|
641
|
+
if system_content:
|
|
642
|
+
if not messages or messages[0].get("role") != "system":
|
|
643
|
+
messages.insert(0, {"role": "system", "content": system_content})
|
|
644
|
+
else:
|
|
645
|
+
# Replace existing system message with our constructed one
|
|
646
|
+
messages[0] = {"role": "system", "content": system_content}
|
|
645
647
|
|
|
646
648
|
# Log conversation history
|
|
647
649
|
logger.info(
|
|
@@ -649,10 +651,17 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
649
651
|
)
|
|
650
652
|
else:
|
|
651
653
|
# Single-turn - build messages array from string
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
654
|
+
# Only include system message if non-empty (Claude API rejects empty system messages)
|
|
655
|
+
if system_content:
|
|
656
|
+
messages = [
|
|
657
|
+
{"role": "system", "content": system_content},
|
|
658
|
+
{"role": "user", "content": message},
|
|
659
|
+
]
|
|
660
|
+
else:
|
|
661
|
+
# Fallback for edge case where system_content is explicitly empty
|
|
662
|
+
messages = [
|
|
663
|
+
{"role": "user", "content": message},
|
|
664
|
+
]
|
|
656
665
|
|
|
657
666
|
logger.info(f"🚀 Starting agentic loop for message: {message[:100]}...")
|
|
658
667
|
|
|
@@ -708,6 +717,16 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
708
717
|
if self.model:
|
|
709
718
|
model_params["model"] = self.model
|
|
710
719
|
|
|
720
|
+
# Issue #459: Include output_schema for provider to apply vendor-specific handling
|
|
721
|
+
# (e.g., OpenAI needs response_format, not prompt-based JSON instructions)
|
|
722
|
+
if self.output_type is not str and hasattr(
|
|
723
|
+
self.output_type, "model_json_schema"
|
|
724
|
+
):
|
|
725
|
+
model_params["output_schema"] = (
|
|
726
|
+
self.output_type.model_json_schema()
|
|
727
|
+
)
|
|
728
|
+
model_params["output_type_name"] = self.output_type.__name__
|
|
729
|
+
|
|
711
730
|
logger.debug(
|
|
712
731
|
f"📤 Delegating to mesh provider with handler-prepared params: "
|
|
713
732
|
f"keys={list(model_params.keys())}"
|
|
@@ -161,36 +161,70 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
161
161
|
# Create UnifiedMCPProxy for the provider
|
|
162
162
|
provider_proxy = self._create_provider_proxy(provider_data)
|
|
163
163
|
|
|
164
|
-
# Update
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
# Update only provider-related fields, preserving tool data if already set.
|
|
165
|
+
# This avoids race conditions where provider and tools updates can arrive in any order.
|
|
166
|
+
if function_id not in self._llm_agents:
|
|
167
|
+
self._llm_agents[function_id] = {}
|
|
167
168
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
self._llm_agents[function_id]["vendor"] = vendor
|
|
169
|
+
# Phase 2: Extract vendor from provider_data for handler selection
|
|
170
|
+
vendor = provider_data.get("vendor", "unknown")
|
|
171
171
|
|
|
172
|
+
self._llm_agents[function_id]["provider_proxy"] = provider_proxy
|
|
173
|
+
self._llm_agents[function_id]["vendor"] = vendor
|
|
174
|
+
|
|
175
|
+
logger.info(
|
|
176
|
+
f"✅ Set provider proxy for '{function_id}': {provider_proxy.function_name} at {provider_proxy.endpoint} (vendor={vendor})"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Re-create and update MeshLlmAgent with new provider
|
|
180
|
+
# Get the function wrapper and metadata from DecoratorRegistry
|
|
181
|
+
llm_agents = DecoratorRegistry.get_mesh_llm_agents()
|
|
182
|
+
wrapper = None
|
|
183
|
+
llm_metadata = None
|
|
184
|
+
for agent_func_id, metadata in llm_agents.items():
|
|
185
|
+
if metadata.function_id == function_id:
|
|
186
|
+
wrapper = metadata.function
|
|
187
|
+
llm_metadata = metadata
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
# Check if tools are required (filter is specified)
|
|
191
|
+
has_filter = False
|
|
192
|
+
if llm_metadata and llm_metadata.config:
|
|
193
|
+
filter_config = llm_metadata.config.get("filter")
|
|
194
|
+
has_filter = filter_config is not None and len(filter_config) > 0
|
|
195
|
+
|
|
196
|
+
# If no filter specified, initialize empty tools data so we can create LLM agent without tools
|
|
197
|
+
# This supports simple LLM calls (text generation) that don't need tool calling
|
|
198
|
+
if not has_filter and "tools_metadata" not in self._llm_agents[function_id]:
|
|
199
|
+
self._llm_agents[function_id].update(
|
|
200
|
+
{
|
|
201
|
+
"config": llm_metadata.config if llm_metadata else {},
|
|
202
|
+
"output_type": llm_metadata.output_type if llm_metadata else None,
|
|
203
|
+
"param_name": llm_metadata.param_name if llm_metadata else "llm",
|
|
204
|
+
"tools_metadata": [], # No tools for simple LLM calls
|
|
205
|
+
"tools_proxies": {}, # No tool proxies needed
|
|
206
|
+
"function": llm_metadata.function if llm_metadata else None,
|
|
207
|
+
}
|
|
208
|
+
)
|
|
172
209
|
logger.info(
|
|
173
|
-
f"✅
|
|
210
|
+
f"✅ Initialized empty tools for '{function_id}' (no filter specified - simple LLM mode)"
|
|
174
211
|
)
|
|
175
212
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
wrapper
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
else:
|
|
192
|
-
logger.warning(
|
|
193
|
-
f"⚠️ Function '{function_id}' not found in _llm_agents, cannot set provider proxy"
|
|
213
|
+
# Update wrapper if we have tools data (either from filter matching or initialized empty)
|
|
214
|
+
if (
|
|
215
|
+
wrapper
|
|
216
|
+
and hasattr(wrapper, "_mesh_update_llm_agent")
|
|
217
|
+
and "tools_metadata" in self._llm_agents[function_id]
|
|
218
|
+
):
|
|
219
|
+
llm_agent = self._create_llm_agent(function_id)
|
|
220
|
+
wrapper._mesh_update_llm_agent(llm_agent)
|
|
221
|
+
logger.info(
|
|
222
|
+
f"🔄 Updated wrapper with MeshLlmAgent for '{function_id}'"
|
|
223
|
+
+ (" (with tools)" if has_filter else " (simple LLM mode)")
|
|
224
|
+
)
|
|
225
|
+
elif wrapper and hasattr(wrapper, "_mesh_update_llm_agent") and has_filter:
|
|
226
|
+
logger.debug(
|
|
227
|
+
f"⏳ Provider set for '{function_id}', waiting for tools before updating wrapper"
|
|
194
228
|
)
|
|
195
229
|
|
|
196
230
|
def _create_provider_proxy(self, provider_data: dict[str, Any]) -> UnifiedMCPProxy:
|
|
@@ -273,21 +307,23 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
273
307
|
logger.error(f"❌ Error creating proxy for tool {tool_name}: {e}")
|
|
274
308
|
# Continue processing other tools
|
|
275
309
|
|
|
276
|
-
#
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
self._llm_agents[function_id]
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
310
|
+
# Update only tool-related fields, preserving provider_proxy if already set.
|
|
311
|
+
# Provider proxy is managed separately by process_llm_providers().
|
|
312
|
+
# This avoids race conditions where tools update wipes out provider resolution.
|
|
313
|
+
if function_id not in self._llm_agents:
|
|
314
|
+
self._llm_agents[function_id] = {}
|
|
315
|
+
|
|
316
|
+
self._llm_agents[function_id].update(
|
|
317
|
+
{
|
|
318
|
+
"config": llm_metadata.config,
|
|
319
|
+
"output_type": llm_metadata.output_type,
|
|
320
|
+
"param_name": llm_metadata.param_name,
|
|
321
|
+
"tools_metadata": tools, # Original metadata for schema building
|
|
322
|
+
"tools_proxies": tool_proxies_map, # Proxies for execution
|
|
323
|
+
"function": llm_metadata.function,
|
|
324
|
+
# Note: provider_proxy is NOT set here - managed by _process_function_provider
|
|
325
|
+
}
|
|
326
|
+
)
|
|
291
327
|
|
|
292
328
|
logger.info(
|
|
293
329
|
f"✅ Processed {len(tool_proxies_map)} tools for LLM function '{function_id}'"
|
|
@@ -380,7 +416,7 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
380
416
|
"""
|
|
381
417
|
Create wrapper that injects MeshLlmAgent into function parameters.
|
|
382
418
|
|
|
383
|
-
Like
|
|
419
|
+
Like McpMeshTool injection, this creates a wrapper at decorator time with llm_agent=None,
|
|
384
420
|
which gets updated during heartbeat when tools arrive from registry.
|
|
385
421
|
|
|
386
422
|
Args:
|
|
@@ -5,15 +5,28 @@ This package provides vendor-specific customization for different LLM providers
|
|
|
5
5
|
(Claude, OpenAI, Gemini, etc.) to optimize API calls and response handling.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .base_provider_handler import
|
|
8
|
+
from .base_provider_handler import (
|
|
9
|
+
BASE_TOOL_INSTRUCTIONS,
|
|
10
|
+
CLAUDE_ANTI_XML_INSTRUCTION,
|
|
11
|
+
BaseProviderHandler,
|
|
12
|
+
make_schema_strict,
|
|
13
|
+
)
|
|
9
14
|
from .claude_handler import ClaudeHandler
|
|
15
|
+
from .gemini_handler import GeminiHandler
|
|
10
16
|
from .generic_handler import GenericHandler
|
|
11
17
|
from .openai_handler import OpenAIHandler
|
|
12
18
|
from .provider_handler_registry import ProviderHandlerRegistry
|
|
13
19
|
|
|
14
20
|
__all__ = [
|
|
21
|
+
# Constants
|
|
22
|
+
"BASE_TOOL_INSTRUCTIONS",
|
|
23
|
+
"CLAUDE_ANTI_XML_INSTRUCTION",
|
|
24
|
+
# Utilities
|
|
25
|
+
"make_schema_strict",
|
|
26
|
+
# Handlers
|
|
15
27
|
"BaseProviderHandler",
|
|
16
28
|
"ClaudeHandler",
|
|
29
|
+
"GeminiHandler",
|
|
17
30
|
"OpenAIHandler",
|
|
18
31
|
"GenericHandler",
|
|
19
32
|
"ProviderHandlerRegistry",
|
|
@@ -5,11 +5,117 @@ This module defines the abstract base class for provider-specific handlers
|
|
|
5
5
|
that customize how different LLM vendors (Claude, OpenAI, Gemini, etc.) are called.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import copy
|
|
8
9
|
from abc import ABC, abstractmethod
|
|
9
|
-
from typing import Any,
|
|
10
|
+
from typing import Any, Optional
|
|
10
11
|
|
|
11
12
|
from pydantic import BaseModel
|
|
12
13
|
|
|
14
|
+
# ============================================================================
|
|
15
|
+
# Shared Constants
|
|
16
|
+
# ============================================================================
|
|
17
|
+
|
|
18
|
+
# Base tool calling instructions shared across all providers.
|
|
19
|
+
# Claude handler adds anti-XML instruction on top of this.
|
|
20
|
+
BASE_TOOL_INSTRUCTIONS = """
|
|
21
|
+
IMPORTANT TOOL CALLING RULES:
|
|
22
|
+
- You have access to tools that you can call to gather information
|
|
23
|
+
- Make ONE tool call at a time
|
|
24
|
+
- After receiving tool results, you can make additional calls if needed
|
|
25
|
+
- Once you have all needed information, provide your final response
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# Anti-XML instruction for Claude (prevents <invoke> style tool calls).
|
|
29
|
+
CLAUDE_ANTI_XML_INSTRUCTION = (
|
|
30
|
+
'- NEVER use XML-style syntax like <invoke name="tool_name"/>'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ============================================================================
|
|
35
|
+
# Shared Schema Utilities
|
|
36
|
+
# ============================================================================
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def make_schema_strict(
|
|
40
|
+
schema: dict[str, Any],
|
|
41
|
+
add_all_required: bool = True,
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
"""
|
|
44
|
+
Make a JSON schema strict for structured output.
|
|
45
|
+
|
|
46
|
+
This is a shared utility used by OpenAI, Gemini, and Claude handlers.
|
|
47
|
+
Adds additionalProperties: false to all object types and optionally
|
|
48
|
+
ensures 'required' includes all property keys.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
schema: JSON schema to make strict
|
|
52
|
+
add_all_required: If True, set 'required' to include ALL property keys.
|
|
53
|
+
OpenAI and Gemini require this; Claude does not.
|
|
54
|
+
Default: True
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
New schema with strict constraints (original not mutated)
|
|
58
|
+
"""
|
|
59
|
+
result = copy.deepcopy(schema)
|
|
60
|
+
_add_strict_constraints_recursive(result, add_all_required)
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _add_strict_constraints_recursive(obj: Any, add_all_required: bool) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Recursively add strict constraints to a schema object.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
obj: Schema object to process (mutated in place)
|
|
70
|
+
add_all_required: Whether to set required to all property keys
|
|
71
|
+
"""
|
|
72
|
+
if not isinstance(obj, dict):
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
# If this is an object type, add additionalProperties: false
|
|
76
|
+
if obj.get("type") == "object":
|
|
77
|
+
obj["additionalProperties"] = False
|
|
78
|
+
|
|
79
|
+
# Optionally set required to include all property keys
|
|
80
|
+
if add_all_required and "properties" in obj:
|
|
81
|
+
obj["required"] = list(obj["properties"].keys())
|
|
82
|
+
|
|
83
|
+
# Process $defs (Pydantic uses this for nested models)
|
|
84
|
+
if "$defs" in obj:
|
|
85
|
+
for def_schema in obj["$defs"].values():
|
|
86
|
+
_add_strict_constraints_recursive(def_schema, add_all_required)
|
|
87
|
+
|
|
88
|
+
# Process properties
|
|
89
|
+
if "properties" in obj:
|
|
90
|
+
for prop_schema in obj["properties"].values():
|
|
91
|
+
_add_strict_constraints_recursive(prop_schema, add_all_required)
|
|
92
|
+
|
|
93
|
+
# Process items (for arrays)
|
|
94
|
+
# items can be an object (single schema) or a list (tuple validation in older drafts)
|
|
95
|
+
if "items" in obj:
|
|
96
|
+
items = obj["items"]
|
|
97
|
+
if isinstance(items, dict):
|
|
98
|
+
_add_strict_constraints_recursive(items, add_all_required)
|
|
99
|
+
elif isinstance(items, list):
|
|
100
|
+
for item in items:
|
|
101
|
+
_add_strict_constraints_recursive(item, add_all_required)
|
|
102
|
+
|
|
103
|
+
# Process prefixItems (tuple validation in JSON Schema draft 2020-12)
|
|
104
|
+
if "prefixItems" in obj:
|
|
105
|
+
for item in obj["prefixItems"]:
|
|
106
|
+
_add_strict_constraints_recursive(item, add_all_required)
|
|
107
|
+
|
|
108
|
+
# Process anyOf, oneOf, allOf
|
|
109
|
+
for key in ("anyOf", "oneOf", "allOf"):
|
|
110
|
+
if key in obj:
|
|
111
|
+
for item in obj[key]:
|
|
112
|
+
_add_strict_constraints_recursive(item, add_all_required)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ============================================================================
|
|
116
|
+
# Base Provider Handler
|
|
117
|
+
# ============================================================================
|
|
118
|
+
|
|
13
119
|
|
|
14
120
|
class BaseProviderHandler(ABC):
|
|
15
121
|
"""
|
|
@@ -43,11 +149,11 @@ class BaseProviderHandler(ABC):
|
|
|
43
149
|
@abstractmethod
|
|
44
150
|
def prepare_request(
|
|
45
151
|
self,
|
|
46
|
-
messages:
|
|
47
|
-
tools: Optional[
|
|
152
|
+
messages: list[dict[str, Any]],
|
|
153
|
+
tools: Optional[list[dict[str, Any]]],
|
|
48
154
|
output_type: type[BaseModel],
|
|
49
|
-
**kwargs: Any
|
|
50
|
-
) ->
|
|
155
|
+
**kwargs: Any,
|
|
156
|
+
) -> dict[str, Any]:
|
|
51
157
|
"""
|
|
52
158
|
Prepare vendor-specific request parameters.
|
|
53
159
|
|
|
@@ -74,8 +180,8 @@ class BaseProviderHandler(ABC):
|
|
|
74
180
|
def format_system_prompt(
|
|
75
181
|
self,
|
|
76
182
|
base_prompt: str,
|
|
77
|
-
tool_schemas: Optional[
|
|
78
|
-
output_type: type[BaseModel]
|
|
183
|
+
tool_schemas: Optional[list[dict[str, Any]]],
|
|
184
|
+
output_type: type[BaseModel],
|
|
79
185
|
) -> str:
|
|
80
186
|
"""
|
|
81
187
|
Format system prompt for vendor-specific requirements.
|
|
@@ -95,7 +201,7 @@ class BaseProviderHandler(ABC):
|
|
|
95
201
|
"""
|
|
96
202
|
pass
|
|
97
203
|
|
|
98
|
-
def get_vendor_capabilities(self) ->
|
|
204
|
+
def get_vendor_capabilities(self) -> dict[str, bool]:
|
|
99
205
|
"""
|
|
100
206
|
Return vendor-specific capability flags.
|
|
101
207
|
|