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 CHANGED
@@ -31,7 +31,7 @@ from .engine.decorator_registry import (
31
31
  get_decorator_stats,
32
32
  )
33
33
 
34
- __version__ = "0.6.2"
34
+ __version__ = "0.6.3"
35
35
 
36
36
  # Store reference to runtime processor if initialized
37
37
  _runtime_processor = None
@@ -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, Dict[str, Any]] = "claude"
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 = self.output_type.model_json_schema()
130
- schema_str = json.dumps(schema, indent=2)
131
- self._cached_json_instructions = (
132
- f"\n\nIMPORTANT: You must return your final response as valid JSON matching this schema:\n"
133
- f"{schema_str}\n\nReturn ONLY the JSON object, no additional text."
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 self._is_mesh_delegated:
487
- # Mesh delegation: use provider handler to prepare vendor-specific request
488
- # Phase 2: Handler prepares params including response_format for OpenAI, etc.
489
- request_params = self._provider_handler.prepare_request(
490
- messages=messages,
491
- tools=self._tool_schemas if self._tool_schemas else None,
492
- output_type=self.output_type,
493
- **kwargs,
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
- # Extract model_params to send to provider
497
- # Don't send messages/tools (already separate params) or model/api_key (provider has them)
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 not in ["messages", "tools", "model", "api_key"]
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, # Now includes response_format!
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
- Delegates to ResponseParser for actual parsing logic.
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
- Parsed Pydantic model instance
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
- from typing import Any, Dict, List, Optional
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
- - Prefers verbose system prompts with explicit guidelines
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
- - Provide clear, detailed system prompts
29
- - Include explicit JSON schema in prompt for structured output
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: List[Dict[str, Any]],
41
- tools: Optional[List[Dict[str, Any]]],
42
- output_type: type[BaseModel],
43
- **kwargs: Any
44
- ) -> Dict[str, Any]:
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
- Claude uses standard LiteLLM interface with:
49
- - Standard messages format
50
- - OpenAI-compatible tools format (LiteLLM converts to Anthropic format)
51
- - No special response_format needed (uses prompt-based JSON instructions)
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 for response
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": 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[List[Dict[str, Any]]],
78
- output_type: type[BaseModel]
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 detailed instructions.
327
+ Format system prompt for Claude with output mode support.
82
328
 
83
- Claude Strategy:
84
- 1. Use base prompt as-is (detailed is better for Claude)
85
- 2. Add anti-XML tool calling instructions if tools present
86
- 3. Add explicit JSON schema instructions for final response
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 - each tool call must be separate
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
- - Each tool must be called using proper JSON tool_use format
110
- - After receiving results from a tool, you can make additional tool calls if needed
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 JSON schema instructions for final response
115
- # Claude needs explicit schema in prompt (no native response_format)
116
- schema = output_type.model_json_schema()
117
- schema_str = json.dumps(schema, indent=2)
118
- system_content += (
119
- f"\n\nIMPORTANT: You must return your final response as valid JSON matching this schema:\n"
120
- f"{schema_str}\n\nReturn ONLY the JSON object, no additional text."
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) -> Dict[str, bool]:
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": False, # No response_format, uses prompt-based JSON
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": False, # No dedicated JSON mode, uses prompting
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: List[Dict[str, Any]],
50
- tools: Optional[List[Dict[str, Any]]],
49
+ messages: list[dict[str, Any]],
50
+ tools: Optional[list[dict[str, Any]]],
51
51
  output_type: type[BaseModel],
52
- **kwargs: Any
53
- ) -> Dict[str, Any]:
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": False, # Allow optional fields with defaults
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[List[Dict[str, Any]]],
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) -> Dict[str, bool]:
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: str, output_type: type[T]) -> T:
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
- # Extract JSON from mixed content (narrative + XML + JSON)
61
- extracted_content = ResponseParser._extract_json_from_mixed_content(content)
62
-
63
- # Strip markdown code fences if present
64
- cleaned_content = ResponseParser._strip_markdown_fences(extracted_content)
65
-
66
- # Try to parse as JSON
67
- response_data = ResponseParser._parse_json_with_fallback(
68
- cleaned_content, output_type
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: dict[str, Any], output_type: type[T]) -> T:
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.2
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=0-xOI5iH1RtI9h1XclT7zPovVsrj3LuH8vbEnmbvhRU,2719
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=UOOfQbhYcljT9TsJJg_edwpk_5H4ZXQ6L1QARgiy8oc,1649
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=h83F7ZJWGv048tnXmNsD8HnmA0_t7D-aEWuCfke7psI,24038
13
- _mcp_mesh/engine/mesh_llm_agent_injector.py,sha256=nye3NYxJUP1ZS4IQZz-HSbCJKHIoaERMhjMW14pHuPg,26224
14
- _mcp_mesh/engine/response_parser.py,sha256=4_yEnxvHGvfgo7oqikgpjEjHuZRpBqBPJ_1QTH9YZOs,6943
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=woTDtrDgv0UlXgbDxnOxE92i7VWTDWCG5NRwZitm6Cs,4875
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=YKLnY1epdnoFDOm8LsuwGOIvkyJ4bSaEdArNxyHyaYQ,5511
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=iCneI4xgihw1cc22mx0K-OnMIOLbh47EtwgTvPGTRzk,54126
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.2.dist-info/METADATA,sha256=HfPNBY41hVS3eq2RauZzQTIrTL28gamvmoGAUZLBzU8,4879
147
- mcp_mesh-0.6.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
148
- mcp_mesh-0.6.2.dist-info/licenses/LICENSE,sha256=_EBQHRQThv9FPOLc5eFOUdeeRO0mYwChC7cx60dM1tM,1078
149
- mcp_mesh-0.6.2.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
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] = combined_injection_wrapper._mesh_llm_agent
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] = llm_injection_wrapper._mesh_llm_agent
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