aip-agents-binary 0.6.5__py3-none-macosx_13_0_arm64.whl → 0.6.6__py3-none-macosx_13_0_arm64.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 (44) hide show
  1. aip_agents/agent/langgraph_react_agent.py +66 -19
  2. aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
  3. aip_agents/examples/hello_world_ptc_custom_tools.pyi +7 -0
  4. aip_agents/examples/tools/multiply_tool.py +43 -0
  5. aip_agents/examples/tools/multiply_tool.pyi +18 -0
  6. aip_agents/ptc/__init__.py +42 -3
  7. aip_agents/ptc/__init__.pyi +5 -1
  8. aip_agents/ptc/custom_tools.py +473 -0
  9. aip_agents/ptc/custom_tools.pyi +184 -0
  10. aip_agents/ptc/custom_tools_payload.py +400 -0
  11. aip_agents/ptc/custom_tools_payload.pyi +31 -0
  12. aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
  13. aip_agents/ptc/custom_tools_templates/__init__.pyi +0 -0
  14. aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +23 -0
  15. aip_agents/ptc/custom_tools_templates/custom_init.py.template +15 -0
  16. aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +60 -0
  17. aip_agents/ptc/custom_tools_templates/custom_registry.py.template +87 -0
  18. aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
  19. aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +19 -0
  20. aip_agents/ptc/exceptions.py +18 -0
  21. aip_agents/ptc/exceptions.pyi +15 -0
  22. aip_agents/ptc/executor.py +151 -33
  23. aip_agents/ptc/executor.pyi +34 -8
  24. aip_agents/ptc/naming.py +13 -1
  25. aip_agents/ptc/naming.pyi +9 -0
  26. aip_agents/ptc/prompt_builder.py +118 -16
  27. aip_agents/ptc/prompt_builder.pyi +12 -8
  28. aip_agents/ptc/sandbox_bridge.py +206 -8
  29. aip_agents/ptc/sandbox_bridge.pyi +18 -5
  30. aip_agents/ptc/tool_def_helpers.py +101 -0
  31. aip_agents/ptc/tool_def_helpers.pyi +38 -0
  32. aip_agents/ptc/tool_enrichment.py +163 -0
  33. aip_agents/ptc/tool_enrichment.pyi +60 -0
  34. aip_agents/sandbox/defaults.py +197 -1
  35. aip_agents/sandbox/defaults.pyi +28 -0
  36. aip_agents/sandbox/e2b_runtime.py +28 -0
  37. aip_agents/sandbox/e2b_runtime.pyi +7 -1
  38. aip_agents/sandbox/template_builder.py +2 -2
  39. aip_agents/tools/execute_ptc_code.py +59 -10
  40. aip_agents/tools/execute_ptc_code.pyi +5 -5
  41. {aip_agents_binary-0.6.5.dist-info → aip_agents_binary-0.6.6.dist-info}/METADATA +3 -3
  42. {aip_agents_binary-0.6.5.dist-info → aip_agents_binary-0.6.6.dist-info}/RECORD +44 -24
  43. {aip_agents_binary-0.6.5.dist-info → aip_agents_binary-0.6.6.dist-info}/WHEEL +0 -0
  44. {aip_agents_binary-0.6.5.dist-info → aip_agents_binary-0.6.6.dist-info}/top_level.txt +0 -0
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
  from dataclasses import dataclass
13
13
  from typing import TYPE_CHECKING, Any, Literal
14
14
 
15
+ from aip_agents.ptc.custom_tools import PTCCustomToolConfig
15
16
  from aip_agents.ptc.naming import (
16
17
  example_value_from_schema,
17
18
  sanitize_function_name,
@@ -53,7 +54,9 @@ PTC_USAGE_RULES = """## PTC (Programmatic Tool Calling) Usage
53
54
 
54
55
  When using `execute_ptc_code`, follow these rules:
55
56
 
56
- 1. **Import pattern**: `from tools.<server> import <tool_name>`
57
+ 1. **Import patterns**:
58
+ - MCP tools: `from tools.<server> import <tool_name>`
59
+ - Custom tools: `from tools.custom import <tool_name>`
57
60
  2. **Output**: Only `print()` output is returned to you. Always print results.
58
61
  3. **Parameter names**: All parameters are lowercase with underscores.
59
62
  - Example: `userId` becomes `userid`, `user-id` becomes `user_id`
@@ -63,19 +66,22 @@ When using `execute_ptc_code`, follow these rules:
63
66
  def build_ptc_prompt(
64
67
  mcp_client: BaseMCPClient | None = None,
65
68
  config: PromptConfig | None = None,
69
+ custom_tools_config: PTCCustomToolConfig | None = None,
66
70
  ) -> str:
67
- """Build PTC usage guidance prompt from MCP configuration.
71
+ """Build PTC usage guidance prompt from MCP and custom tool configurations.
68
72
 
69
73
  Generates a short usage block that includes:
70
- - The import pattern: MCP (`from tools.<server> import <tool>`)
74
+ - The import patterns: MCP (`from tools.<server> import <tool>`) and
75
+ custom (`from tools.custom import <tool>`)
71
76
  - Rule: use `print()`; only printed output returns
72
77
  - Rule: parameter names are sanitized to lowercase/underscored
73
78
  - Prompt mode content (minimal/index/full)
74
79
  - Examples based on the resolved prompt mode
75
80
 
76
81
  Args:
77
- mcp_client: The MCP client with configured servers.
82
+ mcp_client: The MCP client with configured servers. Can be None if only custom tools.
78
83
  config: Prompt configuration. If None, uses default PromptConfig.
84
+ custom_tools_config: Optional custom LangChain tools configuration.
79
85
 
80
86
  Returns:
81
87
  PTC usage guidance prompt string.
@@ -90,19 +96,24 @@ def build_ptc_prompt(
90
96
  tools = _get_server_tools(mcp_client, server_name)
91
97
  server_infos.append({"name": server_name, "tools": tools})
92
98
 
99
+ # Collect custom tool info
100
+ custom_tool_infos: list[dict[str, Any]] = []
101
+ if custom_tools_config and custom_tools_config.enabled and custom_tools_config.tools:
102
+ custom_tool_infos = _get_custom_tool_infos(custom_tools_config)
103
+
93
104
  # Check if we have any tools
94
- if not server_infos:
105
+ if not server_infos and not custom_tool_infos:
95
106
  return _build_placeholder_prompt()
96
107
 
97
108
  # Resolve mode and build appropriate prompt
98
- resolved_mode = _resolve_mode(config, server_infos)
109
+ resolved_mode = _resolve_mode(config, server_infos, custom_tool_infos)
99
110
 
100
111
  if resolved_mode == "minimal":
101
- return _build_minimal_prompt(server_infos, config.include_example)
112
+ return _build_minimal_prompt(server_infos, config.include_example, custom_tool_infos)
102
113
  elif resolved_mode == "index":
103
- return _build_index_prompt(server_infos, config.include_example)
114
+ return _build_index_prompt(server_infos, config.include_example, custom_tool_infos)
104
115
  else: # full
105
- return _build_full_prompt(server_infos, config.include_example)
116
+ return _build_full_prompt(server_infos, config.include_example, custom_tool_infos)
106
117
 
107
118
 
108
119
  def _get_server_tools(
@@ -187,29 +198,61 @@ def _get_allowed_tools_from_config(mcp_client: BaseMCPClient, server_name: str)
187
198
  return None
188
199
 
189
200
 
201
+ def _get_custom_tool_infos(custom_tools_config: PTCCustomToolConfig) -> list[dict[str, Any]]:
202
+ """Get tool info dicts from custom tools configuration.
203
+
204
+ Args:
205
+ custom_tools_config: Custom tools configuration.
206
+
207
+ Returns:
208
+ List of tool info dicts with name, description, and input_schema.
209
+ """
210
+ tools: list[dict[str, Any]] = []
211
+ for tool_def in custom_tools_config.tools:
212
+ name = tool_def.get("name", "")
213
+ description = tool_def.get("description", "")
214
+ input_schema = tool_def.get("input_schema", {"type": "object", "properties": {}})
215
+
216
+ tools.append(
217
+ {
218
+ "name": name,
219
+ "description": description,
220
+ "input_schema": input_schema,
221
+ "stub": not description and not input_schema.get("properties"),
222
+ }
223
+ )
224
+ return tools
225
+
226
+
190
227
  def _count_total_tools(
191
228
  server_infos: list[dict[str, Any]],
229
+ custom_tool_infos: list[dict[str, Any]] | None = None,
192
230
  ) -> int:
193
- """Count total tools across all servers.
231
+ """Count total tools across all servers and custom tools.
194
232
 
195
233
  Args:
196
234
  server_infos: List of server info dicts with name and tools.
235
+ custom_tool_infos: List of custom tool info dicts.
197
236
 
198
237
  Returns:
199
238
  Total tool count.
200
239
  """
201
- return sum(len(info.get("tools", [])) for info in server_infos)
240
+ mcp_count = sum(len(info.get("tools", [])) for info in server_infos)
241
+ custom_count = len(custom_tool_infos) if custom_tool_infos else 0
242
+ return mcp_count + custom_count
202
243
 
203
244
 
204
245
  def _resolve_mode(
205
246
  config: PromptConfig,
206
247
  server_infos: list[dict[str, Any]],
248
+ custom_tool_infos: list[dict[str, Any]] | None = None,
207
249
  ) -> PromptMode:
208
250
  """Resolve auto mode to concrete mode based on tool count.
209
251
 
210
252
  Args:
211
253
  config: Prompt configuration.
212
254
  server_infos: List of server info dicts.
255
+ custom_tool_infos: List of custom tool info dicts.
213
256
 
214
257
  Returns:
215
258
  Resolved mode (minimal, index, or full).
@@ -217,7 +260,7 @@ def _resolve_mode(
217
260
  if config.mode != "auto":
218
261
  return config.mode
219
262
 
220
- total_tools = _count_total_tools(server_infos)
263
+ total_tools = _count_total_tools(server_infos, custom_tool_infos)
221
264
  if total_tools == 0 or total_tools > config.auto_threshold:
222
265
  return "minimal"
223
266
  return "full"
@@ -243,12 +286,14 @@ print(doc["doc"])"""
243
286
  def _build_minimal_prompt(
244
287
  server_infos: list[dict[str, Any]],
245
288
  include_example: bool,
289
+ custom_tool_infos: list[dict[str, Any]] | None = None,
246
290
  ) -> str:
247
291
  """Build minimal prompt with rules and package list only.
248
292
 
249
293
  Args:
250
294
  server_infos: List of server info dicts with name and tools.
251
295
  include_example: Whether to include discovery example.
296
+ custom_tool_infos: List of custom tool info dicts.
252
297
 
253
298
  Returns:
254
299
  Minimal PTC usage prompt.
@@ -265,6 +310,10 @@ def _build_minimal_prompt(
265
310
  for pkg in package_names:
266
311
  lines.append(f"- `tools.{pkg}`")
267
312
 
313
+ # Add custom tools package if present
314
+ if custom_tool_infos:
315
+ lines.append("- `tools.custom`")
316
+
268
317
  lines.append("")
269
318
  lines.append("Use `tools.ptc_helper` to discover available tools and their signatures.")
270
319
 
@@ -286,12 +335,14 @@ def _build_minimal_prompt(
286
335
  def _build_index_prompt(
287
336
  server_infos: list[dict[str, Any]],
288
337
  include_example: bool,
338
+ custom_tool_infos: list[dict[str, Any]] | None = None,
289
339
  ) -> str:
290
340
  """Build index prompt with rules, package list, and tool names.
291
341
 
292
342
  Args:
293
343
  server_infos: List of server info dicts with name and tools.
294
344
  include_example: Whether to include discovery example.
345
+ custom_tool_infos: List of custom tool info dicts.
295
346
 
296
347
  Returns:
297
348
  Index PTC usage prompt.
@@ -316,6 +367,14 @@ def _build_index_prompt(
316
367
  lines.append(f" Tools: {', '.join(tool_names)}")
317
368
  lines.append("")
318
369
 
370
+ # Add custom tools section if present
371
+ if custom_tool_infos:
372
+ lines.append("**`tools.custom`**")
373
+ sorted_custom = sorted(custom_tool_infos, key=lambda t: sanitize_function_name(t["name"]))
374
+ tool_names = [sanitize_function_name(t["name"]) for t in sorted_custom]
375
+ lines.append(f" Tools: {', '.join(tool_names)}")
376
+ lines.append("")
377
+
319
378
  lines.append("Use `tools.ptc_helper` to get tool signatures and descriptions.")
320
379
 
321
380
  if include_example:
@@ -336,12 +395,14 @@ def _build_index_prompt(
336
395
  def _build_full_prompt(
337
396
  server_infos: list[dict[str, Any]],
338
397
  include_example: bool,
398
+ custom_tool_infos: list[dict[str, Any]] | None = None,
339
399
  ) -> str:
340
400
  """Build full prompt with rules, signatures, and descriptions.
341
401
 
342
402
  Args:
343
403
  server_infos: List of server info dicts with name and tools.
344
404
  include_example: Whether to include real tool example.
405
+ custom_tool_infos: List of custom tool info dicts.
345
406
 
346
407
  Returns:
347
408
  Full PTC usage prompt.
@@ -377,8 +438,28 @@ def _build_full_prompt(
377
438
 
378
439
  lines.append("")
379
440
 
441
+ # Add custom tools section if present
442
+ if custom_tool_infos:
443
+ lines.append("**Custom Tools** (from `tools.custom`)")
444
+ lines.append("")
445
+
446
+ sorted_custom = sorted(custom_tool_infos, key=lambda t: sanitize_function_name(t["name"]))
447
+
448
+ for tool in sorted_custom:
449
+ func_name = sanitize_function_name(tool["name"])
450
+ schema = tool.get("input_schema", {})
451
+ params = schema_to_params(schema)
452
+ raw_desc = tool.get("description", "")
453
+ desc = raw_desc[:120]
454
+ if raw_desc and len(raw_desc) > 120:
455
+ desc += "..."
456
+
457
+ lines.append(f"- `{func_name}({params})`: {desc}")
458
+
459
+ lines.append("")
460
+
380
461
  if include_example:
381
- example = _build_example(server_infos)
462
+ example = _build_example(server_infos, custom_tool_infos)
382
463
  lines.extend(
383
464
  [
384
465
  "### Example",
@@ -406,15 +487,18 @@ def _build_prompt_from_servers(server_infos: list[dict[str, Any]]) -> str:
406
487
 
407
488
  def _build_example(
408
489
  server_infos: list[dict[str, Any]],
490
+ custom_tool_infos: list[dict[str, Any]] | None = None,
409
491
  ) -> str:
410
492
  """Build an example code snippet using the first available tool.
411
493
 
412
494
  Args:
413
495
  server_infos: List of server info dicts.
496
+ custom_tool_infos: List of custom tool info dicts.
414
497
 
415
498
  Returns:
416
499
  Example code string.
417
500
  """
501
+ # Try MCP tools first
418
502
  if server_infos:
419
503
  sorted_servers = sorted(server_infos, key=lambda info: sanitize_module_name_with_reserved(info["name"]))
420
504
  for server in sorted_servers:
@@ -427,6 +511,17 @@ def _build_example(
427
511
  args_str = _build_example_args_from_schema(tool.get("input_schema", {}))
428
512
  return f"""from tools.{safe_server} import {func_name}
429
513
 
514
+ result = {func_name}({args_str})
515
+ print(result)"""
516
+
517
+ # Fall back to custom tools
518
+ if custom_tool_infos:
519
+ sorted_custom = sorted(custom_tool_infos, key=lambda t: sanitize_function_name(t["name"]))
520
+ tool = sorted_custom[0]
521
+ func_name = sanitize_function_name(tool["name"])
522
+ args_str = _build_example_args_from_schema(tool.get("input_schema", {}))
523
+ return f"""from tools.custom import {func_name}
524
+
430
525
  result = {func_name}({args_str})
431
526
  print(result)"""
432
527
 
@@ -534,15 +629,17 @@ def _build_server_hash_part(mcp_client: BaseMCPClient, server_name: str) -> str:
534
629
  def compute_ptc_prompt_hash(
535
630
  mcp_client: BaseMCPClient | None = None,
536
631
  config: PromptConfig | None = None,
632
+ custom_tools_config: PTCCustomToolConfig | None = None,
537
633
  ) -> str:
538
- """Compute a hash of the MCP configuration for change detection.
634
+ """Compute a hash of the MCP and custom tool configuration for change detection.
539
635
 
540
- Includes PromptConfig fields and allowed_tools in hash computation
636
+ Includes PromptConfig fields, allowed_tools, and custom tools in hash computation
541
637
  so prompt updates re-sync correctly when configuration changes.
542
638
 
543
639
  Args:
544
- mcp_client: MCP client instance.
640
+ mcp_client: MCP client instance. Can be None if only custom tools.
545
641
  config: Prompt configuration. If None, uses default PromptConfig.
642
+ custom_tools_config: Optional custom LangChain tools configuration.
546
643
 
547
644
  Returns:
548
645
  Hash string representing current configuration.
@@ -563,6 +660,11 @@ def compute_ptc_prompt_hash(
563
660
  for server_name in sorted(mcp_client.servers.keys()):
564
661
  parts.append(_build_server_hash_part(mcp_client, server_name))
565
662
 
663
+ # Add custom tools parts
664
+ if custom_tools_config and custom_tools_config.enabled and custom_tools_config.tools:
665
+ custom_tool_names = sorted(sanitize_function_name(t.get("name", "")) for t in custom_tools_config.tools)
666
+ parts.append(f"custom:{','.join(custom_tool_names)}")
667
+
566
668
  # Return empty hash if no tools configured
567
669
  if len(parts) == 1:
568
670
  return ""
@@ -1,5 +1,6 @@
1
1
  from _typeshed import Incomplete
2
2
  from aip_agents.mcp.client.base_mcp_client import BaseMCPClient as BaseMCPClient
3
+ from aip_agents.ptc.custom_tools import PTCCustomToolConfig as PTCCustomToolConfig
3
4
  from aip_agents.ptc.naming import 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, sanitize_param_name as sanitize_param_name, schema_to_params as schema_to_params
4
5
  from aip_agents.utils.logger import get_logger as get_logger
5
6
  from dataclasses import dataclass
@@ -23,32 +24,35 @@ class PromptConfig:
23
24
 
24
25
  PTC_USAGE_RULES: str
25
26
 
26
- def build_ptc_prompt(mcp_client: BaseMCPClient | None = None, config: PromptConfig | None = None) -> str:
27
- """Build PTC usage guidance prompt from MCP configuration.
27
+ def build_ptc_prompt(mcp_client: BaseMCPClient | None = None, config: PromptConfig | None = None, custom_tools_config: PTCCustomToolConfig | None = None) -> str:
28
+ """Build PTC usage guidance prompt from MCP and custom tool configurations.
28
29
 
29
30
  Generates a short usage block that includes:
30
- - The import pattern: MCP (`from tools.<server> import <tool>`)
31
+ - The import patterns: MCP (`from tools.<server> import <tool>`) and
32
+ custom (`from tools.custom import <tool>`)
31
33
  - Rule: use `print()`; only printed output returns
32
34
  - Rule: parameter names are sanitized to lowercase/underscored
33
35
  - Prompt mode content (minimal/index/full)
34
36
  - Examples based on the resolved prompt mode
35
37
 
36
38
  Args:
37
- mcp_client: The MCP client with configured servers.
39
+ mcp_client: The MCP client with configured servers. Can be None if only custom tools.
38
40
  config: Prompt configuration. If None, uses default PromptConfig.
41
+ custom_tools_config: Optional custom LangChain tools configuration.
39
42
 
40
43
  Returns:
41
44
  PTC usage guidance prompt string.
42
45
  """
43
- def compute_ptc_prompt_hash(mcp_client: BaseMCPClient | None = None, config: PromptConfig | None = None) -> str:
44
- """Compute a hash of the MCP configuration for change detection.
46
+ def compute_ptc_prompt_hash(mcp_client: BaseMCPClient | None = None, config: PromptConfig | None = None, custom_tools_config: PTCCustomToolConfig | None = None) -> str:
47
+ """Compute a hash of the MCP and custom tool configuration for change detection.
45
48
 
46
- Includes PromptConfig fields and allowed_tools in hash computation
49
+ Includes PromptConfig fields, allowed_tools, and custom tools in hash computation
47
50
  so prompt updates re-sync correctly when configuration changes.
48
51
 
49
52
  Args:
50
- mcp_client: MCP client instance.
53
+ mcp_client: MCP client instance. Can be None if only custom tools.
51
54
  config: Prompt configuration. If None, uses default PromptConfig.
55
+ custom_tools_config: Optional custom LangChain tools configuration.
52
56
 
53
57
  Returns:
54
58
  Hash string representing current configuration.
@@ -1,7 +1,7 @@
1
- """Top-level PTC Sandbox Bridge (MCP-only).
1
+ """Top-level PTC Sandbox Bridge.
2
2
 
3
3
  This module provides the unified entry point for building sandbox payloads
4
- for MCP tools.
4
+ across different tool sources (MCP, custom tools, etc.).
5
5
 
6
6
  Authors:
7
7
  Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
@@ -9,37 +9,225 @@ Authors:
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import json
13
+ from typing import Any
14
+
12
15
  from aip_agents.mcp.client.base_mcp_client import BaseMCPClient
16
+ from aip_agents.ptc.custom_tools import (
17
+ PTCCustomToolConfig,
18
+ validate_custom_tool_config,
19
+ )
20
+ from aip_agents.ptc.custom_tools_payload import build_custom_tools_payload
21
+ from aip_agents.ptc.doc_gen import (
22
+ render_tool_doc,
23
+ )
24
+ from aip_agents.ptc.exceptions import PTCPayloadConflictError
13
25
  from aip_agents.ptc.mcp.sandbox_bridge import build_mcp_payload
26
+ from aip_agents.ptc.naming import sanitize_function_name, schema_to_params
14
27
  from aip_agents.ptc.payload import SandboxPayload
28
+ from aip_agents.ptc.ptc_helper import _generate_ptc_helper_module
15
29
 
16
30
 
17
31
  async def build_sandbox_payload(
18
32
  mcp_client: BaseMCPClient | None = None,
19
33
  default_tool_timeout: float = 60.0,
34
+ custom_tools_config: PTCCustomToolConfig | None = None,
35
+ tool_configs: dict[str, dict] | None = None,
20
36
  ) -> SandboxPayload:
21
- """Build sandbox payload from MCP client configuration (MCP-only).
37
+ """Build sandbox payload from all configured tool sources.
38
+
39
+ Composes MCP and custom LangChain tool payloads into a single payload.
22
40
 
23
41
  Args:
24
- mcp_client: The MCP client with configured servers.
42
+ mcp_client: The MCP client with configured servers. Can be None if only custom tools.
25
43
  default_tool_timeout: Default timeout for tool calls in seconds.
44
+ custom_tools_config: Optional custom LangChain tools configuration.
45
+ tool_configs: Optional per-tool config values for custom tools.
26
46
 
27
47
  Returns:
28
48
  SandboxPayload containing files and env vars for the sandbox.
29
49
  """
30
50
  # Build MCP payload
51
+ mcp_payload = SandboxPayload()
31
52
  if mcp_client:
32
- return await build_mcp_payload(mcp_client, default_tool_timeout)
33
- return SandboxPayload()
53
+ mcp_payload = await build_mcp_payload(mcp_client, default_tool_timeout)
54
+
55
+ # Build custom tools payload if enabled
56
+ custom_payload = SandboxPayload()
57
+ if custom_tools_config and custom_tools_config.enabled:
58
+ # Validate config before building payload (fail fast)
59
+ validate_custom_tool_config(custom_tools_config)
60
+ result = build_custom_tools_payload(custom_tools_config, tool_configs)
61
+ custom_payload = result.payload
62
+
63
+ # Check for conflicts before merging payloads
64
+ _check_payload_conflicts(mcp_payload, custom_payload)
65
+
66
+ # Merge payloads (custom tools files should not conflict with MCP files)
67
+ merged = SandboxPayload()
68
+ merged.files.update(mcp_payload.files)
69
+ merged.files.update(custom_payload.files)
70
+ merged.per_run_files.update(mcp_payload.per_run_files)
71
+ merged.per_run_files.update(custom_payload.per_run_files)
72
+ merged.env.update(mcp_payload.env)
73
+ merged.env.update(custom_payload.env)
74
+
75
+ # Merge custom tools into ptc_index.json
76
+ if custom_tools_config and custom_tools_config.enabled and custom_tools_config.tools:
77
+ merged.files["tools/ptc_index.json"] = _merge_custom_tools_into_index(
78
+ merged.files.get("tools/ptc_index.json"),
79
+ custom_tools_config,
80
+ )
81
+ if "tools/ptc_helper.py" not in merged.files:
82
+ merged.files["tools/ptc_helper.py"] = _generate_ptc_helper_module()
83
+ # Generate documentation for custom tools
84
+ custom_docs = _generate_custom_tool_docs(custom_tools_config)
85
+ merged.files.update(custom_docs)
86
+
87
+ return merged
88
+
89
+
90
+ def _check_payload_conflicts(
91
+ mcp_payload: SandboxPayload,
92
+ custom_payload: SandboxPayload,
93
+ ) -> None:
94
+ """Check for conflicts between MCP and custom tool payloads.
95
+
96
+ Args:
97
+ mcp_payload: Payload from MCP tool configuration.
98
+ custom_payload: Payload from custom tool configuration.
99
+
100
+ Raises:
101
+ PTCPayloadConflictError: If any conflicts are detected in files,
102
+ per-run files, or environment variables.
103
+ """
104
+ mcp_files = set(mcp_payload.files.keys())
105
+ custom_files = set(custom_payload.files.keys())
106
+ file_conflicts = mcp_files & custom_files
107
+
108
+ mcp_per_run = set(mcp_payload.per_run_files.keys())
109
+ custom_per_run = set(custom_payload.per_run_files.keys())
110
+ per_run_conflicts = mcp_per_run & custom_per_run
111
+
112
+ mcp_env_keys = set(mcp_payload.env.keys())
113
+ custom_env_keys = set(custom_payload.env.keys())
114
+ env_conflicts = mcp_env_keys & custom_env_keys
115
+
116
+ all_conflicts = file_conflicts | per_run_conflicts | env_conflicts
117
+ if all_conflicts:
118
+ raise PTCPayloadConflictError(
119
+ f"Conflicts detected when merging MCP and custom tool payloads. "
120
+ f"Files: {sorted(file_conflicts)}, "
121
+ f"Per-run files: {sorted(per_run_conflicts)}, "
122
+ f"Environment variables: {sorted(env_conflicts)}",
123
+ conflicts=all_conflicts,
124
+ )
125
+
126
+
127
+ def _merge_custom_tools_into_index(
128
+ existing_index_json: str | None,
129
+ custom_tools_config: PTCCustomToolConfig,
130
+ ) -> str:
131
+ """Merge custom tools into the ptc_index.json.
132
+
133
+ Args:
134
+ existing_index_json: Existing ptc_index.json content (may be None).
135
+ custom_tools_config: Custom tools configuration.
136
+
137
+ Returns:
138
+ Updated JSON string with custom tools included.
139
+ """
140
+ # Parse existing index or create new one
141
+ if existing_index_json:
142
+ index = json.loads(existing_index_json)
143
+ else:
144
+ index = {"packages": {}}
145
+
146
+ # Add custom tools package
147
+ tool_entries = []
148
+ for tool_def in sorted(custom_tools_config.tools, key=lambda t: sanitize_function_name(t.get("name", ""))):
149
+ name = tool_def.get("name", "")
150
+ func_name = sanitize_function_name(name)
151
+ schema = tool_def.get("input_schema", {"type": "object", "properties": {}})
152
+ signature = f"{func_name}({schema_to_params(schema)})"
153
+
154
+ tool_entries.append(
155
+ {
156
+ "name": func_name,
157
+ "signature": signature,
158
+ "doc_path": f"tools/docs/custom/{func_name}.md",
159
+ }
160
+ )
34
161
 
162
+ index["packages"]["custom"] = {"tools": tool_entries}
35
163
 
36
- def wrap_ptc_code(code: str) -> str:
37
- """Wrap user PTC code with necessary imports and setup (MCP-only).
164
+ return json.dumps(index, indent=2, sort_keys=True)
165
+
166
+
167
+ def _generate_custom_tool_docs(custom_tools_config: PTCCustomToolConfig) -> dict[str, str]:
168
+ """Generate documentation files for custom tools.
169
+
170
+ Args:
171
+ custom_tools_config: Custom tools configuration.
172
+
173
+ Returns:
174
+ Dict mapping file path to content.
175
+ """
176
+ docs: dict[str, str] = {}
177
+
178
+ for tool_def in sorted(custom_tools_config.tools, key=lambda t: sanitize_function_name(t.get("name", ""))):
179
+ name = tool_def.get("name", "")
180
+ func_name = sanitize_function_name(name)
181
+ description = tool_def.get("description", "")
182
+ schema = tool_def.get("input_schema", {"type": "object", "properties": {}})
183
+
184
+ doc_content = _generate_tool_doc_content(func_name, description, schema)
185
+ docs[f"tools/docs/custom/{func_name}.md"] = doc_content
186
+
187
+ return docs
188
+
189
+
190
+ def _generate_tool_doc_content(
191
+ func_name: str,
192
+ description: str,
193
+ schema: dict[str, Any],
194
+ ) -> str:
195
+ """Generate markdown documentation for a single tool.
196
+
197
+ Args:
198
+ func_name: Sanitized function name.
199
+ description: Tool description.
200
+ schema: JSON schema for tool input.
201
+
202
+ Returns:
203
+ Markdown documentation string.
204
+ """
205
+ # Use schema_to_params for consistent signatures
206
+ params = schema_to_params(schema)
207
+ signature = f"{func_name}({params})"
208
+
209
+ example_code = f"from tools.custom import {func_name}\nresult = {func_name}(...)\nprint(result)"
210
+
211
+ return render_tool_doc(
212
+ func_name=func_name,
213
+ signature=signature,
214
+ description=description,
215
+ schema=schema,
216
+ example_code=example_code,
217
+ )
218
+
219
+
220
+ def wrap_ptc_code(code: str, include_packages_path: bool = False) -> str:
221
+ """Wrap user PTC code with necessary imports and setup.
38
222
 
39
223
  This prepends sys.path setup to ensure the tools package is importable.
224
+ When custom tools with bundled package sources are enabled, also adds
225
+ the packages/ directory to sys.path.
40
226
 
41
227
  Args:
42
228
  code: User-provided Python code.
229
+ include_packages_path: If True, also add packages/ dir to sys.path
230
+ for bundled package sources from custom LangChain tools.
43
231
 
44
232
  Returns:
45
233
  Wrapped code ready for sandbox execution.
@@ -52,7 +240,17 @@ import os
52
240
  _tools_dir = os.path.dirname(os.path.abspath(__file__)) if "__file__" in dir() else os.getcwd()
53
241
  if _tools_dir not in sys.path:
54
242
  sys.path.insert(0, _tools_dir)
243
+ """
244
+
245
+ if include_packages_path:
246
+ preamble += """
247
+ # Add packages directory to path for bundled custom tool sources
248
+ _packages_dir = os.path.join(_tools_dir, "packages")
249
+ if os.path.isdir(_packages_dir) and _packages_dir not in sys.path:
250
+ sys.path.insert(0, _packages_dir)
251
+ """
55
252
 
253
+ preamble += """
56
254
  # User code below
57
255
  """
58
256
  return preamble + code
@@ -1,24 +1,37 @@
1
1
  from aip_agents.mcp.client.base_mcp_client import BaseMCPClient as BaseMCPClient
2
+ from aip_agents.ptc.custom_tools import PTCCustomToolConfig as PTCCustomToolConfig, validate_custom_tool_config as validate_custom_tool_config
3
+ from aip_agents.ptc.custom_tools_payload import build_custom_tools_payload as build_custom_tools_payload
4
+ from aip_agents.ptc.doc_gen import render_tool_doc as render_tool_doc
5
+ from aip_agents.ptc.exceptions import PTCPayloadConflictError as PTCPayloadConflictError
2
6
  from aip_agents.ptc.mcp.sandbox_bridge import build_mcp_payload as build_mcp_payload
7
+ from aip_agents.ptc.naming import sanitize_function_name as sanitize_function_name, schema_to_params as schema_to_params
3
8
  from aip_agents.ptc.payload import SandboxPayload as SandboxPayload
4
9
 
5
- async def build_sandbox_payload(mcp_client: BaseMCPClient | None = None, default_tool_timeout: float = 60.0) -> SandboxPayload:
6
- """Build sandbox payload from MCP client configuration (MCP-only).
10
+ async def build_sandbox_payload(mcp_client: BaseMCPClient | None = None, default_tool_timeout: float = 60.0, custom_tools_config: PTCCustomToolConfig | None = None, tool_configs: dict[str, dict] | None = None) -> SandboxPayload:
11
+ """Build sandbox payload from all configured tool sources.
12
+
13
+ Composes MCP and custom LangChain tool payloads into a single payload.
7
14
 
8
15
  Args:
9
- mcp_client: The MCP client with configured servers.
16
+ mcp_client: The MCP client with configured servers. Can be None if only custom tools.
10
17
  default_tool_timeout: Default timeout for tool calls in seconds.
18
+ custom_tools_config: Optional custom LangChain tools configuration.
19
+ tool_configs: Optional per-tool config values for custom tools.
11
20
 
12
21
  Returns:
13
22
  SandboxPayload containing files and env vars for the sandbox.
14
23
  """
15
- def wrap_ptc_code(code: str) -> str:
16
- """Wrap user PTC code with necessary imports and setup (MCP-only).
24
+ def wrap_ptc_code(code: str, include_packages_path: bool = False) -> str:
25
+ """Wrap user PTC code with necessary imports and setup.
17
26
 
18
27
  This prepends sys.path setup to ensure the tools package is importable.
28
+ When custom tools with bundled package sources are enabled, also adds
29
+ the packages/ directory to sys.path.
19
30
 
20
31
  Args:
21
32
  code: User-provided Python code.
33
+ include_packages_path: If True, also add packages/ dir to sys.path
34
+ for bundled package sources from custom LangChain tools.
22
35
 
23
36
  Returns:
24
37
  Wrapped code ready for sandbox execution.