mcp-mesh 0.6.1__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.1"
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)
@@ -19,6 +19,35 @@ from .unified_mcp_proxy import UnifiedMCPProxy
19
19
  logger = logging.getLogger(__name__)
20
20
 
21
21
 
22
+ def extract_vendor_from_model(model: str) -> str | None:
23
+ """
24
+ Extract vendor name from LiteLLM model string.
25
+
26
+ LiteLLM uses vendor/model format (e.g., "anthropic/claude-sonnet-4-5").
27
+ This extracts the vendor for provider handler selection.
28
+
29
+ Args:
30
+ model: LiteLLM model string
31
+
32
+ Returns:
33
+ Vendor name (e.g., "anthropic", "openai") or None if not extractable
34
+
35
+ Examples:
36
+ "anthropic/claude-sonnet-4-5" -> "anthropic"
37
+ "openai/gpt-4o" -> "openai"
38
+ "gpt-4" -> None (no vendor prefix)
39
+ """
40
+ if not model:
41
+ return None
42
+
43
+ if "/" in model:
44
+ vendor = model.split("/")[0].lower().strip()
45
+ logger.debug(f"🔍 Extracted vendor '{vendor}' from model '{model}'")
46
+ return vendor
47
+
48
+ return None
49
+
50
+
22
51
  class MeshLlmAgentInjector(BaseInjector):
23
52
  """
24
53
  Manages dynamic injection of MeshLlmAgent proxies.
@@ -86,9 +115,7 @@ class MeshLlmAgentInjector(BaseInjector):
86
115
  exc_info=True,
87
116
  )
88
117
 
89
- def process_llm_providers(
90
- self, llm_providers: dict[str, dict[str, Any]]
91
- ) -> None:
118
+ def process_llm_providers(self, llm_providers: dict[str, dict[str, Any]]) -> None:
92
119
  """
93
120
  Process llm_providers from registry response (v0.6.1 mesh delegation).
94
121
 
@@ -181,9 +208,7 @@ class MeshLlmAgentInjector(BaseInjector):
181
208
  """
182
209
  function_name = provider_data.get("name")
183
210
  if not function_name:
184
- raise ValueError(
185
- f"Provider missing required 'name' field: {provider_data}"
186
- )
211
+ raise ValueError(f"Provider missing required 'name' field: {provider_data}")
187
212
 
188
213
  endpoint = provider_data.get("endpoint")
189
214
  if not endpoint:
@@ -282,6 +307,22 @@ class MeshLlmAgentInjector(BaseInjector):
282
307
  llm_agent = self._create_llm_agent(function_id)
283
308
  wrapper._mesh_update_llm_agent(llm_agent)
284
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
+ )
285
326
  elif wrapper:
286
327
  logger.warning(
287
328
  f"⚠️ Wrapper for '{function_id}' found but has no _mesh_update_llm_agent method"
@@ -487,12 +528,28 @@ class MeshLlmAgentInjector(BaseInjector):
487
528
  api_key=config_dict.get("api_key", ""), # Will use ENV if empty
488
529
  max_iterations=config_dict.get("max_iterations", 10),
489
530
  system_prompt=config_dict.get("system_prompt"),
531
+ output_mode=config_dict.get(
532
+ "output_mode"
533
+ ), # Pass through output_mode from decorator
490
534
  )
491
535
 
492
536
  # Phase 4: Template support - extract template metadata
493
537
  is_template = config_dict.get("is_template", False)
494
538
  template_path = config_dict.get("template_path")
495
539
 
540
+ # Determine vendor for provider handler selection
541
+ # Priority: 1) From registry (mesh delegation), 2) From model name, 3) None
542
+ vendor = llm_agent_data.get("vendor")
543
+ if not vendor:
544
+ # For direct LiteLLM calls, extract vendor from model name
545
+ # e.g., "anthropic/claude-sonnet-4-5" -> "anthropic"
546
+ model = config_dict.get("model", "")
547
+ vendor = extract_vendor_from_model(model)
548
+ if vendor:
549
+ logger.info(
550
+ f"🔍 Extracted vendor '{vendor}' from model '{model}' for handler selection"
551
+ )
552
+
496
553
  # Create MeshLlmAgent with both metadata and proxies
497
554
  llm_agent = MeshLlmAgent(
498
555
  config=llm_config,
@@ -503,14 +560,20 @@ class MeshLlmAgentInjector(BaseInjector):
503
560
  tool_proxies=llm_agent_data["tools_proxies"], # Proxies for execution
504
561
  template_path=template_path if is_template else None,
505
562
  context_value=context_value if is_template else None,
506
- provider_proxy=llm_agent_data.get("provider_proxy"), # Provider proxy for mesh delegation
507
- vendor=llm_agent_data.get("vendor"), # Phase 2: Vendor for provider handler selection
563
+ provider_proxy=llm_agent_data.get(
564
+ "provider_proxy"
565
+ ), # Provider proxy for mesh delegation
566
+ vendor=vendor, # Vendor for provider handler selection (from registry or model name)
508
567
  )
509
568
 
510
569
  logger.debug(
511
570
  f"🤖 Created MeshLlmAgent for {function_id} with {len(llm_agent_data['tools_metadata'])} tools"
512
571
  + (f", template={template_path}" if is_template else "")
513
- + (f", provider_proxy={llm_agent_data.get('provider_proxy').function_name if llm_agent_data.get('provider_proxy') else 'None'}" if isinstance(config_dict.get("provider"), dict) else "")
572
+ + (
573
+ f", provider_proxy={llm_agent_data.get('provider_proxy').function_name if llm_agent_data.get('provider_proxy') else 'None'}"
574
+ if isinstance(config_dict.get("provider"), dict)
575
+ else ""
576
+ )
514
577
  )
515
578
 
516
579
  return llm_agent