aip-agents-binary 0.5.25b9__py3-none-any.whl → 0.6.1__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.
Potentially problematic release.
This version of aip-agents-binary might be problematic. Click here for more details.
- aip_agents/agent/base_langgraph_agent.py +137 -68
- aip_agents/agent/base_langgraph_agent.pyi +3 -2
- aip_agents/agent/langgraph_react_agent.py +252 -16
- aip_agents/agent/langgraph_react_agent.pyi +40 -1
- aip_agents/examples/compare_streaming_client.py +2 -2
- aip_agents/examples/compare_streaming_server.py +1 -1
- aip_agents/examples/hello_world_ptc.py +51 -0
- aip_agents/examples/hello_world_ptc.pyi +5 -0
- aip_agents/examples/hello_world_tool_output_client.py +9 -0
- aip_agents/examples/todolist_planning_a2a_langchain_client.py +2 -2
- aip_agents/examples/todolist_planning_a2a_langgraph_server.py +1 -1
- aip_agents/guardrails/engines/base.py +6 -6
- aip_agents/mcp/client/connection_manager.py +36 -1
- aip_agents/mcp/client/connection_manager.pyi +3 -0
- aip_agents/mcp/client/persistent_session.py +318 -68
- aip_agents/mcp/client/persistent_session.pyi +9 -0
- aip_agents/mcp/client/transports.py +33 -2
- aip_agents/mcp/client/transports.pyi +9 -0
- aip_agents/ptc/__init__.py +48 -0
- aip_agents/ptc/__init__.pyi +10 -0
- aip_agents/ptc/doc_gen.py +122 -0
- aip_agents/ptc/doc_gen.pyi +40 -0
- aip_agents/ptc/exceptions.py +39 -0
- aip_agents/ptc/exceptions.pyi +22 -0
- aip_agents/ptc/executor.py +143 -0
- aip_agents/ptc/executor.pyi +73 -0
- aip_agents/ptc/mcp/__init__.py +45 -0
- aip_agents/ptc/mcp/__init__.pyi +7 -0
- aip_agents/ptc/mcp/sandbox_bridge.py +668 -0
- aip_agents/ptc/mcp/sandbox_bridge.pyi +47 -0
- aip_agents/ptc/mcp/templates/__init__.py +1 -0
- aip_agents/ptc/mcp/templates/__init__.pyi +0 -0
- aip_agents/ptc/mcp/templates/mcp_client.py.template +239 -0
- aip_agents/ptc/naming.py +184 -0
- aip_agents/ptc/naming.pyi +76 -0
- aip_agents/ptc/payload.py +26 -0
- aip_agents/ptc/payload.pyi +15 -0
- aip_agents/ptc/prompt_builder.py +571 -0
- aip_agents/ptc/prompt_builder.pyi +55 -0
- aip_agents/ptc/ptc_helper.py +16 -0
- aip_agents/ptc/ptc_helper.pyi +1 -0
- aip_agents/ptc/sandbox_bridge.py +58 -0
- aip_agents/ptc/sandbox_bridge.pyi +25 -0
- aip_agents/ptc/template_utils.py +33 -0
- aip_agents/ptc/template_utils.pyi +13 -0
- aip_agents/ptc/templates/__init__.py +1 -0
- aip_agents/ptc/templates/__init__.pyi +0 -0
- aip_agents/ptc/templates/ptc_helper.py.template +134 -0
- aip_agents/sandbox/__init__.py +43 -0
- aip_agents/sandbox/__init__.pyi +5 -0
- aip_agents/sandbox/defaults.py +9 -0
- aip_agents/sandbox/defaults.pyi +2 -0
- aip_agents/sandbox/e2b_runtime.py +267 -0
- aip_agents/sandbox/e2b_runtime.pyi +51 -0
- aip_agents/sandbox/template_builder.py +131 -0
- aip_agents/sandbox/template_builder.pyi +36 -0
- aip_agents/sandbox/types.py +24 -0
- aip_agents/sandbox/types.pyi +14 -0
- aip_agents/sandbox/validation.py +50 -0
- aip_agents/sandbox/validation.pyi +20 -0
- aip_agents/tools/__init__.py +2 -0
- aip_agents/tools/__init__.pyi +2 -1
- aip_agents/tools/browser_use/browser_use_tool.py +8 -0
- aip_agents/tools/browser_use/streaming.py +2 -0
- aip_agents/tools/execute_ptc_code.py +305 -0
- aip_agents/tools/execute_ptc_code.pyi +87 -0
- aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +26 -1
- aip_agents/utils/langgraph/tool_output_management.py +80 -0
- aip_agents/utils/langgraph/tool_output_management.pyi +37 -0
- {aip_agents_binary-0.5.25b9.dist-info → aip_agents_binary-0.6.1.dist-info}/METADATA +51 -48
- {aip_agents_binary-0.5.25b9.dist-info → aip_agents_binary-0.6.1.dist-info}/RECORD +73 -27
- {aip_agents_binary-0.5.25b9.dist-info → aip_agents_binary-0.6.1.dist-info}/WHEEL +0 -0
- {aip_agents_binary-0.5.25b9.dist-info → aip_agents_binary-0.6.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
"""Sandbox Bridge for PTC.
|
|
2
|
+
|
|
3
|
+
This module generates the sandbox payload (config + tool modules) that allows
|
|
4
|
+
LLM-generated code to call MCP tools inside an E2B sandbox.
|
|
5
|
+
|
|
6
|
+
The payload includes:
|
|
7
|
+
- ptc_config.json: MCP server configs for the sandbox MCP client
|
|
8
|
+
- tools/__init__.py: Package init with server imports
|
|
9
|
+
- tools/<server>.py: Per-server module with sync tool functions
|
|
10
|
+
- tools/mcp_client.py: HTTP JSON-RPC client for MCP calls
|
|
11
|
+
|
|
12
|
+
Authors:
|
|
13
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from aip_agents.mcp.client.base_mcp_client import BaseMCPClient
|
|
21
|
+
from aip_agents.ptc.doc_gen import (
|
|
22
|
+
render_tool_doc,
|
|
23
|
+
)
|
|
24
|
+
from aip_agents.ptc.naming import (
|
|
25
|
+
DEFAULT_EXAMPLE_PLACEHOLDER,
|
|
26
|
+
example_value_from_schema,
|
|
27
|
+
sanitize_function_name,
|
|
28
|
+
sanitize_module_name_with_reserved,
|
|
29
|
+
schema_to_params,
|
|
30
|
+
)
|
|
31
|
+
from aip_agents.ptc.payload import SandboxPayload
|
|
32
|
+
from aip_agents.ptc.ptc_helper import _generate_ptc_helper_module
|
|
33
|
+
from aip_agents.ptc.template_utils import render_template
|
|
34
|
+
from aip_agents.utils.logger import get_logger
|
|
35
|
+
|
|
36
|
+
logger = get_logger(__name__)
|
|
37
|
+
|
|
38
|
+
# Transport types supported in sandbox (HTTP-based only)
|
|
39
|
+
# Using normalized hyphenated format to align with connection_manager
|
|
40
|
+
SUPPORTED_TRANSPORTS = {"sse", "streamable-http"}
|
|
41
|
+
|
|
42
|
+
_TEMPLATE_PACKAGE = "aip_agents.ptc.mcp.templates"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ServerConfig:
|
|
47
|
+
"""Extracted server configuration for sandbox payload.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
name: Server name identifier.
|
|
51
|
+
transport: Transport type (sse or streamable_http).
|
|
52
|
+
url: Server URL.
|
|
53
|
+
headers: HTTP headers for authentication.
|
|
54
|
+
allowed_tools: List of allowed tool names, or None for all.
|
|
55
|
+
tools: List of tool definitions from the server.
|
|
56
|
+
timeout: Request timeout in seconds.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
name: str
|
|
60
|
+
transport: str
|
|
61
|
+
url: str
|
|
62
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
63
|
+
allowed_tools: list[str] | None = None
|
|
64
|
+
tools: list[dict[str, Any]] = field(default_factory=list)
|
|
65
|
+
timeout: float = 60.0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def build_mcp_payload(
|
|
69
|
+
mcp_client: BaseMCPClient,
|
|
70
|
+
default_tool_timeout: float = 60.0,
|
|
71
|
+
) -> SandboxPayload:
|
|
72
|
+
"""Build MCP sandbox payload from MCP client configuration.
|
|
73
|
+
|
|
74
|
+
Extracts server configs, tools, and generates the necessary files
|
|
75
|
+
for the sandbox to execute PTC code.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
mcp_client: The MCP client with configured servers.
|
|
79
|
+
default_tool_timeout: Default timeout for tool calls in seconds.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
SandboxPayload containing files and env vars for the sandbox.
|
|
83
|
+
"""
|
|
84
|
+
payload = SandboxPayload()
|
|
85
|
+
|
|
86
|
+
# Extract server configs
|
|
87
|
+
server_configs = await _extract_server_configs(mcp_client, default_tool_timeout)
|
|
88
|
+
|
|
89
|
+
if not server_configs:
|
|
90
|
+
logger.warning("No supported MCP servers found for sandbox payload")
|
|
91
|
+
return payload
|
|
92
|
+
|
|
93
|
+
# Generate ptc_config.json
|
|
94
|
+
config_json = _generate_config_json(server_configs)
|
|
95
|
+
payload.files["ptc_config.json"] = config_json
|
|
96
|
+
|
|
97
|
+
# Generate tools/mcp_client.py
|
|
98
|
+
payload.files["tools/mcp_client.py"] = _generate_mcp_client_module()
|
|
99
|
+
|
|
100
|
+
# Generate tools/__init__.py
|
|
101
|
+
server_names = [cfg.name for cfg in server_configs]
|
|
102
|
+
payload.files["tools/__init__.py"] = _generate_tools_init(server_names)
|
|
103
|
+
|
|
104
|
+
# Generate tools/<server>.py for each server (using reserved-aware sanitization)
|
|
105
|
+
for server_cfg in server_configs:
|
|
106
|
+
module_content = _generate_server_module(server_cfg)
|
|
107
|
+
safe_name = sanitize_module_name_with_reserved(server_cfg.name)
|
|
108
|
+
payload.files[f"tools/{safe_name}.py"] = module_content
|
|
109
|
+
|
|
110
|
+
# Generate tools/ptc_helper.py for discovery
|
|
111
|
+
payload.files["tools/ptc_helper.py"] = _generate_ptc_helper_module()
|
|
112
|
+
|
|
113
|
+
# Generate tools/ptc_index.json for tool index
|
|
114
|
+
payload.files["tools/ptc_index.json"] = _generate_ptc_index(server_configs)
|
|
115
|
+
|
|
116
|
+
# Generate tools/docs/<package>/<tool>.md for each tool
|
|
117
|
+
docs = _generate_all_docs(server_configs)
|
|
118
|
+
payload.files.update(docs)
|
|
119
|
+
|
|
120
|
+
logger.info(f"Built sandbox payload with {len(server_configs)} servers, {len(payload.files)} files")
|
|
121
|
+
|
|
122
|
+
return payload
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _get_config_value(config: Any, key: str, default: Any = None) -> Any:
|
|
126
|
+
"""Extract value from config dict or object.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
config: Configuration dict or object.
|
|
130
|
+
key: Key to extract.
|
|
131
|
+
default: Default value if key not found.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Extracted value or default.
|
|
135
|
+
"""
|
|
136
|
+
if isinstance(config, dict):
|
|
137
|
+
return config.get(key, default)
|
|
138
|
+
return getattr(config, key, default)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _normalize_transport(transport: Any) -> str | None:
|
|
142
|
+
"""Normalize transport format.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
transport: Raw transport value.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Normalized transport string or None.
|
|
149
|
+
"""
|
|
150
|
+
if not transport:
|
|
151
|
+
return None
|
|
152
|
+
return str(transport).lower().replace("_", "-")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _extract_headers(config: Any) -> dict[str, str]:
|
|
156
|
+
"""Extract headers from config.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
config: Configuration dict or object.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Headers dictionary.
|
|
163
|
+
"""
|
|
164
|
+
raw_headers = _get_config_value(config, "headers")
|
|
165
|
+
if raw_headers and isinstance(raw_headers, dict):
|
|
166
|
+
return dict(raw_headers)
|
|
167
|
+
return {}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _extract_timeout(config: Any, default_timeout: float) -> float:
|
|
171
|
+
"""Extract timeout from config.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
config: Configuration dict or object.
|
|
175
|
+
default_timeout: Default timeout value.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Timeout in seconds.
|
|
179
|
+
"""
|
|
180
|
+
raw_timeout = _get_config_value(config, "timeout")
|
|
181
|
+
if raw_timeout is not None:
|
|
182
|
+
return float(raw_timeout)
|
|
183
|
+
return default_timeout
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _extract_allowed_tools(mcp_client: BaseMCPClient, server_name: str, config: Any) -> list[str] | None:
|
|
187
|
+
"""Extract allowed tools from session or config.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
mcp_client: MCP client instance.
|
|
191
|
+
server_name: Name of the server.
|
|
192
|
+
config: Configuration dict or object.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
List of allowed tool names or None.
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
session = mcp_client.session_pool.get_session(server_name)
|
|
199
|
+
if session.allowed_tools:
|
|
200
|
+
return list(session.allowed_tools)
|
|
201
|
+
except (KeyError, AttributeError):
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
raw_allowed = _get_config_value(config, "allowed_tools")
|
|
205
|
+
if raw_allowed and isinstance(raw_allowed, list):
|
|
206
|
+
return list(raw_allowed)
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def _extract_tools(
|
|
211
|
+
mcp_client: BaseMCPClient,
|
|
212
|
+
server_name: str,
|
|
213
|
+
allowed_tools: list[str] | None,
|
|
214
|
+
) -> list[dict[str, Any]]:
|
|
215
|
+
"""Extract tools from MCP client.
|
|
216
|
+
|
|
217
|
+
When tools are not loaded but allowed_tools exists, returns stub tool entries.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
mcp_client: MCP client instance.
|
|
221
|
+
server_name: Name of the server.
|
|
222
|
+
allowed_tools: List of allowed tool names or None.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
List of tool definitions. Stubs have minimal schema when tools not loaded.
|
|
226
|
+
"""
|
|
227
|
+
tools: list[dict[str, Any]] = []
|
|
228
|
+
try:
|
|
229
|
+
raw_tools = await mcp_client.get_raw_mcp_tools(server_name)
|
|
230
|
+
for tool in raw_tools:
|
|
231
|
+
if allowed_tools and tool.name not in allowed_tools:
|
|
232
|
+
continue
|
|
233
|
+
tools.append(
|
|
234
|
+
{
|
|
235
|
+
"name": tool.name,
|
|
236
|
+
"description": tool.description or "",
|
|
237
|
+
"input_schema": tool.inputSchema,
|
|
238
|
+
"stub": False,
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.warning(f"Failed to get tools from server '{server_name}': {e}")
|
|
243
|
+
# If tools not loaded but allowlist exists, create stub entries
|
|
244
|
+
if allowed_tools:
|
|
245
|
+
for tool_name in sorted(allowed_tools):
|
|
246
|
+
tools.append(
|
|
247
|
+
{
|
|
248
|
+
"name": tool_name,
|
|
249
|
+
"description": "",
|
|
250
|
+
"input_schema": {"type": "object", "properties": {}},
|
|
251
|
+
"stub": True,
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
return tools
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
async def _extract_server_configs(
|
|
258
|
+
mcp_client: BaseMCPClient,
|
|
259
|
+
default_tool_timeout: float,
|
|
260
|
+
) -> list[ServerConfig]:
|
|
261
|
+
"""Extract server configurations from MCP client.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
mcp_client: The MCP client with configured servers.
|
|
265
|
+
default_tool_timeout: Default timeout for tool calls.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
List of ServerConfig objects for supported servers.
|
|
269
|
+
"""
|
|
270
|
+
server_configs: list[ServerConfig] = []
|
|
271
|
+
|
|
272
|
+
for server_name, config in mcp_client.servers.items():
|
|
273
|
+
transport = _normalize_transport(_get_config_value(config, "transport"))
|
|
274
|
+
|
|
275
|
+
if transport not in SUPPORTED_TRANSPORTS:
|
|
276
|
+
logger.warning(
|
|
277
|
+
f"Skipping server '{server_name}': transport '{transport}' not supported in sandbox "
|
|
278
|
+
f"(supported: {SUPPORTED_TRANSPORTS})"
|
|
279
|
+
)
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
url = _get_config_value(config, "url")
|
|
283
|
+
if not url:
|
|
284
|
+
logger.warning(f"Skipping server '{server_name}': no URL configured")
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
headers = _extract_headers(config)
|
|
288
|
+
timeout = _extract_timeout(config, default_tool_timeout)
|
|
289
|
+
allowed_tools = _extract_allowed_tools(mcp_client, server_name, config)
|
|
290
|
+
tools = await _extract_tools(mcp_client, server_name, allowed_tools)
|
|
291
|
+
|
|
292
|
+
server_configs.append(
|
|
293
|
+
ServerConfig(
|
|
294
|
+
name=server_name,
|
|
295
|
+
transport=transport,
|
|
296
|
+
url=url,
|
|
297
|
+
headers=headers,
|
|
298
|
+
allowed_tools=allowed_tools,
|
|
299
|
+
tools=tools,
|
|
300
|
+
timeout=timeout,
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
logger.debug(f"Extracted config for server '{server_name}': {len(tools)} tools")
|
|
305
|
+
|
|
306
|
+
return server_configs
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _generate_config_json(server_configs: list[ServerConfig]) -> str:
|
|
310
|
+
"""Generate ptc_config.json content.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
server_configs: List of server configurations.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
JSON string of the config.
|
|
317
|
+
"""
|
|
318
|
+
config = {
|
|
319
|
+
"servers": {
|
|
320
|
+
cfg.name: {
|
|
321
|
+
"transport": cfg.transport,
|
|
322
|
+
"url": cfg.url,
|
|
323
|
+
"headers": cfg.headers,
|
|
324
|
+
"timeout": cfg.timeout,
|
|
325
|
+
"allowed_tools": cfg.allowed_tools,
|
|
326
|
+
}
|
|
327
|
+
for cfg in server_configs
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return json.dumps(config, indent=2)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _generate_mcp_client_module() -> str:
|
|
334
|
+
"""Generate the tools/mcp_client.py module.
|
|
335
|
+
|
|
336
|
+
This module uses the official MCP Python SDK to call MCP tools from the sandbox.
|
|
337
|
+
It provides sync wrappers around the async MCP SDK for simpler LLM-generated code.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Python source code for the MCP client module.
|
|
341
|
+
"""
|
|
342
|
+
return render_template(_TEMPLATE_PACKAGE, "mcp_client.py.template")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _generate_tools_init(server_names: list[str]) -> str:
|
|
346
|
+
"""Generate tools/__init__.py content.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
server_names: List of server names.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Python source code for the __init__.py module.
|
|
353
|
+
"""
|
|
354
|
+
# Use reserved-safe sanitization and sort for deterministic output
|
|
355
|
+
safe_names = sorted(sanitize_module_name_with_reserved(name) for name in server_names)
|
|
356
|
+
|
|
357
|
+
imports = "\n".join(f"from tools import {name}" for name in safe_names)
|
|
358
|
+
all_list = ", ".join(f'"{name}"' for name in safe_names)
|
|
359
|
+
|
|
360
|
+
return f'''"""Generated tools package for PTC sandbox execution.
|
|
361
|
+
|
|
362
|
+
This package provides access to MCP tools configured for this agent.
|
|
363
|
+
Import tools from specific server modules:
|
|
364
|
+
|
|
365
|
+
from tools.server_name import tool_name
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
{imports}
|
|
369
|
+
|
|
370
|
+
__all__ = [{all_list}]
|
|
371
|
+
'''
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _generate_server_module(server_cfg: ServerConfig) -> str:
|
|
375
|
+
"""Generate tools/<server>.py module content.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
server_cfg: Server configuration with tools.
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Python source code for the server module.
|
|
382
|
+
"""
|
|
383
|
+
functions: list[str] = []
|
|
384
|
+
function_names: list[str] = []
|
|
385
|
+
|
|
386
|
+
for tool in server_cfg.tools:
|
|
387
|
+
func_name = sanitize_function_name(tool["name"])
|
|
388
|
+
function_names.append(func_name)
|
|
389
|
+
|
|
390
|
+
# Build function signature from input_schema
|
|
391
|
+
schema = tool.get("input_schema", {})
|
|
392
|
+
params = schema_to_params(schema)
|
|
393
|
+
doc = _build_docstring(tool)
|
|
394
|
+
|
|
395
|
+
func_code = f'''
|
|
396
|
+
def {func_name}({params}) -> Any:
|
|
397
|
+
"""{doc}"""
|
|
398
|
+
arguments = {_build_arguments_dict(schema)}
|
|
399
|
+
return call_tool("{server_cfg.name}", "{tool["name"]}", arguments)
|
|
400
|
+
'''
|
|
401
|
+
functions.append(func_code)
|
|
402
|
+
|
|
403
|
+
all_list = ", ".join(f'"{name}"' for name in function_names)
|
|
404
|
+
functions_code = "\n".join(functions)
|
|
405
|
+
|
|
406
|
+
return f'''"""Generated module for MCP server: {server_cfg.name}
|
|
407
|
+
|
|
408
|
+
This module provides Python functions for each tool exposed by the MCP server.
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
from typing import Any
|
|
412
|
+
|
|
413
|
+
from tools.mcp_client import call_tool
|
|
414
|
+
|
|
415
|
+
__all__ = [{all_list}]
|
|
416
|
+
|
|
417
|
+
{functions_code}
|
|
418
|
+
'''
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
# Note: sanitize_module_name, sanitize_function_name, schema_to_params, and
|
|
422
|
+
# json_type_to_python are imported from aip_agents.ptc.naming
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _build_arguments_dict(schema: dict[str, Any]) -> str:
|
|
426
|
+
"""Build arguments dict code from schema.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
schema: JSON schema for tool input.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
Python code for building arguments dict.
|
|
433
|
+
"""
|
|
434
|
+
properties = schema.get("properties", {})
|
|
435
|
+
|
|
436
|
+
if not properties:
|
|
437
|
+
return "kwargs"
|
|
438
|
+
|
|
439
|
+
items: list[str] = []
|
|
440
|
+
for prop_name in properties:
|
|
441
|
+
safe_name = sanitize_function_name(prop_name)
|
|
442
|
+
items.append(f'"{prop_name}": {safe_name}')
|
|
443
|
+
|
|
444
|
+
return "{" + ", ".join(items) + "}"
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _build_docstring(tool: dict[str, Any]) -> str:
|
|
448
|
+
"""Build docstring for a tool function.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
tool: Tool definition with name, description, input_schema.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
Docstring content.
|
|
455
|
+
"""
|
|
456
|
+
desc = tool.get("description", f"Call {tool['name']} tool.")
|
|
457
|
+
# Escape triple quotes in description
|
|
458
|
+
desc = desc.replace('"""', '\\"\\"\\"')
|
|
459
|
+
return desc
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _generate_tool_doc(
|
|
463
|
+
tool: dict[str, Any],
|
|
464
|
+
) -> str:
|
|
465
|
+
"""Generate markdown documentation for a single tool.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
tool: Tool definition with name, description, input_schema, optional stub flag.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Markdown documentation string.
|
|
472
|
+
"""
|
|
473
|
+
func_name = sanitize_function_name(tool["name"])
|
|
474
|
+
schema = tool.get("input_schema", {})
|
|
475
|
+
|
|
476
|
+
# Use schema_to_params for consistent signatures (stubs and loaded tools)
|
|
477
|
+
params = schema_to_params(schema)
|
|
478
|
+
signature = f"{func_name}({params})"
|
|
479
|
+
|
|
480
|
+
# Add example section
|
|
481
|
+
example_args, uses_placeholder = _build_example_args_with_placeholder(schema)
|
|
482
|
+
if tool.get("stub"):
|
|
483
|
+
uses_placeholder = True
|
|
484
|
+
example_heading = "## Example (placeholder)" if uses_placeholder else "## Example"
|
|
485
|
+
example_code = f"{func_name}({example_args})"
|
|
486
|
+
|
|
487
|
+
return render_tool_doc(
|
|
488
|
+
func_name=func_name,
|
|
489
|
+
signature=signature,
|
|
490
|
+
description=tool.get("description", ""),
|
|
491
|
+
schema=schema,
|
|
492
|
+
is_stub=tool.get("stub", False),
|
|
493
|
+
example_code=example_code,
|
|
494
|
+
example_heading=example_heading,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _build_example_args(schema: dict[str, Any]) -> str:
|
|
499
|
+
"""Build example argument string for a tool call.
|
|
500
|
+
|
|
501
|
+
Uses schema defaults, enums, or examples when available.
|
|
502
|
+
Falls back to neutral placeholders. Required params come first.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
schema: Tool input schema.
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
Example arguments string.
|
|
509
|
+
"""
|
|
510
|
+
args, _ = _build_example_args_with_placeholder(schema)
|
|
511
|
+
return args
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _is_placeholder(prop_schema: dict[str, Any]) -> bool:
|
|
515
|
+
"""Check if a property schema uses a placeholder value.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
prop_schema: Property schema dict.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
True if the schema doesn't have examples, default, or enum values.
|
|
522
|
+
"""
|
|
523
|
+
return not (prop_schema.get("examples") or "default" in prop_schema or prop_schema.get("enum"))
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _process_property_args(
|
|
527
|
+
properties: dict[str, Any],
|
|
528
|
+
required: set[str],
|
|
529
|
+
filter_required: bool,
|
|
530
|
+
) -> tuple[list[str], bool]:
|
|
531
|
+
"""Process properties and generate argument strings.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
properties: Schema properties dict.
|
|
535
|
+
required: Set of required property names.
|
|
536
|
+
filter_required: If True, process only required params; if False, only optional.
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Tuple of (list of arg strings, uses_placeholder flag).
|
|
540
|
+
"""
|
|
541
|
+
args: list[str] = []
|
|
542
|
+
uses_placeholder = False
|
|
543
|
+
|
|
544
|
+
for prop_name in sorted(properties.keys()):
|
|
545
|
+
is_required = prop_name in required
|
|
546
|
+
if (filter_required and not is_required) or (not filter_required and is_required):
|
|
547
|
+
continue
|
|
548
|
+
|
|
549
|
+
prop_schema = properties[prop_name]
|
|
550
|
+
safe_name = sanitize_function_name(prop_name)
|
|
551
|
+
example_value = _get_doc_example_value(prop_schema, prop_name)
|
|
552
|
+
args.append(f"{safe_name}={example_value}")
|
|
553
|
+
|
|
554
|
+
if _is_placeholder(prop_schema):
|
|
555
|
+
uses_placeholder = True
|
|
556
|
+
|
|
557
|
+
return args, uses_placeholder
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _build_example_args_with_placeholder(schema: dict[str, Any]) -> tuple[str, bool]:
|
|
561
|
+
"""Build example arguments with placeholder tracking.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
schema: Tool input schema.
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
Tuple of (arguments string, uses_placeholder flag).
|
|
568
|
+
"""
|
|
569
|
+
properties = schema.get("properties", {})
|
|
570
|
+
required = set(schema.get("required", []))
|
|
571
|
+
|
|
572
|
+
if not properties:
|
|
573
|
+
return "", False
|
|
574
|
+
|
|
575
|
+
# Process required params first
|
|
576
|
+
required_args, required_has_placeholder = _process_property_args(properties, required, filter_required=True)
|
|
577
|
+
|
|
578
|
+
# Then add optional params
|
|
579
|
+
optional_args, optional_has_placeholder = _process_property_args(properties, required, filter_required=False)
|
|
580
|
+
|
|
581
|
+
all_args = required_args + optional_args
|
|
582
|
+
uses_placeholder = required_has_placeholder or optional_has_placeholder
|
|
583
|
+
|
|
584
|
+
return ", ".join(all_args), uses_placeholder
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _get_doc_example_value(prop_schema: dict[str, Any], prop_name: str) -> str:
|
|
588
|
+
"""Get example value for documentation.
|
|
589
|
+
|
|
590
|
+
Prefers schema-provided examples, defaults, or enums.
|
|
591
|
+
Falls back to neutral placeholders.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
prop_schema: Property schema.
|
|
595
|
+
prop_name: Property name.
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
Example value as Python literal string.
|
|
599
|
+
"""
|
|
600
|
+
return example_value_from_schema(prop_schema, default_placeholder=DEFAULT_EXAMPLE_PLACEHOLDER)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _generate_all_docs(server_configs: list[ServerConfig]) -> dict[str, str]:
|
|
604
|
+
"""Generate all tool documentation files.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
server_configs: List of server configurations.
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
Dict mapping file path to content.
|
|
611
|
+
"""
|
|
612
|
+
docs: dict[str, str] = {}
|
|
613
|
+
|
|
614
|
+
sorted_configs = sorted(server_configs, key=lambda cfg: sanitize_module_name_with_reserved(cfg.name))
|
|
615
|
+
|
|
616
|
+
for cfg in sorted_configs:
|
|
617
|
+
safe_name = sanitize_module_name_with_reserved(cfg.name)
|
|
618
|
+
sorted_tools = sorted(cfg.tools, key=lambda tool: sanitize_function_name(tool["name"]))
|
|
619
|
+
|
|
620
|
+
for tool in sorted_tools:
|
|
621
|
+
func_name = sanitize_function_name(tool["name"])
|
|
622
|
+
doc_path = f"tools/docs/{safe_name}/{func_name}.md"
|
|
623
|
+
doc_content = _generate_tool_doc(tool)
|
|
624
|
+
docs[doc_path] = doc_content
|
|
625
|
+
|
|
626
|
+
return docs
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _generate_ptc_index(server_configs: list[ServerConfig]) -> str:
|
|
630
|
+
"""Generate the tools/ptc_index.json tool index.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
server_configs: List of server configurations with tools.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
JSON string of the tool index.
|
|
637
|
+
"""
|
|
638
|
+
packages: dict[str, Any] = {}
|
|
639
|
+
|
|
640
|
+
# Sort server configs by sanitized name for deterministic output
|
|
641
|
+
sorted_configs = sorted(server_configs, key=lambda c: sanitize_module_name_with_reserved(c.name))
|
|
642
|
+
|
|
643
|
+
for cfg in sorted_configs:
|
|
644
|
+
safe_name = sanitize_module_name_with_reserved(cfg.name)
|
|
645
|
+
|
|
646
|
+
# Sort tools by sanitized name
|
|
647
|
+
sorted_tools = sorted(cfg.tools, key=lambda t: sanitize_function_name(t["name"]))
|
|
648
|
+
|
|
649
|
+
tool_entries = []
|
|
650
|
+
for tool in sorted_tools:
|
|
651
|
+
func_name = sanitize_function_name(tool["name"])
|
|
652
|
+
schema = tool.get("input_schema", {})
|
|
653
|
+
|
|
654
|
+
# Use schema_to_params for consistent signatures (stubs and loaded tools)
|
|
655
|
+
signature = f"{func_name}({schema_to_params(schema)})"
|
|
656
|
+
|
|
657
|
+
tool_entries.append(
|
|
658
|
+
{
|
|
659
|
+
"name": func_name,
|
|
660
|
+
"signature": signature,
|
|
661
|
+
"doc_path": f"tools/docs/{safe_name}/{func_name}.md",
|
|
662
|
+
}
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
packages[safe_name] = {"tools": tool_entries}
|
|
666
|
+
|
|
667
|
+
index = {"packages": packages}
|
|
668
|
+
return json.dumps(index, indent=2, sort_keys=True)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from _typeshed import Incomplete
|
|
2
|
+
from aip_agents.mcp.client.base_mcp_client import BaseMCPClient as BaseMCPClient
|
|
3
|
+
from aip_agents.ptc.doc_gen import render_tool_doc as render_tool_doc
|
|
4
|
+
from aip_agents.ptc.naming import DEFAULT_EXAMPLE_PLACEHOLDER as DEFAULT_EXAMPLE_PLACEHOLDER, example_value_from_schema as example_value_from_schema, sanitize_function_name as sanitize_function_name, sanitize_module_name_with_reserved as sanitize_module_name_with_reserved, schema_to_params as schema_to_params
|
|
5
|
+
from aip_agents.ptc.payload import SandboxPayload as SandboxPayload
|
|
6
|
+
from aip_agents.ptc.template_utils import render_template as render_template
|
|
7
|
+
from aip_agents.utils.logger import get_logger as get_logger
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
logger: Incomplete
|
|
12
|
+
SUPPORTED_TRANSPORTS: Incomplete
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ServerConfig:
|
|
16
|
+
"""Extracted server configuration for sandbox payload.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
name: Server name identifier.
|
|
20
|
+
transport: Transport type (sse or streamable_http).
|
|
21
|
+
url: Server URL.
|
|
22
|
+
headers: HTTP headers for authentication.
|
|
23
|
+
allowed_tools: List of allowed tool names, or None for all.
|
|
24
|
+
tools: List of tool definitions from the server.
|
|
25
|
+
timeout: Request timeout in seconds.
|
|
26
|
+
"""
|
|
27
|
+
name: str
|
|
28
|
+
transport: str
|
|
29
|
+
url: str
|
|
30
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
31
|
+
allowed_tools: list[str] | None = ...
|
|
32
|
+
tools: list[dict[str, Any]] = field(default_factory=list)
|
|
33
|
+
timeout: float = ...
|
|
34
|
+
|
|
35
|
+
async def build_mcp_payload(mcp_client: BaseMCPClient, default_tool_timeout: float = 60.0) -> SandboxPayload:
|
|
36
|
+
"""Build MCP sandbox payload from MCP client configuration.
|
|
37
|
+
|
|
38
|
+
Extracts server configs, tools, and generates the necessary files
|
|
39
|
+
for the sandbox to execute PTC code.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
mcp_client: The MCP client with configured servers.
|
|
43
|
+
default_tool_timeout: Default timeout for tool calls in seconds.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
SandboxPayload containing files and env vars for the sandbox.
|
|
47
|
+
"""
|