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.
- _mcp_mesh/__init__.py +1 -1
- _mcp_mesh/engine/base_injector.py +171 -0
- _mcp_mesh/engine/decorator_registry.py +136 -33
- _mcp_mesh/engine/dependency_injector.py +91 -18
- _mcp_mesh/engine/http_wrapper.py +5 -22
- _mcp_mesh/engine/llm_config.py +41 -0
- _mcp_mesh/engine/llm_errors.py +115 -0
- _mcp_mesh/engine/mesh_llm_agent.py +440 -0
- _mcp_mesh/engine/mesh_llm_agent_injector.py +487 -0
- _mcp_mesh/engine/response_parser.py +240 -0
- _mcp_mesh/engine/signature_analyzer.py +229 -99
- _mcp_mesh/engine/tool_executor.py +169 -0
- _mcp_mesh/engine/tool_schema_builder.py +125 -0
- _mcp_mesh/engine/unified_mcp_proxy.py +14 -12
- _mcp_mesh/generated/.openapi-generator/FILES +4 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/__init__.py +81 -44
- _mcp_mesh/generated/mcp_mesh_registry_client/models/__init__.py +72 -35
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter.py +132 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner.py +172 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_filter_filter_inner_one_of.py +92 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/models/llm_tool_info.py +121 -0
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_agent_registration.py +98 -51
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_registration_response.py +93 -44
- _mcp_mesh/generated/mcp_mesh_registry_client/models/mesh_tool_registration.py +84 -41
- _mcp_mesh/pipeline/api_heartbeat/api_dependency_resolution.py +9 -72
- _mcp_mesh/pipeline/mcp_heartbeat/heartbeat_pipeline.py +6 -3
- _mcp_mesh/pipeline/mcp_heartbeat/llm_tools_resolution.py +222 -0
- _mcp_mesh/pipeline/mcp_startup/fastmcpserver_discovery.py +7 -0
- _mcp_mesh/pipeline/mcp_startup/heartbeat_preparation.py +65 -4
- _mcp_mesh/pipeline/mcp_startup/startup_pipeline.py +2 -2
- _mcp_mesh/shared/registry_client_wrapper.py +60 -4
- _mcp_mesh/utils/fastmcp_schema_extractor.py +476 -0
- {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/METADATA +1 -1
- {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/RECORD +39 -25
- mesh/__init__.py +8 -4
- mesh/decorators.py +344 -2
- mesh/types.py +145 -94
- {mcp_mesh-0.5.7.dist-info → mcp_mesh-0.6.0.dist-info}/WHEEL +0 -0
- {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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
41
|
+
# Check if it's McpMeshAgent type (handle different import paths and Union types)
|
|
110
42
|
is_agent = False
|
|
111
43
|
|
|
112
|
-
# Direct
|
|
44
|
+
# Direct McpMeshAgent type
|
|
113
45
|
if (
|
|
114
|
-
param_type
|
|
46
|
+
param_type == McpMeshAgent
|
|
115
47
|
or (
|
|
116
48
|
hasattr(param_type, "__name__")
|
|
117
|
-
and param_type.__name__
|
|
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.,
|
|
58
|
+
# Union type (e.g., McpMeshAgent | None)
|
|
128
59
|
elif hasattr(param_type, "__args__"):
|
|
129
|
-
# Check if any arg in the union is
|
|
60
|
+
# Check if any arg in the union is McpMeshAgent
|
|
130
61
|
for arg in param_type.__args__:
|
|
131
|
-
if arg
|
|
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
|
|
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
|
|
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
|
|
101
|
+
# Check if it's McpMeshAgent type (handle different import paths and Union types)
|
|
172
102
|
is_mesh_agent = False
|
|
173
103
|
|
|
174
|
-
# Direct
|
|
104
|
+
# Direct McpMeshAgent type
|
|
175
105
|
if (
|
|
176
|
-
param_type
|
|
106
|
+
param_type == McpMeshAgent
|
|
177
107
|
or (
|
|
178
108
|
hasattr(param_type, "__name__")
|
|
179
|
-
and param_type.__name__
|
|
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.,
|
|
118
|
+
# Union type (e.g., McpMeshAgent | None)
|
|
190
119
|
elif hasattr(param_type, "__args__"):
|
|
191
|
-
# Check if any arg in the union is
|
|
120
|
+
# Check if any arg in the union is McpMeshAgent
|
|
192
121
|
for arg in param_type.__args__:
|
|
193
|
-
if arg
|
|
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
|
|
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
|
|
152
|
+
f"Function {func.__name__} has {len(mesh_positions)} McpMeshAgent parameters "
|
|
225
153
|
f"but {len(dependencies)} dependencies declared. "
|
|
226
|
-
f"Each McpMeshAgent
|
|
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
|