mcp-mesh 0.8.0b8__tar.gz → 0.8.1__tar.gz
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-0.8.0b8 → mcp_mesh-0.8.1}/.gitignore +9 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/PKG-INFO +4 -2
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/__init__.py +1 -1
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/dependency_injector.py +9 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/mesh_llm_agent.py +36 -14
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/mesh_llm_agent_injector.py +232 -69
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_heartbeat/rust_api_heartbeat.py +5 -1
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_heartbeat/rust_heartbeat.py +12 -1
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/mesh/decorators.py +174 -92
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/mesh/helpers.py +52 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/pyproject.toml +7 -5
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/LICENSE +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/README.md +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/__init__.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/async_mcp_client.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/base_injector.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/decorator_registry.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/http_wrapper.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/llm_config.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/llm_errors.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/provider_handlers/__init__.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/provider_handlers/base_provider_handler.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/provider_handlers/claude_handler.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/provider_handlers/gemini_handler.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/provider_handlers/generic_handler.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/provider_handlers/openai_handler.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/provider_handlers/provider_handler_registry.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/response_parser.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/self_dependency_proxy.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/session_aware_client.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/session_manager.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/signature_analyzer.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/tool_executor.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/tool_schema_builder.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/engine/unified_mcp_proxy.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/__init__.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_heartbeat/__init__.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_heartbeat/api_lifespan_integration.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_startup/__init__.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_startup/api_pipeline.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_startup/api_server_setup.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_startup/fastapi_discovery.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_startup/middleware_integration.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_startup/route_collection.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/api_startup/route_integration.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_heartbeat/__init__.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/__init__.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/configuration.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/decorator_collection.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/fastapiserver_setup.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/heartbeat_loop.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/lifespan_factory.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/server_discovery.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/startup_orchestrator.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/shared/__init__.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/shared/base_step.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/shared/mesh_pipeline.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/pipeline/shared/pipeline_types.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/reload.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/reload_runner.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/shared/__init__.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/shared/config_resolver.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/shared/content_extractor.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/shared/defaults.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/shared/fast_heartbeat_status.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/shared/fastapi_middleware_manager.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/shared/health_check_manager.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/shared/host_resolver.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/shared/logging_config.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/shared/server_discovery.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/shared/simple_shutdown.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/shared/sse_parser.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/shared/support_types.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/tracing/agent_context_helper.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/tracing/context.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/tracing/execution_tracer.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/tracing/fastapi_tracing_middleware.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/tracing/redis_metadata_publisher.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/tracing/trace_context_helper.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/tracing/utils.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/_mcp_mesh/utils/fastmcp_schema_extractor.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/mesh/__init__.py +0 -0
- {mcp_mesh-0.8.0b8 → mcp_mesh-0.8.1}/mesh/types.py +0 -0
|
@@ -256,3 +256,12 @@ src/runtime/core/index.js
|
|
|
256
256
|
src/runtime/core/index.d.ts
|
|
257
257
|
test/
|
|
258
258
|
scaffold-test/
|
|
259
|
+
|
|
260
|
+
# Test suite build outputs
|
|
261
|
+
tests/src-tests/out/
|
|
262
|
+
tests/lib-tests/out/
|
|
263
|
+
|
|
264
|
+
# Allow test suite build artifacts
|
|
265
|
+
!tests/**/build/
|
|
266
|
+
.claude/
|
|
267
|
+
*.org
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-mesh
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: Kubernetes-native platform for distributed MCP applications
|
|
5
5
|
Project-URL: Homepage, https://github.com/dhyansraj/mcp-mesh
|
|
6
6
|
Project-URL: Documentation, https://github.com/dhyansraj/mcp-mesh/tree/main/docs
|
|
@@ -18,6 +18,8 @@ Classifier: Operating System :: OS Independent
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
23
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
24
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
25
|
Classifier: Topic :: System :: Distributed Computing
|
|
@@ -30,7 +32,7 @@ Requires-Dist: fastmcp<3.0.0,>=2.8.0
|
|
|
30
32
|
Requires-Dist: httpx<1.0.0,>=0.25.0
|
|
31
33
|
Requires-Dist: jinja2>=3.1.0
|
|
32
34
|
Requires-Dist: litellm>=1.30.0
|
|
33
|
-
Requires-Dist: mcp-mesh-core>=0.8.
|
|
35
|
+
Requires-Dist: mcp-mesh-core>=0.8.1
|
|
34
36
|
Requires-Dist: mcp<2.0.0,>=1.9.0
|
|
35
37
|
Requires-Dist: prometheus-client<1.0.0,>=0.19.0
|
|
36
38
|
Requires-Dist: pydantic<3.0.0,>=2.4.0
|
|
@@ -348,6 +348,15 @@ class DependencyInjector:
|
|
|
348
348
|
logger.debug(f"🤖 Creating LLM injection wrapper for {function_id}")
|
|
349
349
|
return self._llm_injector.create_injection_wrapper(func, function_id)
|
|
350
350
|
|
|
351
|
+
def initialize_direct_llm_agents(self) -> None:
|
|
352
|
+
"""
|
|
353
|
+
Initialize LLM agents that use direct LiteLLM (no mesh delegation).
|
|
354
|
+
|
|
355
|
+
This should be called during agent startup to initialize agents that
|
|
356
|
+
don't need to wait for registry response.
|
|
357
|
+
"""
|
|
358
|
+
self._llm_injector.initialize_direct_llm_agents()
|
|
359
|
+
|
|
351
360
|
def create_injection_wrapper(
|
|
352
361
|
self, func: Callable, dependencies: list[str]
|
|
353
362
|
) -> Callable:
|
|
@@ -14,8 +14,12 @@ from typing import Any, Dict, List, Literal, Optional, Union
|
|
|
14
14
|
from pydantic import BaseModel
|
|
15
15
|
|
|
16
16
|
from .llm_config import LLMConfig
|
|
17
|
-
from .llm_errors import (
|
|
18
|
-
|
|
17
|
+
from .llm_errors import (
|
|
18
|
+
LLMAPIError,
|
|
19
|
+
MaxIterationsError,
|
|
20
|
+
ResponseParseError,
|
|
21
|
+
ToolExecutionError,
|
|
22
|
+
)
|
|
19
23
|
from .provider_handlers import ProviderHandlerRegistry
|
|
20
24
|
from .response_parser import ResponseParser
|
|
21
25
|
from .tool_executor import ToolExecutor
|
|
@@ -23,8 +27,7 @@ from .tool_schema_builder import ToolSchemaBuilder
|
|
|
23
27
|
|
|
24
28
|
# Import Jinja2 for template rendering
|
|
25
29
|
try:
|
|
26
|
-
from jinja2 import
|
|
27
|
-
TemplateSyntaxError)
|
|
30
|
+
from jinja2 import Environment, FileSystemLoader, Template, TemplateSyntaxError
|
|
28
31
|
except ImportError:
|
|
29
32
|
Environment = None
|
|
30
33
|
FileSystemLoader = None
|
|
@@ -633,12 +636,14 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
633
636
|
# Multi-turn conversation - use provided messages array
|
|
634
637
|
messages = message.copy()
|
|
635
638
|
|
|
636
|
-
#
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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}
|
|
642
647
|
|
|
643
648
|
# Log conversation history
|
|
644
649
|
logger.info(
|
|
@@ -646,10 +651,17 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
646
651
|
)
|
|
647
652
|
else:
|
|
648
653
|
# Single-turn - build messages array from string
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
+
]
|
|
653
665
|
|
|
654
666
|
logger.info(f"🚀 Starting agentic loop for message: {message[:100]}...")
|
|
655
667
|
|
|
@@ -705,6 +717,16 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
705
717
|
if self.model:
|
|
706
718
|
model_params["model"] = self.model
|
|
707
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
|
+
|
|
708
730
|
logger.debug(
|
|
709
731
|
f"📤 Delegating to mesh provider with handler-prepared params: "
|
|
710
732
|
f"keys={list(model_params.keys())}"
|
|
@@ -65,6 +65,75 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
65
65
|
super().__init__()
|
|
66
66
|
self._llm_agents: dict[str, dict[str, Any]] = {}
|
|
67
67
|
|
|
68
|
+
def initialize_direct_llm_agents(self) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Initialize LLM agents that use direct LiteLLM (no mesh delegation).
|
|
71
|
+
|
|
72
|
+
This handles the case where:
|
|
73
|
+
- provider is a string (e.g., "claude") - direct LiteLLM call
|
|
74
|
+
- filter is None or empty - no mesh tools needed
|
|
75
|
+
|
|
76
|
+
These agents don't need to wait for registry response since all
|
|
77
|
+
information is available at decorator time.
|
|
78
|
+
"""
|
|
79
|
+
llm_agents = DecoratorRegistry.get_mesh_llm_agents()
|
|
80
|
+
|
|
81
|
+
for function_id, llm_metadata in llm_agents.items():
|
|
82
|
+
config = llm_metadata.config
|
|
83
|
+
provider = config.get("provider")
|
|
84
|
+
filter_config = config.get("filter")
|
|
85
|
+
|
|
86
|
+
# Check if this is a direct LiteLLM agent (provider is string, not dict)
|
|
87
|
+
is_direct_llm = isinstance(provider, str)
|
|
88
|
+
|
|
89
|
+
# Check if no tools needed (filter is None or empty)
|
|
90
|
+
has_no_filter = filter_config is None or (
|
|
91
|
+
isinstance(filter_config, list) and len(filter_config) == 0
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if is_direct_llm and has_no_filter:
|
|
95
|
+
# Skip if already initialized
|
|
96
|
+
if function_id in self._llm_agents:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
logger.info(
|
|
100
|
+
f"🔧 Initializing direct LiteLLM agent for '{function_id}' "
|
|
101
|
+
f"(provider={provider}, no filter)"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Initialize empty tools data for direct LiteLLM
|
|
105
|
+
self._llm_agents[function_id] = {
|
|
106
|
+
"config": config,
|
|
107
|
+
"output_type": llm_metadata.output_type,
|
|
108
|
+
"param_name": llm_metadata.param_name,
|
|
109
|
+
"tools_metadata": [], # No tools for direct LiteLLM
|
|
110
|
+
"tools_proxies": {}, # No tool proxies needed
|
|
111
|
+
"function": llm_metadata.function,
|
|
112
|
+
"provider_proxy": None, # No mesh delegation
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Get the wrapper and update it with LLM agent
|
|
116
|
+
wrapper = llm_metadata.function
|
|
117
|
+
if wrapper and hasattr(wrapper, "_mesh_update_llm_agent"):
|
|
118
|
+
llm_agent = self._create_llm_agent(function_id)
|
|
119
|
+
wrapper._mesh_update_llm_agent(llm_agent)
|
|
120
|
+
logger.info(
|
|
121
|
+
f"🔄 Updated wrapper with MeshLlmAgent for '{function_id}' (direct LiteLLM mode)"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Set factory for per-call context agent creation (template support)
|
|
125
|
+
if config.get("is_template", False):
|
|
126
|
+
def create_context_agent(
|
|
127
|
+
context_value: Any, _func_id: str = function_id
|
|
128
|
+
) -> MeshLlmAgent:
|
|
129
|
+
"""Factory to create MeshLlmAgent with context for template rendering."""
|
|
130
|
+
return self._create_llm_agent(_func_id, context_value=context_value)
|
|
131
|
+
|
|
132
|
+
wrapper._mesh_create_context_agent = create_context_agent
|
|
133
|
+
logger.info(
|
|
134
|
+
f"🎯 Set context agent factory for template-based function '{function_id}' (direct LiteLLM mode)"
|
|
135
|
+
)
|
|
136
|
+
|
|
68
137
|
def _build_function_name_to_id_mapping(self) -> dict[str, str]:
|
|
69
138
|
"""
|
|
70
139
|
Build mapping from function_name to function_id.
|
|
@@ -161,36 +230,86 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
161
230
|
# Create UnifiedMCPProxy for the provider
|
|
162
231
|
provider_proxy = self._create_provider_proxy(provider_data)
|
|
163
232
|
|
|
164
|
-
# Update
|
|
165
|
-
|
|
166
|
-
|
|
233
|
+
# Update only provider-related fields, preserving tool data if already set.
|
|
234
|
+
# This avoids race conditions where provider and tools updates can arrive in any order.
|
|
235
|
+
if function_id not in self._llm_agents:
|
|
236
|
+
self._llm_agents[function_id] = {}
|
|
237
|
+
|
|
238
|
+
# Phase 2: Extract vendor from provider_data for handler selection
|
|
239
|
+
vendor = provider_data.get("vendor", "unknown")
|
|
240
|
+
|
|
241
|
+
self._llm_agents[function_id]["provider_proxy"] = provider_proxy
|
|
242
|
+
self._llm_agents[function_id]["vendor"] = vendor
|
|
243
|
+
|
|
244
|
+
logger.info(
|
|
245
|
+
f"✅ Set provider proxy for '{function_id}': {provider_proxy.function_name} at {provider_proxy.endpoint} (vendor={vendor})"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Re-create and update MeshLlmAgent with new provider
|
|
249
|
+
# Get the function wrapper and metadata from DecoratorRegistry
|
|
250
|
+
llm_agents = DecoratorRegistry.get_mesh_llm_agents()
|
|
251
|
+
wrapper = None
|
|
252
|
+
llm_metadata = None
|
|
253
|
+
for agent_func_id, metadata in llm_agents.items():
|
|
254
|
+
if metadata.function_id == function_id:
|
|
255
|
+
wrapper = metadata.function
|
|
256
|
+
llm_metadata = metadata
|
|
257
|
+
break
|
|
167
258
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
259
|
+
# Check if tools are required (filter is specified)
|
|
260
|
+
has_filter = False
|
|
261
|
+
if llm_metadata and llm_metadata.config:
|
|
262
|
+
filter_config = llm_metadata.config.get("filter")
|
|
263
|
+
has_filter = filter_config is not None and len(filter_config) > 0
|
|
264
|
+
|
|
265
|
+
# If no filter specified, initialize empty tools data so we can create LLM agent without tools
|
|
266
|
+
# This supports simple LLM calls (text generation) that don't need tool calling
|
|
267
|
+
if not has_filter and "tools_metadata" not in self._llm_agents[function_id]:
|
|
268
|
+
self._llm_agents[function_id].update(
|
|
269
|
+
{
|
|
270
|
+
"config": llm_metadata.config if llm_metadata else {},
|
|
271
|
+
"output_type": llm_metadata.output_type if llm_metadata else None,
|
|
272
|
+
"param_name": llm_metadata.param_name if llm_metadata else "llm",
|
|
273
|
+
"tools_metadata": [], # No tools for simple LLM calls
|
|
274
|
+
"tools_proxies": {}, # No tool proxies needed
|
|
275
|
+
"function": llm_metadata.function if llm_metadata else None,
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
logger.info(
|
|
279
|
+
f"✅ Initialized empty tools for '{function_id}' (no filter specified - simple LLM mode)"
|
|
280
|
+
)
|
|
171
281
|
|
|
282
|
+
# Update wrapper if we have tools data (either from filter matching or initialized empty)
|
|
283
|
+
if (
|
|
284
|
+
wrapper
|
|
285
|
+
and hasattr(wrapper, "_mesh_update_llm_agent")
|
|
286
|
+
and "tools_metadata" in self._llm_agents[function_id]
|
|
287
|
+
):
|
|
288
|
+
llm_agent = self._create_llm_agent(function_id)
|
|
289
|
+
wrapper._mesh_update_llm_agent(llm_agent)
|
|
172
290
|
logger.info(
|
|
173
|
-
f"
|
|
291
|
+
f"🔄 Updated wrapper with MeshLlmAgent for '{function_id}'"
|
|
292
|
+
+ (" (with tools)" if has_filter else " (simple LLM mode)")
|
|
174
293
|
)
|
|
175
294
|
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
wrapper.
|
|
295
|
+
# Set factory for per-call context agent creation (template support)
|
|
296
|
+
# This is critical for filter=None cases where _process_function_tools isn't called
|
|
297
|
+
config_dict = llm_metadata.config if llm_metadata else {}
|
|
298
|
+
if config_dict.get("is_template", False):
|
|
299
|
+
# Capture function_id by value using default argument to avoid closure issues
|
|
300
|
+
def create_context_agent(
|
|
301
|
+
context_value: Any, _func_id: str = function_id
|
|
302
|
+
) -> MeshLlmAgent:
|
|
303
|
+
"""Factory to create MeshLlmAgent with context for template rendering."""
|
|
304
|
+
return self._create_llm_agent(_func_id, context_value=context_value)
|
|
305
|
+
|
|
306
|
+
wrapper._mesh_create_context_agent = create_context_agent
|
|
188
307
|
logger.info(
|
|
189
|
-
f"
|
|
308
|
+
f"🎯 Set context agent factory for template-based function '{function_id}' (simple LLM mode)"
|
|
190
309
|
)
|
|
191
|
-
|
|
192
|
-
logger.
|
|
193
|
-
f"
|
|
310
|
+
elif wrapper and hasattr(wrapper, "_mesh_update_llm_agent") and has_filter:
|
|
311
|
+
logger.debug(
|
|
312
|
+
f"⏳ Provider set for '{function_id}', waiting for tools before updating wrapper"
|
|
194
313
|
)
|
|
195
314
|
|
|
196
315
|
def _create_provider_proxy(self, provider_data: dict[str, Any]) -> UnifiedMCPProxy:
|
|
@@ -273,21 +392,23 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
273
392
|
logger.error(f"❌ Error creating proxy for tool {tool_name}: {e}")
|
|
274
393
|
# Continue processing other tools
|
|
275
394
|
|
|
276
|
-
#
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
self._llm_agents[function_id]
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
395
|
+
# Update only tool-related fields, preserving provider_proxy if already set.
|
|
396
|
+
# Provider proxy is managed separately by process_llm_providers().
|
|
397
|
+
# This avoids race conditions where tools update wipes out provider resolution.
|
|
398
|
+
if function_id not in self._llm_agents:
|
|
399
|
+
self._llm_agents[function_id] = {}
|
|
400
|
+
|
|
401
|
+
self._llm_agents[function_id].update(
|
|
402
|
+
{
|
|
403
|
+
"config": llm_metadata.config,
|
|
404
|
+
"output_type": llm_metadata.output_type,
|
|
405
|
+
"param_name": llm_metadata.param_name,
|
|
406
|
+
"tools_metadata": tools, # Original metadata for schema building
|
|
407
|
+
"tools_proxies": tool_proxies_map, # Proxies for execution
|
|
408
|
+
"function": llm_metadata.function,
|
|
409
|
+
# Note: provider_proxy is NOT set here - managed by _process_function_provider
|
|
410
|
+
}
|
|
411
|
+
)
|
|
291
412
|
|
|
292
413
|
logger.info(
|
|
293
414
|
f"✅ Processed {len(tool_proxies_map)} tools for LLM function '{function_id}'"
|
|
@@ -422,37 +543,60 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
422
543
|
def inject_llm_agent(func: Callable, args: tuple, kwargs: dict) -> tuple:
|
|
423
544
|
"""Inject LLM agent into kwargs if not provided."""
|
|
424
545
|
if param_name not in kwargs or kwargs.get(param_name) is None:
|
|
425
|
-
#
|
|
546
|
+
# Get config from runtime data or fallback to decorator registry.
|
|
547
|
+
# Runtime data (self._llm_agents) is populated during heartbeat and has
|
|
548
|
+
# tools/provider info. Decorator registry is populated at decorator time
|
|
549
|
+
# and always has config/context_param. For self-dependency calls that
|
|
550
|
+
# happen before heartbeat, we need the decorator registry fallback.
|
|
551
|
+
agent_data = None
|
|
552
|
+
config_dict = None
|
|
553
|
+
|
|
554
|
+
# Try runtime data first (has tools, provider from heartbeat)
|
|
426
555
|
if function_id in self._llm_agents:
|
|
427
556
|
agent_data = self._llm_agents[function_id]
|
|
428
|
-
config_dict = agent_data
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
context_info = get_context_parameter_name(
|
|
440
|
-
func, explicit_name=context_param_name
|
|
557
|
+
config_dict = agent_data.get("config")
|
|
558
|
+
|
|
559
|
+
# Fallback to decorator registry (always available, has context_param)
|
|
560
|
+
# This is critical for self-dependency calls that happen before heartbeat
|
|
561
|
+
if config_dict is None:
|
|
562
|
+
llm_agents_registry = DecoratorRegistry.get_mesh_llm_agents()
|
|
563
|
+
if function_id in llm_agents_registry:
|
|
564
|
+
llm_metadata = llm_agents_registry[function_id]
|
|
565
|
+
config_dict = llm_metadata.config
|
|
566
|
+
logger.debug(
|
|
567
|
+
f"🔄 Using DecoratorRegistry fallback for '{function_id}' config (self-dependency before heartbeat)"
|
|
441
568
|
)
|
|
442
569
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
570
|
+
# Check if templates are enabled
|
|
571
|
+
is_template = config_dict.get("is_template", False) if config_dict else False
|
|
572
|
+
|
|
573
|
+
if is_template and config_dict:
|
|
574
|
+
# Templates enabled - create per-call agent with context
|
|
575
|
+
# Import signature analyzer for context detection
|
|
576
|
+
from .signature_analyzer import get_context_parameter_name
|
|
447
577
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
context_value = args[ctx_index]
|
|
578
|
+
# Detect context parameter
|
|
579
|
+
context_param_name = config_dict.get("context_param")
|
|
580
|
+
context_info = get_context_parameter_name(
|
|
581
|
+
func, explicit_name=context_param_name
|
|
582
|
+
)
|
|
454
583
|
|
|
455
|
-
|
|
584
|
+
# Extract context value from call
|
|
585
|
+
context_value = None
|
|
586
|
+
if context_info is not None:
|
|
587
|
+
ctx_name, ctx_index = context_info
|
|
588
|
+
|
|
589
|
+
# Try kwargs first
|
|
590
|
+
if ctx_name in kwargs:
|
|
591
|
+
context_value = kwargs[ctx_name]
|
|
592
|
+
# Then try positional args
|
|
593
|
+
elif ctx_index < len(args):
|
|
594
|
+
context_value = args[ctx_index]
|
|
595
|
+
|
|
596
|
+
# Create agent with context for this call
|
|
597
|
+
# Note: _create_llm_agent requires function_id in self._llm_agents
|
|
598
|
+
# If not available yet, use cached agent with context_value set directly
|
|
599
|
+
if function_id in self._llm_agents:
|
|
456
600
|
current_agent = self._create_llm_agent(
|
|
457
601
|
function_id, context_value=context_value
|
|
458
602
|
)
|
|
@@ -460,22 +604,41 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
460
604
|
f"🤖 Created MeshLlmAgent with context for {func.__name__}.{param_name}"
|
|
461
605
|
)
|
|
462
606
|
else:
|
|
463
|
-
#
|
|
607
|
+
# Runtime data not yet available - use cached agent but log warning
|
|
608
|
+
# The cached agent may have been created without context
|
|
464
609
|
current_agent = wrapper._mesh_llm_agent
|
|
465
610
|
if current_agent is not None:
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
611
|
+
# Update context on the cached agent if possible
|
|
612
|
+
if hasattr(current_agent, "_context_value"):
|
|
613
|
+
current_agent._context_value = context_value
|
|
614
|
+
logger.debug(
|
|
615
|
+
f"🤖 Updated context on cached MeshLlmAgent for {func.__name__}.{param_name}"
|
|
616
|
+
)
|
|
617
|
+
else:
|
|
618
|
+
logger.debug(
|
|
619
|
+
f"🤖 Injected cached MeshLlmAgent into {func.__name__}.{param_name} (context may not be applied)"
|
|
620
|
+
)
|
|
469
621
|
else:
|
|
470
622
|
logger.warning(
|
|
471
623
|
f"⚠️ MeshLlmAgent for {func.__name__}.{param_name} is None (tools not yet received from registry)"
|
|
472
624
|
)
|
|
625
|
+
elif config_dict:
|
|
626
|
+
# No template - use cached agent (existing behavior)
|
|
627
|
+
current_agent = wrapper._mesh_llm_agent
|
|
628
|
+
if current_agent is not None:
|
|
629
|
+
logger.debug(
|
|
630
|
+
f"🤖 Injected MeshLlmAgent into {func.__name__}.{param_name}"
|
|
631
|
+
)
|
|
632
|
+
else:
|
|
633
|
+
logger.warning(
|
|
634
|
+
f"⚠️ MeshLlmAgent for {func.__name__}.{param_name} is None (tools not yet received from registry)"
|
|
635
|
+
)
|
|
473
636
|
else:
|
|
474
|
-
# No
|
|
637
|
+
# No config found anywhere - use cached (backward compatibility)
|
|
475
638
|
current_agent = wrapper._mesh_llm_agent
|
|
476
639
|
if current_agent is None:
|
|
477
640
|
logger.warning(
|
|
478
|
-
f"⚠️ MeshLlmAgent for {func.__name__}.{param_name} is None (
|
|
641
|
+
f"⚠️ MeshLlmAgent for {func.__name__}.{param_name} is None (no config found)"
|
|
479
642
|
)
|
|
480
643
|
|
|
481
644
|
kwargs[param_name] = current_agent
|
|
@@ -104,9 +104,10 @@ def _build_api_agent_spec(context: dict[str, Any], service_id: str = None) -> An
|
|
|
104
104
|
# Build dependency specs
|
|
105
105
|
deps = []
|
|
106
106
|
for dep_cap in dependencies:
|
|
107
|
+
# Tags must be serialized to JSON string (Rust core expects string, not list)
|
|
107
108
|
dep_spec = core.DependencySpec(
|
|
108
109
|
capability=dep_cap,
|
|
109
|
-
tags=[],
|
|
110
|
+
tags=json.dumps([]),
|
|
110
111
|
version=None,
|
|
111
112
|
)
|
|
112
113
|
deps.append(dep_spec)
|
|
@@ -136,6 +137,7 @@ def _build_api_agent_spec(context: dict[str, Any], service_id: str = None) -> An
|
|
|
136
137
|
http_port=http_port,
|
|
137
138
|
http_host=http_host,
|
|
138
139
|
namespace=namespace,
|
|
140
|
+
agent_type="api", # API services only consume capabilities, not provide them
|
|
139
141
|
tools=tools if tools else None,
|
|
140
142
|
llm_agents=None, # API services don't have LLM agents
|
|
141
143
|
heartbeat_interval=heartbeat_interval,
|
|
@@ -272,6 +274,8 @@ async def _handle_api_dependency_change(
|
|
|
272
274
|
)
|
|
273
275
|
if not current_service_id:
|
|
274
276
|
# Use config resolver for consistent env var handling
|
|
277
|
+
from ...shared.config_resolver import get_config_value
|
|
278
|
+
|
|
275
279
|
current_service_id = get_config_value("MCP_MESH_AGENT_ID")
|
|
276
280
|
|
|
277
281
|
is_self_dependency = (
|
|
@@ -117,9 +117,12 @@ def _build_agent_spec(context: dict[str, Any]) -> Any:
|
|
|
117
117
|
# Build dependency specs
|
|
118
118
|
deps = []
|
|
119
119
|
for dep_info in tool_metadata.get("dependencies", []):
|
|
120
|
+
# Serialize tags to JSON to support nested arrays for OR alternatives
|
|
121
|
+
# e.g., ["addition", ["python", "typescript"]] -> addition AND (python OR typescript)
|
|
122
|
+
tags_json = json.dumps(dep_info.get("tags", []))
|
|
120
123
|
dep_spec = core.DependencySpec(
|
|
121
124
|
capability=dep_info.get("capability", ""),
|
|
122
|
-
tags=
|
|
125
|
+
tags=tags_json,
|
|
123
126
|
version=dep_info.get("version"),
|
|
124
127
|
)
|
|
125
128
|
deps.append(dep_spec)
|
|
@@ -269,6 +272,14 @@ async def _handle_mesh_event(event: Any, context: dict[str, Any]) -> None:
|
|
|
269
272
|
if event_type == "agent_registered":
|
|
270
273
|
logger.info(f"Agent registered with ID: {event.agent_id}")
|
|
271
274
|
|
|
275
|
+
# Initialize direct LiteLLM agents that don't need mesh delegation
|
|
276
|
+
# These agents have provider="string" and filter=None, so all info is
|
|
277
|
+
# available at decorator time - no need to wait for registry response
|
|
278
|
+
from ...engine.dependency_injector import get_global_injector
|
|
279
|
+
|
|
280
|
+
injector = get_global_injector()
|
|
281
|
+
injector.initialize_direct_llm_agents()
|
|
282
|
+
|
|
272
283
|
elif event_type == "registration_failed":
|
|
273
284
|
logger.error(f"Agent registration failed: {event.error}")
|
|
274
285
|
|