mcp-mesh 0.6.2__py3-none-any.whl → 0.6.3__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/llm_config.py +10 -1
- _mcp_mesh/engine/mesh_llm_agent.py +51 -33
- _mcp_mesh/engine/mesh_llm_agent_injector.py +19 -0
- _mcp_mesh/engine/provider_handlers/claude_handler.py +322 -42
- _mcp_mesh/engine/provider_handlers/openai_handler.py +65 -9
- _mcp_mesh/engine/response_parser.py +54 -15
- {mcp_mesh-0.6.2.dist-info → mcp_mesh-0.6.3.dist-info}/METADATA +1 -1
- {mcp_mesh-0.6.2.dist-info → mcp_mesh-0.6.3.dist-info}/RECORD +12 -12
- {mcp_mesh-0.6.2.dist-info → mcp_mesh-0.6.3.dist-info}/WHEEL +1 -1
- mesh/decorators.py +39 -2
- {mcp_mesh-0.6.2.dist-info → mcp_mesh-0.6.3.dist-info}/licenses/LICENSE +0 -0
_mcp_mesh/__init__.py
CHANGED
_mcp_mesh/engine/llm_config.py
CHANGED
|
@@ -17,7 +17,7 @@ class LLMConfig:
|
|
|
17
17
|
Supports both direct LiteLLM providers (string) and mesh delegation (dict).
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
provider: Union[str,
|
|
20
|
+
provider: Union[str, dict[str, Any]] = "claude"
|
|
21
21
|
"""LLM provider - string for direct LiteLLM (e.g., 'claude', 'openai') or dict for mesh delegation
|
|
22
22
|
Mesh delegation format: {"capability": "llm", "tags": ["claude"], "version": ">=1.0.0"}"""
|
|
23
23
|
|
|
@@ -33,6 +33,9 @@ class LLMConfig:
|
|
|
33
33
|
system_prompt: Optional[str] = None
|
|
34
34
|
"""Optional system prompt to prepend to all interactions"""
|
|
35
35
|
|
|
36
|
+
output_mode: Optional[str] = None
|
|
37
|
+
"""Output mode override: 'strict', 'hint', or 'text'. If None, auto-detected by handler."""
|
|
38
|
+
|
|
36
39
|
def __post_init__(self):
|
|
37
40
|
"""Validate configuration after initialization."""
|
|
38
41
|
if self.max_iterations < 1:
|
|
@@ -43,3 +46,9 @@ class LLMConfig:
|
|
|
43
46
|
# Only validate model for string providers (not needed for mesh delegation)
|
|
44
47
|
if isinstance(self.provider, str) and not self.model:
|
|
45
48
|
raise ValueError("model cannot be empty when using string provider")
|
|
49
|
+
|
|
50
|
+
# Validate output_mode if provided
|
|
51
|
+
if self.output_mode and self.output_mode not in ("strict", "hint", "text"):
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"output_mode must be 'strict', 'hint', or 'text', got '{self.output_mode}'"
|
|
54
|
+
)
|
|
@@ -58,7 +58,7 @@ class MeshLlmAgent:
|
|
|
58
58
|
self,
|
|
59
59
|
config: LLMConfig,
|
|
60
60
|
filtered_tools: list[dict[str, Any]],
|
|
61
|
-
output_type: type[BaseModel],
|
|
61
|
+
output_type: type[BaseModel] | type[str],
|
|
62
62
|
tool_proxies: Optional[dict[str, Any]] = None,
|
|
63
63
|
template_path: Optional[str] = None,
|
|
64
64
|
context_value: Optional[Any] = None,
|
|
@@ -71,7 +71,7 @@ class MeshLlmAgent:
|
|
|
71
71
|
Args:
|
|
72
72
|
config: LLM configuration (provider, model, api_key, etc.)
|
|
73
73
|
filtered_tools: List of tool metadata from registry (for schema building)
|
|
74
|
-
output_type: Pydantic BaseModel for response validation
|
|
74
|
+
output_type: Pydantic BaseModel for response validation, or str for plain text
|
|
75
75
|
tool_proxies: Optional map of function_name -> proxy for tool execution
|
|
76
76
|
template_path: Optional path to Jinja2 template file for system prompt
|
|
77
77
|
context_value: Optional context for template rendering (MeshContextModel, dict, or None)
|
|
@@ -87,6 +87,7 @@ class MeshLlmAgent:
|
|
|
87
87
|
self.max_iterations = config.max_iterations
|
|
88
88
|
self.output_type = output_type
|
|
89
89
|
self.system_prompt = config.system_prompt # Public attribute for tests
|
|
90
|
+
self.output_mode = config.output_mode # Output mode override (strict/hint/text)
|
|
90
91
|
self._iteration_count = 0
|
|
91
92
|
|
|
92
93
|
# Detect if using mesh delegation (provider is dict)
|
|
@@ -126,12 +127,19 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
126
127
|
- Once you have gathered all necessary information, provide your final response
|
|
127
128
|
"""
|
|
128
129
|
|
|
129
|
-
schema
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
# Only generate JSON schema for Pydantic models, not for str return type
|
|
131
|
+
if self.output_type is not str and hasattr(
|
|
132
|
+
self.output_type, "model_json_schema"
|
|
133
|
+
):
|
|
134
|
+
schema = self.output_type.model_json_schema()
|
|
135
|
+
schema_str = json.dumps(schema, indent=2)
|
|
136
|
+
self._cached_json_instructions = (
|
|
137
|
+
f"\n\nIMPORTANT: You must return your final response as valid JSON matching this schema:\n"
|
|
138
|
+
f"{schema_str}\n\nReturn ONLY the JSON object, no additional text."
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
# str return type - no JSON schema needed
|
|
142
|
+
self._cached_json_instructions = ""
|
|
135
143
|
|
|
136
144
|
logger.debug(
|
|
137
145
|
f"🤖 MeshLlmAgent initialized: provider={config.provider}, model={config.model}, "
|
|
@@ -483,22 +491,36 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
483
491
|
try:
|
|
484
492
|
# Call LLM (either direct LiteLLM or mesh-delegated)
|
|
485
493
|
try:
|
|
486
|
-
if
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
494
|
+
# Build kwargs with output_mode override if set
|
|
495
|
+
call_kwargs = (
|
|
496
|
+
{**kwargs, "output_mode": self.output_mode}
|
|
497
|
+
if self.output_mode
|
|
498
|
+
else kwargs
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Use provider handler to prepare vendor-specific request
|
|
502
|
+
request_params = self._provider_handler.prepare_request(
|
|
503
|
+
messages=messages,
|
|
504
|
+
tools=self._tool_schemas if self._tool_schemas else None,
|
|
505
|
+
output_type=self.output_type,
|
|
506
|
+
**call_kwargs,
|
|
507
|
+
)
|
|
495
508
|
|
|
496
|
-
|
|
497
|
-
#
|
|
509
|
+
if self._is_mesh_delegated:
|
|
510
|
+
# Mesh delegation: extract model_params to send to provider
|
|
511
|
+
# Exclude messages/tools (separate params), model/api_key (provider has them),
|
|
512
|
+
# and output_mode (only used locally by prepare_request)
|
|
498
513
|
model_params = {
|
|
499
514
|
k: v
|
|
500
515
|
for k, v in request_params.items()
|
|
501
|
-
if k
|
|
516
|
+
if k
|
|
517
|
+
not in [
|
|
518
|
+
"messages",
|
|
519
|
+
"tools",
|
|
520
|
+
"model",
|
|
521
|
+
"api_key",
|
|
522
|
+
"output_mode",
|
|
523
|
+
]
|
|
502
524
|
}
|
|
503
525
|
|
|
504
526
|
logger.debug(
|
|
@@ -509,19 +531,10 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
509
531
|
response = await self._call_mesh_provider(
|
|
510
532
|
messages=messages,
|
|
511
533
|
tools=self._tool_schemas if self._tool_schemas else None,
|
|
512
|
-
**model_params,
|
|
534
|
+
**model_params,
|
|
513
535
|
)
|
|
514
536
|
else:
|
|
515
|
-
# Direct LiteLLM call
|
|
516
|
-
# Phase 2: Use provider handler to prepare vendor-specific request
|
|
517
|
-
request_params = self._provider_handler.prepare_request(
|
|
518
|
-
messages=messages,
|
|
519
|
-
tools=self._tool_schemas if self._tool_schemas else None,
|
|
520
|
-
output_type=self.output_type,
|
|
521
|
-
**kwargs,
|
|
522
|
-
)
|
|
523
|
-
|
|
524
|
-
# Add model and API key (common to all vendors)
|
|
537
|
+
# Direct LiteLLM call: add model and API key
|
|
525
538
|
request_params["model"] = self.model
|
|
526
539
|
request_params["api_key"] = self.api_key
|
|
527
540
|
|
|
@@ -612,15 +625,20 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
612
625
|
"""
|
|
613
626
|
Parse LLM response into output type.
|
|
614
627
|
|
|
615
|
-
|
|
628
|
+
For str return type, returns content directly without parsing.
|
|
629
|
+
For Pydantic models, delegates to ResponseParser.
|
|
616
630
|
|
|
617
631
|
Args:
|
|
618
632
|
content: Response content from LLM
|
|
619
633
|
|
|
620
634
|
Returns:
|
|
621
|
-
|
|
635
|
+
Raw string (if output_type is str) or parsed Pydantic model instance
|
|
622
636
|
|
|
623
637
|
Raises:
|
|
624
638
|
ResponseParseError: If response doesn't match output_type schema or invalid JSON
|
|
625
639
|
"""
|
|
640
|
+
# For str return type, return content directly without parsing
|
|
641
|
+
if self.output_type is str:
|
|
642
|
+
return content
|
|
643
|
+
|
|
626
644
|
return ResponseParser.parse(content, self.output_type)
|
|
@@ -307,6 +307,22 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
307
307
|
llm_agent = self._create_llm_agent(function_id)
|
|
308
308
|
wrapper._mesh_update_llm_agent(llm_agent)
|
|
309
309
|
logger.info(f"🔄 Updated wrapper with MeshLlmAgent for '{function_id}'")
|
|
310
|
+
|
|
311
|
+
# Set factory for per-call context agent creation (template support)
|
|
312
|
+
# This allows the decorator's wrapper to create new agents with context per-call
|
|
313
|
+
config_dict = llm_metadata.config
|
|
314
|
+
if config_dict.get("is_template", False):
|
|
315
|
+
# Capture function_id by value using default argument to avoid closure issues
|
|
316
|
+
def create_context_agent(
|
|
317
|
+
context_value: Any, _func_id: str = function_id
|
|
318
|
+
) -> MeshLlmAgent:
|
|
319
|
+
"""Factory to create MeshLlmAgent with context for template rendering."""
|
|
320
|
+
return self._create_llm_agent(_func_id, context_value=context_value)
|
|
321
|
+
|
|
322
|
+
wrapper._mesh_create_context_agent = create_context_agent
|
|
323
|
+
logger.info(
|
|
324
|
+
f"🎯 Set context agent factory for template-based function '{function_id}'"
|
|
325
|
+
)
|
|
310
326
|
elif wrapper:
|
|
311
327
|
logger.warning(
|
|
312
328
|
f"⚠️ Wrapper for '{function_id}' found but has no _mesh_update_llm_agent method"
|
|
@@ -512,6 +528,9 @@ class MeshLlmAgentInjector(BaseInjector):
|
|
|
512
528
|
api_key=config_dict.get("api_key", ""), # Will use ENV if empty
|
|
513
529
|
max_iterations=config_dict.get("max_iterations", 10),
|
|
514
530
|
system_prompt=config_dict.get("system_prompt"),
|
|
531
|
+
output_mode=config_dict.get(
|
|
532
|
+
"output_mode"
|
|
533
|
+
), # Pass through output_mode from decorator
|
|
515
534
|
)
|
|
516
535
|
|
|
517
536
|
# Phase 4: Template support - extract template metadata
|
|
@@ -3,15 +3,33 @@ Claude/Anthropic provider handler.
|
|
|
3
3
|
|
|
4
4
|
Optimized for Claude API (Claude 3.x, Sonnet, Opus, Haiku)
|
|
5
5
|
using Anthropic's best practices for tool calling and JSON responses.
|
|
6
|
+
|
|
7
|
+
Supports three output modes for performance/reliability tradeoffs:
|
|
8
|
+
- strict: Use response_format for guaranteed schema compliance (slowest, 100% reliable)
|
|
9
|
+
- hint: Use prompt-based JSON instructions (medium speed, ~95% reliable)
|
|
10
|
+
- text: Plain text output for str return types (fastest)
|
|
11
|
+
|
|
12
|
+
Features:
|
|
13
|
+
- Automatic prompt caching for system messages (up to 90% cost reduction)
|
|
14
|
+
- Anti-XML tool calling instructions
|
|
15
|
+
- Output mode optimization based on return type
|
|
6
16
|
"""
|
|
7
17
|
|
|
8
18
|
import json
|
|
9
|
-
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Any, Optional, get_args, get_origin
|
|
10
21
|
|
|
11
22
|
from pydantic import BaseModel
|
|
12
23
|
|
|
13
24
|
from .base_provider_handler import BaseProviderHandler
|
|
14
25
|
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# Output mode constants
|
|
29
|
+
OUTPUT_MODE_STRICT = "strict"
|
|
30
|
+
OUTPUT_MODE_HINT = "hint"
|
|
31
|
+
OUTPUT_MODE_TEXT = "text"
|
|
32
|
+
|
|
15
33
|
|
|
16
34
|
class ClaudeHandler(BaseProviderHandler):
|
|
17
35
|
"""
|
|
@@ -19,48 +37,260 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
19
37
|
|
|
20
38
|
Claude Characteristics:
|
|
21
39
|
- Excellent at following detailed instructions
|
|
22
|
-
-
|
|
23
|
-
- Handles JSON output well with schema instructions
|
|
40
|
+
- Native structured output via response_format (requires strict schema)
|
|
24
41
|
- Native tool calling (via Anthropic messages API)
|
|
25
42
|
- Performs best with anti-XML tool calling instructions
|
|
43
|
+
- Automatic prompt caching for cost optimization
|
|
44
|
+
|
|
45
|
+
Output Modes:
|
|
46
|
+
- strict: response_format with JSON schema (slowest, guaranteed valid JSON)
|
|
47
|
+
- hint: JSON schema in prompt (medium speed, usually valid JSON)
|
|
48
|
+
- text: Plain text output for str return types (fastest)
|
|
26
49
|
|
|
27
50
|
Best Practices (from Anthropic docs):
|
|
28
|
-
-
|
|
29
|
-
-
|
|
51
|
+
- Use response_format for guaranteed JSON schema compliance
|
|
52
|
+
- Schema must have additionalProperties: false on all objects
|
|
30
53
|
- Add anti-XML instructions to prevent <invoke> style tool calls
|
|
31
54
|
- Use one tool call at a time for better reliability
|
|
55
|
+
- Use cache_control for system prompts to reduce costs
|
|
32
56
|
"""
|
|
33
57
|
|
|
34
58
|
def __init__(self):
|
|
35
59
|
"""Initialize Claude handler."""
|
|
36
60
|
super().__init__(vendor="anthropic")
|
|
37
61
|
|
|
62
|
+
def _is_simple_schema(self, model_class: type[BaseModel]) -> bool:
|
|
63
|
+
"""
|
|
64
|
+
Check if a Pydantic model has a simple schema.
|
|
65
|
+
|
|
66
|
+
Simple schema criteria:
|
|
67
|
+
- Less than 5 fields
|
|
68
|
+
- All fields are basic types (str, int, float, bool, list, Optional)
|
|
69
|
+
- No nested Pydantic models
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
model_class: Pydantic model class
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True if schema is simple, False otherwise
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
schema = model_class.model_json_schema()
|
|
79
|
+
properties = schema.get("properties", {})
|
|
80
|
+
|
|
81
|
+
# Check field count
|
|
82
|
+
if len(properties) >= 5:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
# Check for nested objects or complex types
|
|
86
|
+
for field_name, field_schema in properties.items():
|
|
87
|
+
field_type = field_schema.get("type")
|
|
88
|
+
|
|
89
|
+
# Check for nested objects (indicates nested Pydantic model)
|
|
90
|
+
if field_type == "object" and "properties" in field_schema:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
# Check for $ref (nested model reference)
|
|
94
|
+
if "$ref" in field_schema:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
# Check array items for complex types
|
|
98
|
+
if field_type == "array":
|
|
99
|
+
items = field_schema.get("items", {})
|
|
100
|
+
if items.get("type") == "object" or "$ref" in items:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
return True
|
|
104
|
+
except Exception:
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
def determine_output_mode(
|
|
108
|
+
self, output_type: type, override_mode: Optional[str] = None
|
|
109
|
+
) -> str:
|
|
110
|
+
"""
|
|
111
|
+
Determine the output mode based on return type.
|
|
112
|
+
|
|
113
|
+
Logic:
|
|
114
|
+
- If override_mode specified, use it
|
|
115
|
+
- If return type is str, use "text" mode
|
|
116
|
+
- If return type is simple schema (<5 fields, basic types), use "hint" mode
|
|
117
|
+
- Otherwise, use "strict" mode
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
output_type: Return type (str or BaseModel subclass)
|
|
121
|
+
override_mode: Optional override ("strict", "hint", or "text")
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Output mode string
|
|
125
|
+
"""
|
|
126
|
+
# Allow explicit override
|
|
127
|
+
if override_mode:
|
|
128
|
+
return override_mode
|
|
129
|
+
|
|
130
|
+
# String return type -> text mode
|
|
131
|
+
if output_type is str:
|
|
132
|
+
return OUTPUT_MODE_TEXT
|
|
133
|
+
|
|
134
|
+
# Check if it's a Pydantic model
|
|
135
|
+
if isinstance(output_type, type) and issubclass(output_type, BaseModel):
|
|
136
|
+
if self._is_simple_schema(output_type):
|
|
137
|
+
return OUTPUT_MODE_HINT
|
|
138
|
+
else:
|
|
139
|
+
return OUTPUT_MODE_STRICT
|
|
140
|
+
|
|
141
|
+
# Default to strict for unknown types
|
|
142
|
+
return OUTPUT_MODE_STRICT
|
|
143
|
+
|
|
144
|
+
def _make_schema_strict(self, schema: dict[str, Any]) -> dict[str, Any]:
|
|
145
|
+
"""
|
|
146
|
+
Make a JSON schema strict for Claude's structured output.
|
|
147
|
+
|
|
148
|
+
Claude requires additionalProperties: false on all object types.
|
|
149
|
+
This recursively processes the schema to add this constraint.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
schema: JSON schema dict
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Schema with additionalProperties: false on all objects
|
|
156
|
+
"""
|
|
157
|
+
if not isinstance(schema, dict):
|
|
158
|
+
return schema
|
|
159
|
+
|
|
160
|
+
result = schema.copy()
|
|
161
|
+
|
|
162
|
+
# If this is an object type, add additionalProperties: false
|
|
163
|
+
if result.get("type") == "object":
|
|
164
|
+
result["additionalProperties"] = False
|
|
165
|
+
|
|
166
|
+
# Recursively process nested schemas
|
|
167
|
+
if "properties" in result:
|
|
168
|
+
result["properties"] = {
|
|
169
|
+
k: self._make_schema_strict(v) for k, v in result["properties"].items()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Process $defs (Pydantic uses this for nested models)
|
|
173
|
+
if "$defs" in result:
|
|
174
|
+
result["$defs"] = {
|
|
175
|
+
k: self._make_schema_strict(v) for k, v in result["$defs"].items()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# Process items for arrays
|
|
179
|
+
if "items" in result:
|
|
180
|
+
result["items"] = self._make_schema_strict(result["items"])
|
|
181
|
+
|
|
182
|
+
# Process anyOf, oneOf, allOf
|
|
183
|
+
for key in ["anyOf", "oneOf", "allOf"]:
|
|
184
|
+
if key in result:
|
|
185
|
+
result[key] = [self._make_schema_strict(s) for s in result[key]]
|
|
186
|
+
|
|
187
|
+
return result
|
|
188
|
+
|
|
189
|
+
def _apply_prompt_caching(
|
|
190
|
+
self, messages: list[dict[str, Any]]
|
|
191
|
+
) -> list[dict[str, Any]]:
|
|
192
|
+
"""
|
|
193
|
+
Apply prompt caching to system messages for Claude.
|
|
194
|
+
|
|
195
|
+
Claude's prompt caching feature caches the system prompt prefix,
|
|
196
|
+
reducing costs by up to 90% and improving latency for repeated calls.
|
|
197
|
+
|
|
198
|
+
The cache_control with type "ephemeral" tells Claude to cache
|
|
199
|
+
this content for the duration of the session (typically 5 minutes).
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
messages: List of message dicts
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Messages with cache_control applied to system messages
|
|
206
|
+
|
|
207
|
+
Reference:
|
|
208
|
+
https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
|
|
209
|
+
"""
|
|
210
|
+
cached_messages = []
|
|
211
|
+
|
|
212
|
+
for msg in messages:
|
|
213
|
+
if msg.get("role") == "system":
|
|
214
|
+
content = msg.get("content", "")
|
|
215
|
+
|
|
216
|
+
# Convert string content to cached content block format
|
|
217
|
+
if isinstance(content, str):
|
|
218
|
+
cached_msg = {
|
|
219
|
+
"role": "system",
|
|
220
|
+
"content": [
|
|
221
|
+
{
|
|
222
|
+
"type": "text",
|
|
223
|
+
"text": content,
|
|
224
|
+
"cache_control": {"type": "ephemeral"},
|
|
225
|
+
}
|
|
226
|
+
],
|
|
227
|
+
}
|
|
228
|
+
cached_messages.append(cached_msg)
|
|
229
|
+
logger.debug(
|
|
230
|
+
f"🗄️ Applied prompt caching to system message ({len(content)} chars)"
|
|
231
|
+
)
|
|
232
|
+
elif isinstance(content, list):
|
|
233
|
+
# Already in content block format - add cache_control to last block
|
|
234
|
+
cached_content = []
|
|
235
|
+
for i, block in enumerate(content):
|
|
236
|
+
if isinstance(block, dict):
|
|
237
|
+
block_copy = block.copy()
|
|
238
|
+
# Add cache_control to the last text block
|
|
239
|
+
if i == len(content) - 1 and block.get("type") == "text":
|
|
240
|
+
block_copy["cache_control"] = {"type": "ephemeral"}
|
|
241
|
+
cached_content.append(block_copy)
|
|
242
|
+
else:
|
|
243
|
+
cached_content.append(block)
|
|
244
|
+
cached_messages.append(
|
|
245
|
+
{"role": "system", "content": cached_content}
|
|
246
|
+
)
|
|
247
|
+
logger.debug("🗄️ Applied prompt caching to system content blocks")
|
|
248
|
+
else:
|
|
249
|
+
# Unknown format - pass through unchanged
|
|
250
|
+
cached_messages.append(msg)
|
|
251
|
+
else:
|
|
252
|
+
# Non-system messages pass through unchanged
|
|
253
|
+
cached_messages.append(msg)
|
|
254
|
+
|
|
255
|
+
return cached_messages
|
|
256
|
+
|
|
38
257
|
def prepare_request(
|
|
39
258
|
self,
|
|
40
|
-
messages:
|
|
41
|
-
tools: Optional[
|
|
42
|
-
output_type: type
|
|
43
|
-
**kwargs: Any
|
|
44
|
-
) ->
|
|
259
|
+
messages: list[dict[str, Any]],
|
|
260
|
+
tools: Optional[list[dict[str, Any]]],
|
|
261
|
+
output_type: type,
|
|
262
|
+
**kwargs: Any,
|
|
263
|
+
) -> dict[str, Any]:
|
|
45
264
|
"""
|
|
46
|
-
Prepare request parameters for Claude API.
|
|
265
|
+
Prepare request parameters for Claude API with output mode support.
|
|
47
266
|
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
- No
|
|
267
|
+
Output Mode Strategy:
|
|
268
|
+
- strict: Use response_format for guaranteed JSON schema compliance (slowest)
|
|
269
|
+
- hint: No response_format, rely on prompt instructions (medium speed)
|
|
270
|
+
- text: No response_format, plain text output (fastest)
|
|
52
271
|
|
|
53
272
|
Args:
|
|
54
273
|
messages: List of message dicts
|
|
55
274
|
tools: Optional list of tool schemas
|
|
56
|
-
output_type: Pydantic model
|
|
57
|
-
**kwargs: Additional model parameters
|
|
275
|
+
output_type: Return type (str or Pydantic model)
|
|
276
|
+
**kwargs: Additional model parameters (may include output_mode override)
|
|
58
277
|
|
|
59
278
|
Returns:
|
|
60
279
|
Dictionary of parameters for litellm.completion()
|
|
61
280
|
"""
|
|
281
|
+
# Extract output_mode from kwargs if provided
|
|
282
|
+
output_mode = kwargs.pop("output_mode", None)
|
|
283
|
+
determined_mode = self.determine_output_mode(output_type, output_mode)
|
|
284
|
+
|
|
285
|
+
# Remove response_format from kwargs - we control this based on output mode
|
|
286
|
+
# The decorator's response_format="json" is just a hint for parsing, not API param
|
|
287
|
+
kwargs.pop("response_format", None)
|
|
288
|
+
|
|
289
|
+
# Apply prompt caching to system messages for cost optimization
|
|
290
|
+
cached_messages = self._apply_prompt_caching(messages)
|
|
291
|
+
|
|
62
292
|
request_params = {
|
|
63
|
-
"messages":
|
|
293
|
+
"messages": cached_messages,
|
|
64
294
|
**kwargs, # Pass through temperature, max_tokens, etc.
|
|
65
295
|
}
|
|
66
296
|
|
|
@@ -69,32 +299,49 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
69
299
|
if tools:
|
|
70
300
|
request_params["tools"] = tools
|
|
71
301
|
|
|
302
|
+
# Only add response_format in "strict" mode
|
|
303
|
+
if determined_mode == OUTPUT_MODE_STRICT:
|
|
304
|
+
# Claude requires additionalProperties: false on all object types
|
|
305
|
+
if isinstance(output_type, type) and issubclass(output_type, BaseModel):
|
|
306
|
+
schema = output_type.model_json_schema()
|
|
307
|
+
strict_schema = self._make_schema_strict(schema)
|
|
308
|
+
request_params["response_format"] = {
|
|
309
|
+
"type": "json_schema",
|
|
310
|
+
"json_schema": {
|
|
311
|
+
"name": output_type.__name__,
|
|
312
|
+
"schema": strict_schema,
|
|
313
|
+
"strict": False, # Allow optional fields with defaults
|
|
314
|
+
},
|
|
315
|
+
}
|
|
316
|
+
|
|
72
317
|
return request_params
|
|
73
318
|
|
|
74
319
|
def format_system_prompt(
|
|
75
320
|
self,
|
|
76
321
|
base_prompt: str,
|
|
77
|
-
tool_schemas: Optional[
|
|
78
|
-
output_type: type
|
|
322
|
+
tool_schemas: Optional[list[dict[str, Any]]],
|
|
323
|
+
output_type: type,
|
|
324
|
+
output_mode: Optional[str] = None,
|
|
79
325
|
) -> str:
|
|
80
326
|
"""
|
|
81
|
-
Format system prompt for Claude with
|
|
327
|
+
Format system prompt for Claude with output mode support.
|
|
82
328
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
4. Claude performs best with verbose, explicit guidelines
|
|
329
|
+
Output Mode Strategy:
|
|
330
|
+
- strict: Minimal JSON instructions (response_format handles schema)
|
|
331
|
+
- hint: Add detailed JSON schema instructions in prompt
|
|
332
|
+
- text: No JSON instructions (plain text output)
|
|
88
333
|
|
|
89
334
|
Args:
|
|
90
335
|
base_prompt: Base system prompt
|
|
91
336
|
tool_schemas: Optional tool schemas
|
|
92
337
|
output_type: Expected response type
|
|
338
|
+
output_mode: Optional override for output mode
|
|
93
339
|
|
|
94
340
|
Returns:
|
|
95
341
|
Formatted system prompt optimized for Claude
|
|
96
342
|
"""
|
|
97
343
|
system_content = base_prompt
|
|
344
|
+
determined_mode = self.determine_output_mode(output_type, output_mode)
|
|
98
345
|
|
|
99
346
|
# Add tool calling instructions if tools available
|
|
100
347
|
# These prevent Claude from using XML-style <invoke> syntax
|
|
@@ -103,26 +350,58 @@ class ClaudeHandler(BaseProviderHandler):
|
|
|
103
350
|
|
|
104
351
|
IMPORTANT TOOL CALLING RULES:
|
|
105
352
|
- You have access to tools that you can call to gather information
|
|
106
|
-
- Make ONE tool call at a time
|
|
107
|
-
- NEVER combine multiple tools in a single tool_use block
|
|
353
|
+
- Make ONE tool call at a time
|
|
108
354
|
- NEVER use XML-style syntax like <invoke name="tool_name"/>
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
- Once you have gathered all necessary information, provide your final response
|
|
355
|
+
- After receiving tool results, you can make additional calls if needed
|
|
356
|
+
- Once you have all needed information, provide your final response
|
|
112
357
|
"""
|
|
113
358
|
|
|
114
|
-
# Add
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
359
|
+
# Add output format instructions based on mode
|
|
360
|
+
if determined_mode == OUTPUT_MODE_TEXT:
|
|
361
|
+
# Text mode: No JSON instructions
|
|
362
|
+
pass
|
|
363
|
+
|
|
364
|
+
elif determined_mode == OUTPUT_MODE_STRICT:
|
|
365
|
+
# Strict mode: Minimal instructions (response_format handles schema)
|
|
366
|
+
if isinstance(output_type, type) and issubclass(output_type, BaseModel):
|
|
367
|
+
system_content += f"\n\nYour final response will be structured as JSON matching the {output_type.__name__} format."
|
|
368
|
+
|
|
369
|
+
elif determined_mode == OUTPUT_MODE_HINT:
|
|
370
|
+
# Hint mode: Add detailed JSON schema instructions
|
|
371
|
+
if isinstance(output_type, type) and issubclass(output_type, BaseModel):
|
|
372
|
+
schema = output_type.model_json_schema()
|
|
373
|
+
properties = schema.get("properties", {})
|
|
374
|
+
required = schema.get("required", [])
|
|
375
|
+
|
|
376
|
+
# Build human-readable schema description
|
|
377
|
+
field_descriptions = []
|
|
378
|
+
for field_name, field_schema in properties.items():
|
|
379
|
+
field_type = field_schema.get("type", "any")
|
|
380
|
+
is_required = field_name in required
|
|
381
|
+
req_marker = " (required)" if is_required else " (optional)"
|
|
382
|
+
desc = field_schema.get("description", "")
|
|
383
|
+
desc_text = f" - {desc}" if desc else ""
|
|
384
|
+
field_descriptions.append(
|
|
385
|
+
f" - {field_name}: {field_type}{req_marker}{desc_text}"
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
fields_text = "\n".join(field_descriptions)
|
|
389
|
+
system_content += f"""
|
|
390
|
+
|
|
391
|
+
RESPONSE FORMAT:
|
|
392
|
+
You MUST respond with valid JSON matching this schema:
|
|
393
|
+
{{
|
|
394
|
+
{fields_text}
|
|
395
|
+
}}
|
|
396
|
+
|
|
397
|
+
Example format:
|
|
398
|
+
{json.dumps({k: f"<{v.get('type', 'value')}>" for k, v in properties.items()}, indent=2)}
|
|
399
|
+
|
|
400
|
+
IMPORTANT: Respond ONLY with valid JSON. No markdown code fences, no preamble text."""
|
|
122
401
|
|
|
123
402
|
return system_content
|
|
124
403
|
|
|
125
|
-
def get_vendor_capabilities(self) ->
|
|
404
|
+
def get_vendor_capabilities(self) -> dict[str, bool]:
|
|
126
405
|
"""
|
|
127
406
|
Return Claude-specific capabilities.
|
|
128
407
|
|
|
@@ -131,8 +410,9 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
131
410
|
"""
|
|
132
411
|
return {
|
|
133
412
|
"native_tool_calling": True, # Claude has native function calling
|
|
134
|
-
"structured_output":
|
|
413
|
+
"structured_output": True, # Native response_format support via LiteLLM
|
|
135
414
|
"streaming": True, # Supports streaming
|
|
136
415
|
"vision": True, # Claude 3+ supports vision
|
|
137
|
-
"json_mode":
|
|
416
|
+
"json_mode": True, # Native JSON mode via response_format
|
|
417
|
+
"prompt_caching": True, # Automatic system prompt caching for cost savings
|
|
138
418
|
}
|
|
@@ -46,11 +46,11 @@ class OpenAIHandler(BaseProviderHandler):
|
|
|
46
46
|
|
|
47
47
|
def prepare_request(
|
|
48
48
|
self,
|
|
49
|
-
messages:
|
|
50
|
-
tools: Optional[
|
|
49
|
+
messages: list[dict[str, Any]],
|
|
50
|
+
tools: Optional[list[dict[str, Any]]],
|
|
51
51
|
output_type: type[BaseModel],
|
|
52
|
-
**kwargs: Any
|
|
53
|
-
) ->
|
|
52
|
+
**kwargs: Any,
|
|
53
|
+
) -> dict[str, Any]:
|
|
54
54
|
"""
|
|
55
55
|
Prepare request parameters for OpenAI API with structured output.
|
|
56
56
|
|
|
@@ -83,6 +83,10 @@ class OpenAIHandler(BaseProviderHandler):
|
|
|
83
83
|
# rather than relying on prompt instructions alone
|
|
84
84
|
schema = output_type.model_json_schema()
|
|
85
85
|
|
|
86
|
+
# Transform schema for OpenAI strict mode
|
|
87
|
+
# OpenAI requires additionalProperties: false on all object schemas
|
|
88
|
+
schema = self._add_additional_properties_false(schema)
|
|
89
|
+
|
|
86
90
|
# OpenAI structured output format
|
|
87
91
|
# See: https://platform.openai.com/docs/guides/structured-outputs
|
|
88
92
|
request_params["response_format"] = {
|
|
@@ -90,8 +94,8 @@ class OpenAIHandler(BaseProviderHandler):
|
|
|
90
94
|
"json_schema": {
|
|
91
95
|
"name": output_type.__name__,
|
|
92
96
|
"schema": schema,
|
|
93
|
-
"strict":
|
|
94
|
-
}
|
|
97
|
+
"strict": True, # Enforce schema compliance
|
|
98
|
+
},
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
return request_params
|
|
@@ -99,8 +103,8 @@ class OpenAIHandler(BaseProviderHandler):
|
|
|
99
103
|
def format_system_prompt(
|
|
100
104
|
self,
|
|
101
105
|
base_prompt: str,
|
|
102
|
-
tool_schemas: Optional[
|
|
103
|
-
output_type: type[BaseModel]
|
|
106
|
+
tool_schemas: Optional[list[dict[str, Any]]],
|
|
107
|
+
output_type: type[BaseModel],
|
|
104
108
|
) -> str:
|
|
105
109
|
"""
|
|
106
110
|
Format system prompt for OpenAI (concise approach).
|
|
@@ -147,7 +151,7 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
147
151
|
|
|
148
152
|
return system_content
|
|
149
153
|
|
|
150
|
-
def get_vendor_capabilities(self) ->
|
|
154
|
+
def get_vendor_capabilities(self) -> dict[str, bool]:
|
|
151
155
|
"""
|
|
152
156
|
Return OpenAI-specific capabilities.
|
|
153
157
|
|
|
@@ -161,3 +165,55 @@ IMPORTANT TOOL CALLING RULES:
|
|
|
161
165
|
"vision": True, # GPT-4V and later support vision
|
|
162
166
|
"json_mode": True, # Has dedicated JSON mode via response_format
|
|
163
167
|
}
|
|
168
|
+
|
|
169
|
+
def _add_additional_properties_false(
|
|
170
|
+
self, schema: dict[str, Any]
|
|
171
|
+
) -> dict[str, Any]:
|
|
172
|
+
"""
|
|
173
|
+
Recursively add additionalProperties: false to all object schemas.
|
|
174
|
+
|
|
175
|
+
OpenAI strict mode requires this for all object schemas.
|
|
176
|
+
See: https://platform.openai.com/docs/guides/structured-outputs
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
schema: JSON schema from Pydantic model
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Modified schema with additionalProperties: false on all objects
|
|
183
|
+
"""
|
|
184
|
+
import copy
|
|
185
|
+
|
|
186
|
+
schema = copy.deepcopy(schema)
|
|
187
|
+
self._add_additional_properties_recursive(schema)
|
|
188
|
+
return schema
|
|
189
|
+
|
|
190
|
+
def _add_additional_properties_recursive(self, obj: Any) -> None:
|
|
191
|
+
"""Recursively process schema for OpenAI strict mode compliance."""
|
|
192
|
+
if isinstance(obj, dict):
|
|
193
|
+
# If this is an object type, add additionalProperties: false
|
|
194
|
+
# and ensure required includes all properties
|
|
195
|
+
if obj.get("type") == "object":
|
|
196
|
+
obj["additionalProperties"] = False
|
|
197
|
+
# OpenAI strict mode: required must include ALL property keys
|
|
198
|
+
if "properties" in obj:
|
|
199
|
+
obj["required"] = list(obj["properties"].keys())
|
|
200
|
+
|
|
201
|
+
# Process $defs (Pydantic uses this for nested models)
|
|
202
|
+
if "$defs" in obj:
|
|
203
|
+
for def_schema in obj["$defs"].values():
|
|
204
|
+
self._add_additional_properties_recursive(def_schema)
|
|
205
|
+
|
|
206
|
+
# Process properties
|
|
207
|
+
if "properties" in obj:
|
|
208
|
+
for prop_schema in obj["properties"].values():
|
|
209
|
+
self._add_additional_properties_recursive(prop_schema)
|
|
210
|
+
|
|
211
|
+
# Process items (for arrays)
|
|
212
|
+
if "items" in obj:
|
|
213
|
+
self._add_additional_properties_recursive(obj["items"])
|
|
214
|
+
|
|
215
|
+
# Process anyOf, oneOf, allOf
|
|
216
|
+
for key in ("anyOf", "oneOf", "allOf"):
|
|
217
|
+
if key in obj:
|
|
218
|
+
for item in obj[key]:
|
|
219
|
+
self._add_additional_properties_recursive(item)
|
|
@@ -8,7 +8,7 @@ Separated from MeshLlmAgent for better testability and reusability.
|
|
|
8
8
|
import json
|
|
9
9
|
import logging
|
|
10
10
|
import re
|
|
11
|
-
from typing import Any, TypeVar
|
|
11
|
+
from typing import Any, TypeVar, Union
|
|
12
12
|
|
|
13
13
|
from pydantic import BaseModel, ValidationError
|
|
14
14
|
|
|
@@ -40,12 +40,12 @@ class ResponseParser:
|
|
|
40
40
|
"""
|
|
41
41
|
|
|
42
42
|
@staticmethod
|
|
43
|
-
def parse(content:
|
|
43
|
+
def parse(content: Any, output_type: type[T]) -> T:
|
|
44
44
|
"""
|
|
45
45
|
Parse LLM response into Pydantic model.
|
|
46
46
|
|
|
47
47
|
Args:
|
|
48
|
-
content: Raw response content from LLM
|
|
48
|
+
content: Raw response content from LLM (string or pre-parsed dict/list)
|
|
49
49
|
output_type: Pydantic BaseModel class to parse into
|
|
50
50
|
|
|
51
51
|
Returns:
|
|
@@ -57,16 +57,26 @@ class ResponseParser:
|
|
|
57
57
|
logger.debug(f"📝 Parsing response into {output_type.__name__}...")
|
|
58
58
|
|
|
59
59
|
try:
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
# If content is already parsed (e.g., OpenAI strict mode), skip string processing
|
|
61
|
+
if isinstance(content, (dict, list)):
|
|
62
|
+
logger.debug("📦 Content already parsed, skipping string processing")
|
|
63
|
+
response_data = content
|
|
64
|
+
else:
|
|
65
|
+
# String processing for Claude, Gemini, and non-strict OpenAI
|
|
66
|
+
# Extract JSON from mixed content (narrative + XML + JSON)
|
|
67
|
+
extracted_content = ResponseParser._extract_json_from_mixed_content(
|
|
68
|
+
content
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Strip markdown code fences if present
|
|
72
|
+
cleaned_content = ResponseParser._strip_markdown_fences(
|
|
73
|
+
extracted_content
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Try to parse as JSON
|
|
77
|
+
response_data = ResponseParser._parse_json_with_fallback(
|
|
78
|
+
cleaned_content, output_type
|
|
79
|
+
)
|
|
70
80
|
|
|
71
81
|
# Validate against output type
|
|
72
82
|
return ResponseParser._validate_and_create(response_data, output_type)
|
|
@@ -175,12 +185,16 @@ class ResponseParser:
|
|
|
175
185
|
raise ResponseParseError(f"Invalid JSON response: {e}")
|
|
176
186
|
|
|
177
187
|
@staticmethod
|
|
178
|
-
def _validate_and_create(response_data:
|
|
188
|
+
def _validate_and_create(response_data: Any, output_type: type[T]) -> T:
|
|
179
189
|
"""
|
|
180
190
|
Validate data against Pydantic model and create instance.
|
|
181
191
|
|
|
192
|
+
Handles both dict and list responses:
|
|
193
|
+
- Dict: Direct unpacking into model
|
|
194
|
+
- List: Auto-wrap into first list field of model (for OpenAI strict mode)
|
|
195
|
+
|
|
182
196
|
Args:
|
|
183
|
-
response_data: Parsed JSON data
|
|
197
|
+
response_data: Parsed JSON data (dict or list)
|
|
184
198
|
output_type: Target Pydantic model
|
|
185
199
|
|
|
186
200
|
Returns:
|
|
@@ -190,6 +204,31 @@ class ResponseParser:
|
|
|
190
204
|
ResponseParseError: If validation fails
|
|
191
205
|
"""
|
|
192
206
|
try:
|
|
207
|
+
# Handle list responses - wrap into first list field of model
|
|
208
|
+
if isinstance(response_data, list):
|
|
209
|
+
# Find the first list field in the model
|
|
210
|
+
model_fields = output_type.model_fields
|
|
211
|
+
list_field_name = None
|
|
212
|
+
for field_name, field_info in model_fields.items():
|
|
213
|
+
# Check if field annotation is a list type
|
|
214
|
+
field_type = field_info.annotation
|
|
215
|
+
if (
|
|
216
|
+
hasattr(field_type, "__origin__")
|
|
217
|
+
and field_type.__origin__ is list
|
|
218
|
+
):
|
|
219
|
+
list_field_name = field_name
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
if list_field_name:
|
|
223
|
+
logger.debug(
|
|
224
|
+
f"📦 Wrapping list response into '{list_field_name}' field"
|
|
225
|
+
)
|
|
226
|
+
response_data = {list_field_name: response_data}
|
|
227
|
+
else:
|
|
228
|
+
raise ResponseParseError(
|
|
229
|
+
f"Response is a list but {output_type.__name__} has no list field to wrap into"
|
|
230
|
+
)
|
|
231
|
+
|
|
193
232
|
parsed = output_type(**response_data)
|
|
194
233
|
logger.debug(f"✅ Response parsed successfully: {parsed}")
|
|
195
234
|
return parsed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-mesh
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
_mcp_mesh/__init__.py,sha256=
|
|
1
|
+
_mcp_mesh/__init__.py,sha256=4Pk7c0F7B-b6m0mHql0kvKWn6HEFWJmd-mRFEkin6VY,2719
|
|
2
2
|
_mcp_mesh/engine/__init__.py,sha256=2ennzbo7yJcpkXO9BqN69TruLjJfmJY4Y5VEsG644K4,3630
|
|
3
3
|
_mcp_mesh/engine/async_mcp_client.py,sha256=UcbQjxtgVfeRw6DHTZhAzN1gkcKlTg-lUPEePRPQWAU,6306
|
|
4
4
|
_mcp_mesh/engine/base_injector.py,sha256=qzRLZqFP2VvEFagVovkpdldvDmm3VwPHm6tHwV58a2k,5648
|
|
@@ -6,12 +6,12 @@ _mcp_mesh/engine/decorator_registry.py,sha256=sb4Ng2y0hZovFzmrLdRCiwIEdwCm57WUD9
|
|
|
6
6
|
_mcp_mesh/engine/dependency_injector.py,sha256=1bjeJ7pHUPEF_IoTF-7_Wm1pDLHphtfcFfSrUPWrWI4,31230
|
|
7
7
|
_mcp_mesh/engine/full_mcp_proxy.py,sha256=PlRv7GSKqn5riOCqeCVulVdtq3z1Ug76mOkwMsOFHXw,25297
|
|
8
8
|
_mcp_mesh/engine/http_wrapper.py,sha256=T9VQ2LZbGgCzyOVXVwdqas7-W3Wid0EwRboFUpdYWtM,20718
|
|
9
|
-
_mcp_mesh/engine/llm_config.py,sha256=
|
|
9
|
+
_mcp_mesh/engine/llm_config.py,sha256=95bOsGWro5E1JGq7oZtEYhVdrzcIJqjht_r5vEdJVz4,2049
|
|
10
10
|
_mcp_mesh/engine/llm_errors.py,sha256=h7BiI14u-jL8vtvBfFbFDDrN7gIw8PQjXIl5AP1SBuA,3276
|
|
11
11
|
_mcp_mesh/engine/mcp_client_proxy.py,sha256=eJStwy_VQJexYYD8bOh_m4Ld3Bb8Ae_dt8N1CC41qBc,17625
|
|
12
|
-
_mcp_mesh/engine/mesh_llm_agent.py,sha256=
|
|
13
|
-
_mcp_mesh/engine/mesh_llm_agent_injector.py,sha256=
|
|
14
|
-
_mcp_mesh/engine/response_parser.py,sha256=
|
|
12
|
+
_mcp_mesh/engine/mesh_llm_agent.py,sha256=CcS2WX0ku1DSwUSx0H9YdV7oIiPNMs1jbxSPJvScRao,24679
|
|
13
|
+
_mcp_mesh/engine/mesh_llm_agent_injector.py,sha256=isufzCBExli8tdLUZOaPuea3uQs3C_yeVXbOVSF0YIU,27270
|
|
14
|
+
_mcp_mesh/engine/response_parser.py,sha256=NsOuGD7HJ0BFiiDUCp9v9cjLzVaU86HShVKzsrNnulk,8786
|
|
15
15
|
_mcp_mesh/engine/self_dependency_proxy.py,sha256=OkKt0-B_ADnJlWtHiHItoZCBZ7Su0iz2unEPFfXvrs4,3302
|
|
16
16
|
_mcp_mesh/engine/session_aware_client.py,sha256=mc9eh-aCvUvfllORiXTf_X8_jPqV-32QdWKlr8tHLkU,10600
|
|
17
17
|
_mcp_mesh/engine/session_manager.py,sha256=MCr0_fXBaUjXM51WU5EhDkiGvBdfzYQFVNb9DCXXL0A,10418
|
|
@@ -21,9 +21,9 @@ _mcp_mesh/engine/tool_schema_builder.py,sha256=SQCxQIrSfdLu9-dLqiFurQLK7dhl0dc0x
|
|
|
21
21
|
_mcp_mesh/engine/unified_mcp_proxy.py,sha256=SmhLWXdjmgvJWOLGQk-cXrvYjGSzx98HzL0Q5jpMNIY,36326
|
|
22
22
|
_mcp_mesh/engine/provider_handlers/__init__.py,sha256=LLTCOgnuM3dlogbLmrpiMK3oB5L22eAmDC4BfxJ-L2I,593
|
|
23
23
|
_mcp_mesh/engine/provider_handlers/base_provider_handler.py,sha256=J-SPFFFG1eFSUVvfsv7y4EuNM4REjSxaYWC5E_lC6Pc,4195
|
|
24
|
-
_mcp_mesh/engine/provider_handlers/claude_handler.py,sha256=
|
|
24
|
+
_mcp_mesh/engine/provider_handlers/claude_handler.py,sha256=CCmlsWiCfIcgrLbAZzeSnl0g2pq0uDffT8zOj4F-sPQ,15727
|
|
25
25
|
_mcp_mesh/engine/provider_handlers/generic_handler.py,sha256=ewcwxWMmNEFEeBJ_2m16Oc3SnhCKpc0PVDtKy7TsLv0,5153
|
|
26
|
-
_mcp_mesh/engine/provider_handlers/openai_handler.py,sha256=
|
|
26
|
+
_mcp_mesh/engine/provider_handlers/openai_handler.py,sha256=rpHvnOfZkk73uICgU4pKe-BsWts4cQeykm_UXkAA3Rk,7754
|
|
27
27
|
_mcp_mesh/engine/provider_handlers/provider_handler_registry.py,sha256=d2G3vndANzTiNl2ApfJuE2bmOlUI88y42144PjVst4s,5605
|
|
28
28
|
_mcp_mesh/generated/.openapi-generator-ignore,sha256=5opOTZ_fahF3ctMAmN-i3PzJXM0d9Tnji_uAET2ZyEw,162
|
|
29
29
|
_mcp_mesh/generated/.openapi-generator/FILES,sha256=Jpd-j6le0SjEvwdAJ51SWdZrlOUrUAFLtQ4sCHZVdKk,2571
|
|
@@ -140,10 +140,10 @@ _mcp_mesh/tracing/trace_context_helper.py,sha256=6tEkwjWFqMBe45zBlhacktmIpzJWTF9
|
|
|
140
140
|
_mcp_mesh/tracing/utils.py,sha256=t9lJuTH7CeuzAiiAaD0WxsJMFJPdzZFR0w6-vyR9f2E,3849
|
|
141
141
|
_mcp_mesh/utils/fastmcp_schema_extractor.py,sha256=M54ffesC-56zl_fNJHj9dZxElDQaWFf1MXdSLCuFStg,17253
|
|
142
142
|
mesh/__init__.py,sha256=0zequaBtd_9NLOLsr9sNONuwWa_fT_-G4LnJ1CHTEY0,3808
|
|
143
|
-
mesh/decorators.py,sha256=
|
|
143
|
+
mesh/decorators.py,sha256=QTd1wJ8XV0Pfjq2ejyXjyeXBtyodbj1gbs5WKG9AFTs,55632
|
|
144
144
|
mesh/helpers.py,sha256=c3FhSy9U4KBHEH6WH6MjCVrPMw9li5JAgBLUTIoamz4,9472
|
|
145
145
|
mesh/types.py,sha256=9TqbJSxlybLQaPVjugcKwPiIrVnJEzqAOvPRhlX1zmo,15559
|
|
146
|
-
mcp_mesh-0.6.
|
|
147
|
-
mcp_mesh-0.6.
|
|
148
|
-
mcp_mesh-0.6.
|
|
149
|
-
mcp_mesh-0.6.
|
|
146
|
+
mcp_mesh-0.6.3.dist-info/METADATA,sha256=6xLJu280gn5O2D1OUOZYAwOVGtQVf0g2tHDk3XnRXz8,4879
|
|
147
|
+
mcp_mesh-0.6.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
148
|
+
mcp_mesh-0.6.3.dist-info/licenses/LICENSE,sha256=_EBQHRQThv9FPOLc5eFOUdeeRO0mYwChC7cx60dM1tM,1078
|
|
149
|
+
mcp_mesh-0.6.3.dist-info/RECORD,,
|
mesh/decorators.py
CHANGED
|
@@ -989,6 +989,39 @@ def set_shutdown_context(context: dict[str, Any]):
|
|
|
989
989
|
set_global_shutdown_context(context)
|
|
990
990
|
|
|
991
991
|
|
|
992
|
+
def _get_llm_agent_for_injection(
|
|
993
|
+
wrapper: Any, param_name: str, kwargs: dict, func_name: str
|
|
994
|
+
) -> Any:
|
|
995
|
+
"""
|
|
996
|
+
Get the appropriate LLM agent for injection based on template mode.
|
|
997
|
+
|
|
998
|
+
Handles both template-based (per-call context) and non-template (cached) modes.
|
|
999
|
+
|
|
1000
|
+
Args:
|
|
1001
|
+
wrapper: The wrapper function with _mesh_llm_* attributes
|
|
1002
|
+
param_name: Name of the LLM parameter to inject
|
|
1003
|
+
kwargs: Current call kwargs (may contain context value)
|
|
1004
|
+
func_name: Function name for logging
|
|
1005
|
+
|
|
1006
|
+
Returns:
|
|
1007
|
+
MeshLlmAgent instance (either per-call with context or cached)
|
|
1008
|
+
"""
|
|
1009
|
+
config = getattr(wrapper, "_mesh_llm_config", {})
|
|
1010
|
+
is_template = config.get("is_template", False)
|
|
1011
|
+
context_param_name = config.get("context_param")
|
|
1012
|
+
create_context_agent = getattr(wrapper, "_mesh_create_context_agent", None)
|
|
1013
|
+
|
|
1014
|
+
if is_template and context_param_name and create_context_agent:
|
|
1015
|
+
# Template mode: create per-call agent with context
|
|
1016
|
+
context_value = kwargs.get(context_param_name)
|
|
1017
|
+
if context_value is not None:
|
|
1018
|
+
logger.debug(f"🎯 Created per-call LLM agent with context for {func_name}")
|
|
1019
|
+
return create_context_agent(context_value)
|
|
1020
|
+
|
|
1021
|
+
# Non-template mode or no context provided: use cached agent
|
|
1022
|
+
return wrapper._mesh_llm_agent
|
|
1023
|
+
|
|
1024
|
+
|
|
992
1025
|
def llm(
|
|
993
1026
|
filter: dict[str, Any] | list[dict[str, Any] | str] | str | None = None,
|
|
994
1027
|
*,
|
|
@@ -1247,7 +1280,9 @@ def llm(
|
|
|
1247
1280
|
"""Wrapper that injects both MeshLlmAgent and DI parameters."""
|
|
1248
1281
|
# Inject LLM parameter if not provided or if it's None
|
|
1249
1282
|
if param_name not in kwargs or kwargs.get(param_name) is None:
|
|
1250
|
-
kwargs[param_name] =
|
|
1283
|
+
kwargs[param_name] = _get_llm_agent_for_injection(
|
|
1284
|
+
combined_injection_wrapper, param_name, kwargs, func.__name__
|
|
1285
|
+
)
|
|
1251
1286
|
# Then call the original wrapper (which handles DI injection)
|
|
1252
1287
|
return original_call(*args, **kwargs)
|
|
1253
1288
|
|
|
@@ -1310,7 +1345,9 @@ def llm(
|
|
|
1310
1345
|
"""Wrapper that injects MeshLlmAgent parameter."""
|
|
1311
1346
|
# Inject llm parameter if not provided or if it's None
|
|
1312
1347
|
if param_name not in kwargs or kwargs.get(param_name) is None:
|
|
1313
|
-
kwargs[param_name] =
|
|
1348
|
+
kwargs[param_name] = _get_llm_agent_for_injection(
|
|
1349
|
+
llm_injection_wrapper, param_name, kwargs, func.__name__
|
|
1350
|
+
)
|
|
1314
1351
|
return func(*args, **kwargs)
|
|
1315
1352
|
|
|
1316
1353
|
# Create update method for heartbeat - updates the wrapper, not func
|
|
File without changes
|