mcp-mesh 0.5.7__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. _mcp_mesh/__init__.py +1 -1
  2. _mcp_mesh/engine/base_injector.py +171 -0
  3. _mcp_mesh/engine/decorator_registry.py +136 -33
  4. _mcp_mesh/engine/dependency_injector.py +91 -18
  5. _mcp_mesh/engine/http_wrapper.py +5 -22
  6. _mcp_mesh/engine/llm_config.py +41 -0
  7. _mcp_mesh/engine/llm_errors.py +115 -0
  8. _mcp_mesh/engine/mesh_llm_agent.py +440 -0
  9. _mcp_mesh/engine/mesh_llm_agent_injector.py +487 -0
  10. _mcp_mesh/engine/response_parser.py +240 -0
  11. _mcp_mesh/engine/signature_analyzer.py +229 -99
  12. _mcp_mesh/engine/tool_executor.py +169 -0
  13. _mcp_mesh/engine/tool_schema_builder.py +125 -0
  14. _mcp_mesh/engine/unified_mcp_proxy.py +14 -12
  15. _mcp_mesh/generated/.openapi-generator/FILES +4 -0
  16. _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +81 -44
  17. _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +72 -35
  18. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +132 -0
  19. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +172 -0
  20. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +92 -0
  21. _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +121 -0
  22. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +98 -51
  23. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +93 -44
  24. _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +84 -41
  25. _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +9 -72
  26. _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +6 -3
  27. _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +222 -0
  28. _mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py +7 -0
  29. _mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py +65 -4
  30. _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +2 -2
  31. _mcp_mesh/shared/registry_client_wrapper.py +60 -4
  32. _mcp_mesh/utils/fastmcp_schema_extractor.py +476 -0
  33. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/METADATA +1 -1
  34. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/RECORD +39 -25
  35. mesh/__init__.py +8 -4
  36. mesh/decorators.py +344 -2
  37. mesh/types.py +145 -94
  38. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/WHEEL +0 -0
  39. {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,240 @@
1
+ """
2
+ Response parser for LLM outputs.
3
+
4
+ Handles parsing and validation of LLM responses into Pydantic models.
5
+ Separated from MeshLlmAgent for better testability and reusability.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import re
11
+ from typing import Any, TypeVar
12
+
13
+ from pydantic import BaseModel, ValidationError
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Module-level compiled regex for code fence stripping (compile once, use many times)
18
+ _CODE_FENCE_PATTERN = re.compile(r"^```(?:json)?\s*|\s*```$", re.MULTILINE)
19
+
20
+ # REMOVE_LATER: Regex to extract JSON from code fences in mixed content
21
+ _JSON_BLOCK_PATTERN = re.compile(r"```json\s*\n(.+?)\n```", re.DOTALL)
22
+
23
+ T = TypeVar("T", bound=BaseModel)
24
+
25
+
26
+ class ResponseParseError(Exception):
27
+ """Raised when response parsing or validation fails."""
28
+
29
+ pass
30
+
31
+
32
+ class ResponseParser:
33
+ """
34
+ Utility class for parsing LLM responses into Pydantic models.
35
+
36
+ Handles:
37
+ - Markdown code fence stripping (```json ... ```)
38
+ - JSON parsing with fallback wrapping
39
+ - Pydantic validation
40
+ """
41
+
42
+ @staticmethod
43
+ def parse(content: str, output_type: type[T]) -> T:
44
+ """
45
+ Parse LLM response into Pydantic model.
46
+
47
+ Args:
48
+ content: Raw response content from LLM
49
+ output_type: Pydantic BaseModel class to parse into
50
+
51
+ Returns:
52
+ Parsed and validated Pydantic model instance
53
+
54
+ Raises:
55
+ ResponseParseError: If response doesn't match schema or invalid JSON
56
+ """
57
+ logger.debug(f"📝 Parsing response into {output_type.__name__}...")
58
+
59
+ # REMOVE_LATER: Debug raw content
60
+ logger.warning(f"🔍 REMOVE_LATER: Raw content length: {len(content)}")
61
+ logger.warning(
62
+ f"🔍 REMOVE_LATER: Raw content (first 500 chars): {content[:500]!r}"
63
+ )
64
+ logger.warning(
65
+ f"🔍 REMOVE_LATER: Raw content (last 200 chars): {content[-200:]!r}"
66
+ )
67
+
68
+ try:
69
+ # REMOVE_LATER: Extract JSON from mixed content (narrative + XML + JSON)
70
+ extracted_content = ResponseParser._extract_json_from_mixed_content(content)
71
+
72
+ # Strip markdown code fences if present
73
+ cleaned_content = ResponseParser._strip_markdown_fences(extracted_content)
74
+
75
+ # REMOVE_LATER: Debug cleaned content
76
+ logger.warning(
77
+ f"🔍 REMOVE_LATER: Cleaned content length: {len(cleaned_content)}"
78
+ )
79
+ logger.warning(
80
+ f"🔍 REMOVE_LATER: Cleaned content: {cleaned_content[:500]!r}"
81
+ )
82
+
83
+ # Try to parse as JSON
84
+ response_data = ResponseParser._parse_json_with_fallback(
85
+ cleaned_content, output_type
86
+ )
87
+
88
+ # Validate against output type
89
+ return ResponseParser._validate_and_create(response_data, output_type)
90
+
91
+ except ResponseParseError:
92
+ raise
93
+ except Exception as e:
94
+ logger.error(f"❌ Unexpected error parsing response: {e}")
95
+ raise ResponseParseError(f"Unexpected parsing error: {e}")
96
+
97
+ @staticmethod
98
+ def _extract_json_from_mixed_content(content: str) -> str:
99
+ """
100
+ REMOVE_LATER: Extract JSON from mixed content (narrative + XML + JSON).
101
+
102
+ Tries multiple strategies to find JSON in mixed responses:
103
+ 1. Find ```json ... ``` code fence blocks
104
+ 2. Find any JSON object {...} in the content
105
+ 3. Return original content if no extraction needed
106
+
107
+ Args:
108
+ content: Raw content that may contain narrative, XML, and JSON
109
+
110
+ Returns:
111
+ Extracted JSON string or original content
112
+ """
113
+ # REMOVE_LATER: Debug extraction attempt
114
+ logger.warning(
115
+ f"🔍 REMOVE_LATER: Attempting JSON extraction from content length: {len(content)}"
116
+ )
117
+
118
+ # Strategy 1: Try to find ```json ... ``` blocks
119
+ json_match = _JSON_BLOCK_PATTERN.search(content)
120
+ if json_match:
121
+ extracted = json_match.group(1).strip()
122
+ logger.warning(
123
+ f"🔍 REMOVE_LATER: Extracted JSON from code fence, length: {len(extracted)}"
124
+ )
125
+ logger.warning(f"🔍 REMOVE_LATER: Extracted content: {extracted[:200]}...")
126
+ return extracted
127
+
128
+ # Strategy 2: Try to find any JSON object {...} in content
129
+ # Look for balanced braces starting with { and ending with }
130
+ brace_start = content.find("{")
131
+ if brace_start != -1:
132
+ # Find matching closing brace
133
+ brace_count = 0
134
+ for i in range(brace_start, len(content)):
135
+ if content[i] == "{":
136
+ brace_count += 1
137
+ elif content[i] == "}":
138
+ brace_count -= 1
139
+ if brace_count == 0:
140
+ # Found matching brace
141
+ extracted = content[brace_start : i + 1]
142
+ logger.warning(
143
+ f"🔍 REMOVE_LATER: Extracted JSON from braces, length: {len(extracted)}"
144
+ )
145
+ logger.warning(
146
+ f"🔍 REMOVE_LATER: Extracted content: {extracted[:200]}..."
147
+ )
148
+ return extracted
149
+
150
+ # No JSON found, return original
151
+ logger.warning(
152
+ "🔍 REMOVE_LATER: No JSON extraction needed, returning original"
153
+ )
154
+ return content
155
+
156
+ @staticmethod
157
+ def _strip_markdown_fences(content: str) -> str:
158
+ """
159
+ Strip markdown code fences from content using regex.
160
+
161
+ Handles:
162
+ - ```json ... ``` (with optional whitespace/newlines)
163
+ - ``` ... ``` (with optional whitespace/newlines)
164
+ - Mixed whitespace and newline patterns
165
+
166
+ Uses compiled regex for optimal performance.
167
+
168
+ Args:
169
+ content: Raw content
170
+
171
+ Returns:
172
+ Content with fences removed
173
+ """
174
+ return _CODE_FENCE_PATTERN.sub("", content).strip()
175
+
176
+ @staticmethod
177
+ def _parse_json_with_fallback(content: str, output_type: type[T]) -> dict[str, Any]:
178
+ """
179
+ Parse content as JSON with fallback wrapping.
180
+
181
+ If direct JSON parsing fails, tries to wrap content in {"response": content}
182
+ to handle plain text responses.
183
+
184
+ Args:
185
+ content: Cleaned content
186
+ output_type: Target Pydantic model
187
+
188
+ Returns:
189
+ Parsed JSON dict
190
+
191
+ Raises:
192
+ ResponseParseError: If JSON parsing fails even with fallback
193
+ """
194
+ try:
195
+ return json.loads(content)
196
+ except json.JSONDecodeError as e:
197
+ # If not JSON, try wrapping it as a simple response
198
+ logger.warning(
199
+ f"⚠️ Response is not valid JSON, attempting to wrap: {content[:100]}..."
200
+ )
201
+ try:
202
+ # Try to match it to the output type as a simple string
203
+ response_data = {"response": content}
204
+ # Test if wrapping works by validating
205
+ output_type(**response_data)
206
+ logger.debug("✅ Response wrapped successfully")
207
+ return response_data
208
+ except ValidationError:
209
+ # If wrapping doesn't work, raise the original JSON error
210
+ raise ResponseParseError(f"Invalid JSON response: {e}")
211
+
212
+ @staticmethod
213
+ def _validate_and_create(response_data: dict[str, Any], output_type: type[T]) -> T:
214
+ """
215
+ Validate data against Pydantic model and create instance.
216
+
217
+ Args:
218
+ response_data: Parsed JSON data
219
+ output_type: Target Pydantic model
220
+
221
+ Returns:
222
+ Validated Pydantic model instance
223
+
224
+ Raises:
225
+ ResponseParseError: If validation fails
226
+ """
227
+ try:
228
+ parsed = output_type(**response_data)
229
+ logger.debug(f"✅ Response parsed successfully: {parsed}")
230
+ return parsed
231
+ except ValidationError as e:
232
+ # Enhanced error logging with schema diff
233
+ expected_schema = output_type.model_json_schema()
234
+ logger.error(
235
+ f"❌ Schema validation failed:\n"
236
+ f"Expected schema: {json.dumps(expected_schema, indent=2)}\n"
237
+ f"Received data: {json.dumps(response_data, indent=2)}\n"
238
+ f"Validation errors: {e}"
239
+ )
240
+ raise ResponseParseError(f"Response validation failed: {e}")
@@ -5,89 +5,21 @@ Function signature analysis for MCP Mesh dependency injection.
5
5
  import inspect
6
6
  from typing import Any, get_type_hints
7
7
 
8
- from mesh.types import McpAgent, McpMeshAgent
9
-
10
-
11
- def get_agent_parameter_types(func: Any) -> dict[int, str]:
12
- """
13
- Get parameter positions and their agent types (McpAgent vs McpMeshAgent).
14
-
15
- Args:
16
- func: Function to analyze
17
-
18
- Returns:
19
- Dict mapping parameter position to agent type name
20
- e.g., {1: "McpAgent", 2: "McpMeshAgent"}
21
- """
22
- try:
23
- # Get type hints for the function
24
- type_hints = get_type_hints(func)
25
-
26
- # Get parameter names in order
27
- sig = inspect.signature(func)
28
- param_names = list(sig.parameters.keys())
29
-
30
- # Find positions and types of agent parameters
31
- agent_types = {}
32
- for i, param_name in enumerate(param_names):
33
- if param_name in type_hints:
34
- param_type = type_hints[param_name]
35
-
36
- # Check for McpAgent or McpMeshAgent (handle Union types too)
37
- agent_type = None
38
-
39
- # Direct type check
40
- if param_type == McpAgent or (
41
- hasattr(param_type, "__name__")
42
- and param_type.__name__ == "McpAgent"
43
- ):
44
- agent_type = "McpAgent"
45
- elif param_type == McpMeshAgent or (
46
- hasattr(param_type, "__name__")
47
- and param_type.__name__ == "McpMeshAgent"
48
- ):
49
- agent_type = "McpMeshAgent"
50
-
51
- # Union type check (e.g., McpAgent | None)
52
- elif hasattr(param_type, "__args__"):
53
- for arg in param_type.__args__:
54
- if arg == McpAgent or (
55
- hasattr(arg, "__name__") and arg.__name__ == "McpAgent"
56
- ):
57
- agent_type = "McpAgent"
58
- break
59
- elif arg == McpMeshAgent or (
60
- hasattr(arg, "__name__") and arg.__name__ == "McpMeshAgent"
61
- ):
62
- agent_type = "McpMeshAgent"
63
- break
64
-
65
- if agent_type:
66
- agent_types[i] = agent_type
67
-
68
- return agent_types
69
-
70
- except Exception as e:
71
- # If we can't analyze the signature, return empty dict
72
- import logging
73
-
74
- logger = logging.getLogger(__name__)
75
- logger.warning(f"Failed to analyze agent parameter types for {func}: {e}")
76
- return {}
8
+ from mesh.types import McpMeshAgent, MeshLlmAgent
77
9
 
78
10
 
79
11
  def get_mesh_agent_positions(func: Any) -> list[int]:
80
12
  """
81
- Get positions of McpAgent and McpMeshAgent parameters in function signature.
13
+ Get positions of McpMeshAgent parameters in function signature.
82
14
 
83
15
  Args:
84
16
  func: Function to analyze
85
17
 
86
18
  Returns:
87
- List of parameter positions (0-indexed) that are McpAgent or McpMeshAgent types
19
+ List of parameter positions (0-indexed) that are McpMeshAgent types
88
20
 
89
21
  Example:
90
- def greet(name: str, date_svc: McpMeshAgent, file_svc: McpAgent):
22
+ def greet(name: str, date_svc: McpMeshAgent, file_svc: McpMeshAgent):
91
23
  pass
92
24
 
93
25
  get_mesh_agent_positions(greet) → [1, 2]
@@ -106,31 +38,29 @@ def get_mesh_agent_positions(func: Any) -> list[int]:
106
38
  if param_name in type_hints:
107
39
  param_type = type_hints[param_name]
108
40
 
109
- # Check if it's McpAgent or McpMeshAgent type (handle different import paths and Union types)
41
+ # Check if it's McpMeshAgent type (handle different import paths and Union types)
110
42
  is_agent = False
111
43
 
112
- # Direct McpAgent or McpMeshAgent type
44
+ # Direct McpMeshAgent type
113
45
  if (
114
- param_type in (McpAgent, McpMeshAgent)
46
+ param_type == McpMeshAgent
115
47
  or (
116
48
  hasattr(param_type, "__name__")
117
- and param_type.__name__ in ("McpAgent", "McpMeshAgent")
49
+ and param_type.__name__ == "McpMeshAgent"
118
50
  )
119
51
  or (
120
52
  hasattr(param_type, "__origin__")
121
- and param_type.__origin__
122
- in (type(McpAgent), type(McpMeshAgent))
53
+ and param_type.__origin__ == type(McpMeshAgent)
123
54
  )
124
55
  ):
125
56
  is_agent = True
126
57
 
127
- # Union type (e.g., McpAgent | None, McpMeshAgent | None)
58
+ # Union type (e.g., McpMeshAgent | None)
128
59
  elif hasattr(param_type, "__args__"):
129
- # Check if any arg in the union is McpAgent or McpMeshAgent
60
+ # Check if any arg in the union is McpMeshAgent
130
61
  for arg in param_type.__args__:
131
- if arg in (McpAgent, McpMeshAgent) or (
132
- hasattr(arg, "__name__")
133
- and arg.__name__ in ("McpAgent", "McpMeshAgent")
62
+ if arg == McpMeshAgent or (
63
+ hasattr(arg, "__name__") and arg.__name__ == "McpMeshAgent"
134
64
  ):
135
65
  is_agent = True
136
66
  break
@@ -151,13 +81,13 @@ def get_mesh_agent_positions(func: Any) -> list[int]:
151
81
 
152
82
  def get_mesh_agent_parameter_names(func: Any) -> list[str]:
153
83
  """
154
- Get names of McpMeshAgent/McpAgent parameters in function signature.
84
+ Get names of McpMeshAgent parameters in function signature.
155
85
 
156
86
  Args:
157
87
  func: Function to analyze
158
88
 
159
89
  Returns:
160
- List of parameter names that are McpMeshAgent or McpAgent types
90
+ List of parameter names that are McpMeshAgent types
161
91
  """
162
92
  try:
163
93
  type_hints = get_type_hints(func)
@@ -168,31 +98,29 @@ def get_mesh_agent_parameter_names(func: Any) -> list[str]:
168
98
  if param_name in type_hints:
169
99
  param_type = type_hints[param_name]
170
100
 
171
- # Check if it's McpAgent or McpMeshAgent type (handle different import paths and Union types)
101
+ # Check if it's McpMeshAgent type (handle different import paths and Union types)
172
102
  is_mesh_agent = False
173
103
 
174
- # Direct McpAgent or McpMeshAgent type
104
+ # Direct McpMeshAgent type
175
105
  if (
176
- param_type in (McpAgent, McpMeshAgent)
106
+ param_type == McpMeshAgent
177
107
  or (
178
108
  hasattr(param_type, "__name__")
179
- and param_type.__name__ in ("McpAgent", "McpMeshAgent")
109
+ and param_type.__name__ == "McpMeshAgent"
180
110
  )
181
111
  or (
182
112
  hasattr(param_type, "__origin__")
183
- and param_type.__origin__
184
- in (type(McpAgent), type(McpMeshAgent))
113
+ and param_type.__origin__ == type(McpMeshAgent)
185
114
  )
186
115
  ):
187
116
  is_mesh_agent = True
188
117
 
189
- # Union type (e.g., McpAgent | None, McpMeshAgent | None)
118
+ # Union type (e.g., McpMeshAgent | None)
190
119
  elif hasattr(param_type, "__args__"):
191
- # Check if any arg in the union is McpAgent or McpMeshAgent
120
+ # Check if any arg in the union is McpMeshAgent
192
121
  for arg in param_type.__args__:
193
- if arg in (McpAgent, McpMeshAgent) or (
194
- hasattr(arg, "__name__")
195
- and arg.__name__ in ("McpAgent", "McpMeshAgent")
122
+ if arg == McpMeshAgent or (
123
+ hasattr(arg, "__name__") and arg.__name__ == "McpMeshAgent"
196
124
  ):
197
125
  is_mesh_agent = True
198
126
  break
@@ -208,7 +136,7 @@ def get_mesh_agent_parameter_names(func: Any) -> list[str]:
208
136
 
209
137
  def validate_mesh_dependencies(func: Any, dependencies: list[dict]) -> tuple[bool, str]:
210
138
  """
211
- Validate that the number of dependencies matches McpMeshAgent/McpAgent parameters.
139
+ Validate that the number of dependencies matches McpMeshAgent parameters.
212
140
 
213
141
  Args:
214
142
  func: Function to validate
@@ -221,9 +149,211 @@ def validate_mesh_dependencies(func: Any, dependencies: list[dict]) -> tuple[boo
221
149
 
222
150
  if len(dependencies) != len(mesh_positions):
223
151
  return False, (
224
- f"Function {func.__name__} has {len(mesh_positions)} McpMeshAgent/McpAgent parameters "
152
+ f"Function {func.__name__} has {len(mesh_positions)} McpMeshAgent parameters "
225
153
  f"but {len(dependencies)} dependencies declared. "
226
- f"Each McpMeshAgent/McpAgent parameter needs a corresponding dependency."
154
+ f"Each McpMeshAgent parameter needs a corresponding dependency."
227
155
  )
228
156
 
229
157
  return True, ""
158
+
159
+
160
+ def get_llm_agent_positions(func: Any) -> list[int]:
161
+ """
162
+ Get positions of MeshLlmAgent parameters in function signature.
163
+
164
+ Args:
165
+ func: Function to analyze
166
+
167
+ Returns:
168
+ List of parameter positions (0-indexed) that are MeshLlmAgent types
169
+
170
+ Example:
171
+ def chat(msg: str, llm: MeshLlmAgent):
172
+ pass
173
+
174
+ get_llm_agent_positions(chat) → [1]
175
+ """
176
+ try:
177
+ # Get type hints for the function
178
+ type_hints = get_type_hints(func)
179
+
180
+ # Get parameter names in order
181
+ sig = inspect.signature(func)
182
+ param_names = list(sig.parameters.keys())
183
+
184
+ # Find positions of MeshLlmAgent parameters
185
+ llm_positions = []
186
+ for i, param_name in enumerate(param_names):
187
+ if param_name in type_hints:
188
+ param_type = type_hints[param_name]
189
+
190
+ # Check if it's MeshLlmAgent type (handle different import paths and Union types)
191
+ is_llm_agent = False
192
+
193
+ # Direct MeshLlmAgent type
194
+ if (
195
+ param_type == MeshLlmAgent
196
+ or (
197
+ hasattr(param_type, "__name__")
198
+ and param_type.__name__ == "MeshLlmAgent"
199
+ )
200
+ or (
201
+ hasattr(param_type, "__origin__")
202
+ and param_type.__origin__ == type(MeshLlmAgent)
203
+ )
204
+ ):
205
+ is_llm_agent = True
206
+
207
+ # Union type (e.g., MeshLlmAgent | None)
208
+ elif hasattr(param_type, "__args__"):
209
+ # Check if any arg in the union is MeshLlmAgent
210
+ for arg in param_type.__args__:
211
+ if arg == MeshLlmAgent or (
212
+ hasattr(arg, "__name__") and arg.__name__ == "MeshLlmAgent"
213
+ ):
214
+ is_llm_agent = True
215
+ break
216
+
217
+ if is_llm_agent:
218
+ llm_positions.append(i)
219
+
220
+ return llm_positions
221
+
222
+ except Exception as e:
223
+ # If we can't analyze the signature, return empty list
224
+ import logging
225
+
226
+ logger = logging.getLogger(__name__)
227
+ logger.warning(f"Failed to analyze signature for {func}: {e}")
228
+ return []
229
+
230
+
231
+ def has_llm_agent_parameter(func: Any) -> bool:
232
+ """
233
+ Check if function has any MeshLlmAgent parameters.
234
+
235
+ Args:
236
+ func: Function to analyze
237
+
238
+ Returns:
239
+ True if function has at least one MeshLlmAgent parameter
240
+ """
241
+ return len(get_llm_agent_positions(func)) > 0
242
+
243
+
244
+ def get_context_parameter_name(
245
+ func: Any, explicit_name: str | None = None
246
+ ) -> tuple[str, int] | None:
247
+ """
248
+ Get context parameter name and index for template rendering (Phase 2).
249
+
250
+ This function detects context parameters using a hybrid approach:
251
+ 1. Explicit name (if provided) - validates existence
252
+ 2. Convention-based detection - checks for prompt_context, llm_context, context
253
+ 3. Type hint detection - finds MeshContextModel subclass parameters
254
+
255
+ Args:
256
+ func: Function to analyze
257
+ explicit_name: Optional explicit parameter name from @mesh.llm(context_param="...")
258
+
259
+ Returns:
260
+ Tuple of (param_name, param_index) or None if no context parameter found
261
+
262
+ Raises:
263
+ ValueError: If explicit_name provided but parameter not found
264
+
265
+ Example:
266
+ # Explicit name
267
+ def chat(msg: str, ctx: ChatContext, llm: MeshLlmAgent = None):
268
+ pass
269
+ get_context_parameter_name(chat, "ctx") → ("ctx", 1)
270
+
271
+ # Convention-based
272
+ def analyze(query: str, prompt_context: dict, llm: MeshLlmAgent = None):
273
+ pass
274
+ get_context_parameter_name(analyze) → ("prompt_context", 1)
275
+
276
+ # Type hint detection
277
+ def process(data: str, my_ctx: ChatContext, llm: MeshLlmAgent = None):
278
+ pass
279
+ get_context_parameter_name(process) → ("my_ctx", 1)
280
+ """
281
+ try:
282
+ sig = inspect.signature(func)
283
+ param_names = list(sig.parameters.keys())
284
+
285
+ # Get type hints (may fail for some functions)
286
+ type_hints = {}
287
+ try:
288
+ type_hints = get_type_hints(func)
289
+ except Exception:
290
+ pass # Continue without type hints
291
+
292
+ # Strategy 1: Explicit name (highest priority)
293
+ if explicit_name is not None:
294
+ if explicit_name in param_names:
295
+ param_index = param_names.index(explicit_name)
296
+ return (explicit_name, param_index)
297
+ else:
298
+ raise ValueError(
299
+ f"Context parameter '{explicit_name}' not found in function '{func.__name__}'. "
300
+ f"Available parameters: {param_names}"
301
+ )
302
+
303
+ # Strategy 2: Type hint detection (find MeshContextModel parameters)
304
+ # This has priority over convention names
305
+ if type_hints:
306
+ from mesh.types import MeshContextModel
307
+
308
+ for i, param_name in enumerate(param_names):
309
+ if param_name in type_hints:
310
+ param_type = type_hints[param_name]
311
+
312
+ # Check if it's MeshContextModel or subclass
313
+ is_context_model = False
314
+
315
+ # Direct MeshContextModel type
316
+ try:
317
+ if inspect.isclass(param_type) and issubclass(
318
+ param_type, MeshContextModel
319
+ ):
320
+ is_context_model = True
321
+ except TypeError:
322
+ pass # Not a class, check other cases
323
+
324
+ # Union type (e.g., Optional[MeshContextModel])
325
+ if not is_context_model and hasattr(param_type, "__args__"):
326
+ for arg in param_type.__args__:
327
+ if arg is not type(None): # Skip None in Optional
328
+ try:
329
+ if inspect.isclass(arg) and issubclass(
330
+ arg, MeshContextModel
331
+ ):
332
+ is_context_model = True
333
+ break
334
+ except TypeError:
335
+ pass
336
+
337
+ if is_context_model:
338
+ return (param_name, i)
339
+
340
+ # Strategy 3: Convention-based detection (check in priority order)
341
+ # This comes after type hint detection
342
+ convention_names = ["prompt_context", "llm_context", "context"]
343
+ for convention_name in convention_names:
344
+ if convention_name in param_names:
345
+ param_index = param_names.index(convention_name)
346
+ return (convention_name, param_index)
347
+
348
+ # No context parameter found
349
+ return None
350
+
351
+ except ValueError:
352
+ # Re-raise ValueError for explicit name validation errors
353
+ raise
354
+ except Exception as e:
355
+ import logging
356
+
357
+ logger = logging.getLogger(__name__)
358
+ logger.debug(f"Failed to detect context parameter for {func.__name__}: {e}")
359
+ return None