aip-agents-binary 0.6.4__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 (48) hide show
  1. aip_agents/agent/__init__.py +44 -4
  2. aip_agents/agent/langgraph_react_agent.py +66 -19
  3. aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
  4. aip_agents/examples/hello_world_ptc_custom_tools.pyi +7 -0
  5. aip_agents/examples/tools/multiply_tool.py +43 -0
  6. aip_agents/examples/tools/multiply_tool.pyi +18 -0
  7. aip_agents/mcp/client/__init__.py +38 -2
  8. aip_agents/ptc/__init__.py +42 -3
  9. aip_agents/ptc/__init__.pyi +5 -1
  10. aip_agents/ptc/custom_tools.py +473 -0
  11. aip_agents/ptc/custom_tools.pyi +184 -0
  12. aip_agents/ptc/custom_tools_payload.py +400 -0
  13. aip_agents/ptc/custom_tools_payload.pyi +31 -0
  14. aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
  15. aip_agents/ptc/custom_tools_templates/__init__.pyi +0 -0
  16. aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +23 -0
  17. aip_agents/ptc/custom_tools_templates/custom_init.py.template +15 -0
  18. aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +60 -0
  19. aip_agents/ptc/custom_tools_templates/custom_registry.py.template +87 -0
  20. aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
  21. aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +19 -0
  22. aip_agents/ptc/exceptions.py +18 -0
  23. aip_agents/ptc/exceptions.pyi +15 -0
  24. aip_agents/ptc/executor.py +151 -33
  25. aip_agents/ptc/executor.pyi +34 -8
  26. aip_agents/ptc/naming.py +13 -1
  27. aip_agents/ptc/naming.pyi +9 -0
  28. aip_agents/ptc/prompt_builder.py +118 -16
  29. aip_agents/ptc/prompt_builder.pyi +12 -8
  30. aip_agents/ptc/sandbox_bridge.py +206 -8
  31. aip_agents/ptc/sandbox_bridge.pyi +18 -5
  32. aip_agents/ptc/tool_def_helpers.py +101 -0
  33. aip_agents/ptc/tool_def_helpers.pyi +38 -0
  34. aip_agents/ptc/tool_enrichment.py +163 -0
  35. aip_agents/ptc/tool_enrichment.pyi +60 -0
  36. aip_agents/sandbox/defaults.py +197 -1
  37. aip_agents/sandbox/defaults.pyi +28 -0
  38. aip_agents/sandbox/e2b_runtime.py +28 -0
  39. aip_agents/sandbox/e2b_runtime.pyi +7 -1
  40. aip_agents/sandbox/template_builder.py +2 -2
  41. aip_agents/sentry/sentry.py +29 -8
  42. aip_agents/sentry/sentry.pyi +3 -2
  43. aip_agents/tools/execute_ptc_code.py +59 -10
  44. aip_agents/tools/execute_ptc_code.pyi +5 -5
  45. {aip_agents_binary-0.6.4.dist-info → aip_agents_binary-0.6.6.dist-info}/METADATA +3 -3
  46. {aip_agents_binary-0.6.4.dist-info → aip_agents_binary-0.6.6.dist-info}/RECORD +48 -28
  47. {aip_agents_binary-0.6.4.dist-info → aip_agents_binary-0.6.6.dist-info}/WHEEL +0 -0
  48. {aip_agents_binary-0.6.4.dist-info → aip_agents_binary-0.6.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,101 @@
1
+ """Helper functions for constructing PTC tool definitions.
2
+
3
+ Author: Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
4
+
5
+ These helpers provide a cleaner API for defining custom tools in PTCCustomToolConfig.
6
+ They return properly-typed dictionaries that can be used in PTCCustomToolConfig.tools.
7
+
8
+ Example:
9
+ >>> from aip_agents.ptc import package_tool, file_tool, PTCCustomToolConfig
10
+ >>> config = PTCCustomToolConfig(
11
+ ... enabled=True,
12
+ ... tools=[
13
+ ... package_tool("time_tool", import_path="aip_agents.tools.time_tool", class_name="TimeTool"),
14
+ ... file_tool("multiply", file_path="/path/to/multiply.py", class_name="MultiplyTool"),
15
+ ... ]
16
+ ... )
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any
22
+
23
+ from aip_agents.ptc.custom_tools import PTCFileToolDef, PTCPackageToolDef
24
+
25
+
26
+ def package_tool(
27
+ name: str,
28
+ *,
29
+ import_path: str,
30
+ class_name: str,
31
+ package_path: str | None = None,
32
+ description: str | None = None,
33
+ input_schema: dict[str, Any] | None = None,
34
+ ) -> PTCPackageToolDef:
35
+ """Create a package-based tool definition.
36
+
37
+ Args:
38
+ name: Tool name (will be sanitized for Python compatibility).
39
+ import_path: Python import path (e.g., "aip_agents.tools.time_tool").
40
+ class_name: Tool class name to instantiate.
41
+ package_path: Optional local package path for bundling.
42
+ description: Optional tool description (auto-derived from tool if not provided).
43
+ input_schema: Optional JSON schema (auto-derived from tool if not provided).
44
+
45
+ Returns:
46
+ PTCPackageToolDef dict ready for use in PTCCustomToolConfig.tools.
47
+
48
+ Example:
49
+ >>> package_tool("time_tool", import_path="aip_agents.tools.time_tool", class_name="TimeTool")
50
+ {'name': 'time_tool', 'kind': 'package', 'import_path': 'aip_agents.tools.time_tool', 'class_name': 'TimeTool'}
51
+ """
52
+ result: PTCPackageToolDef = {
53
+ "name": name,
54
+ "kind": "package",
55
+ "import_path": import_path,
56
+ "class_name": class_name,
57
+ }
58
+ if package_path is not None:
59
+ result["package_path"] = package_path
60
+ if description is not None:
61
+ result["description"] = description
62
+ if input_schema is not None:
63
+ result["input_schema"] = input_schema
64
+ return result
65
+
66
+
67
+ def file_tool(
68
+ name: str,
69
+ *,
70
+ file_path: str,
71
+ class_name: str,
72
+ description: str | None = None,
73
+ input_schema: dict[str, Any] | None = None,
74
+ ) -> PTCFileToolDef:
75
+ """Create a file-based tool definition.
76
+
77
+ Args:
78
+ name: Tool name (will be sanitized for Python compatibility).
79
+ file_path: Absolute path to the Python file containing the tool.
80
+ class_name: Tool class name to instantiate.
81
+ description: Optional tool description (auto-derived from tool if not provided).
82
+ input_schema: Optional JSON schema (auto-derived from tool if not provided).
83
+
84
+ Returns:
85
+ PTCFileToolDef dict ready for use in PTCCustomToolConfig.tools.
86
+
87
+ Example:
88
+ >>> file_tool("multiply", file_path="/path/to/multiply_tool.py", class_name="MultiplyTool")
89
+ {'name': 'multiply', 'kind': 'file', 'file_path': '/path/to/multiply_tool.py', 'class_name': 'MultiplyTool'}
90
+ """
91
+ result: PTCFileToolDef = {
92
+ "name": name,
93
+ "kind": "file",
94
+ "file_path": file_path,
95
+ "class_name": class_name,
96
+ }
97
+ if description is not None:
98
+ result["description"] = description
99
+ if input_schema is not None:
100
+ result["input_schema"] = input_schema
101
+ return result
@@ -0,0 +1,38 @@
1
+ from aip_agents.ptc.custom_tools import PTCFileToolDef as PTCFileToolDef, PTCPackageToolDef as PTCPackageToolDef
2
+ from typing import Any
3
+
4
+ def package_tool(name: str, *, import_path: str, class_name: str, package_path: str | None = None, description: str | None = None, input_schema: dict[str, Any] | None = None) -> PTCPackageToolDef:
5
+ '''Create a package-based tool definition.
6
+
7
+ Args:
8
+ name: Tool name (will be sanitized for Python compatibility).
9
+ import_path: Python import path (e.g., "aip_agents.tools.time_tool").
10
+ class_name: Tool class name to instantiate.
11
+ package_path: Optional local package path for bundling.
12
+ description: Optional tool description (auto-derived from tool if not provided).
13
+ input_schema: Optional JSON schema (auto-derived from tool if not provided).
14
+
15
+ Returns:
16
+ PTCPackageToolDef dict ready for use in PTCCustomToolConfig.tools.
17
+
18
+ Example:
19
+ >>> package_tool("time_tool", import_path="aip_agents.tools.time_tool", class_name="TimeTool")
20
+ {\'name\': \'time_tool\', \'kind\': \'package\', \'import_path\': \'aip_agents.tools.time_tool\', \'class_name\': \'TimeTool\'}
21
+ '''
22
+ def file_tool(name: str, *, file_path: str, class_name: str, description: str | None = None, input_schema: dict[str, Any] | None = None) -> PTCFileToolDef:
23
+ '''Create a file-based tool definition.
24
+
25
+ Args:
26
+ name: Tool name (will be sanitized for Python compatibility).
27
+ file_path: Absolute path to the Python file containing the tool.
28
+ class_name: Tool class name to instantiate.
29
+ description: Optional tool description (auto-derived from tool if not provided).
30
+ input_schema: Optional JSON schema (auto-derived from tool if not provided).
31
+
32
+ Returns:
33
+ PTCFileToolDef dict ready for use in PTCCustomToolConfig.tools.
34
+
35
+ Example:
36
+ >>> file_tool("multiply", file_path="/path/to/multiply_tool.py", class_name="MultiplyTool")
37
+ {\'name\': \'multiply\', \'kind\': \'file\', \'file_path\': \'/path/to/multiply_tool.py\', \'class_name\': \'MultiplyTool\'}
38
+ '''
@@ -0,0 +1,163 @@
1
+ """Tool metadata enrichment utilities.
2
+
3
+ This module provides functions to enrich custom tool definitions with metadata
4
+ from actual tool objects (name, description, input_schema).
5
+
6
+ Author: Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import TYPE_CHECKING
13
+
14
+ from aip_agents.ptc.custom_tools import (
15
+ PTCToolDef,
16
+ enrich_tool_def_with_metadata,
17
+ )
18
+ from aip_agents.ptc.naming import sanitize_function_name
19
+
20
+ if TYPE_CHECKING:
21
+ from aip_agents.ptc import PTCCustomToolConfig
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def build_tool_lookup(tools: list[object]) -> dict[str, object]:
27
+ """Build a lookup map from tool objects for name-based matching.
28
+
29
+ Creates entries for both original and sanitized names to support
30
+ fuzzy matching (e.g., "web-search" matches "web_search").
31
+
32
+ Args:
33
+ tools: List of tool objects with `name` attribute.
34
+
35
+ Returns:
36
+ Dict mapping tool names (and sanitized names) to tool objects.
37
+
38
+ Example:
39
+ >>> tools = [TimeTool(), WebSearchTool()] # names: "time_tool", "web-search"
40
+ >>> lookup = build_tool_lookup(tools)
41
+ >>> lookup.keys()
42
+ dict_keys(['time_tool', 'web-search', 'web_search'])
43
+ """
44
+ tool_lookup: dict[str, object] = {}
45
+
46
+ for tool in tools:
47
+ if not hasattr(tool, "name"):
48
+ continue
49
+
50
+ tool_name = tool.name
51
+ if not tool_name: # Skip None or empty string names
52
+ continue
53
+ tool_name = str(tool_name)
54
+ tool_lookup[tool_name] = tool
55
+
56
+ # Also add sanitized name for fuzzy matching
57
+ sanitized = sanitize_function_name(tool_name)
58
+ if sanitized != tool_name:
59
+ tool_lookup[sanitized] = tool
60
+
61
+ return tool_lookup
62
+
63
+
64
+ def match_tool_by_name(
65
+ tool_def: PTCToolDef,
66
+ tool_lookup: dict[str, object],
67
+ ) -> tuple[object | None, str | None]:
68
+ """Find a matching tool object for a tool definition.
69
+
70
+ Tries exact name match first, then falls back to sanitized name.
71
+
72
+ Args:
73
+ tool_def: Tool definition dict with "name" key.
74
+ tool_lookup: Lookup map from build_tool_lookup().
75
+
76
+ Returns:
77
+ Tuple of (matching_tool, canonical_name) or (None, None) if no match.
78
+ canonical_name is the tool object's actual name (may differ from tool_def name).
79
+ """
80
+ def_name = tool_def.get("name", "")
81
+ sanitized_def_name = sanitize_function_name(def_name)
82
+
83
+ # Try exact match first, then sanitized
84
+ matching_tool = tool_lookup.get(def_name) or tool_lookup.get(sanitized_def_name)
85
+
86
+ if matching_tool is None:
87
+ return None, None
88
+
89
+ # Get canonical name from tool object
90
+ canonical_name = str(getattr(matching_tool, "name", ""))
91
+
92
+ # Treat empty canonical name as no match
93
+ if not canonical_name:
94
+ return None, None
95
+
96
+ return matching_tool, canonical_name
97
+
98
+
99
+ def enrich_custom_tools_from_agent(
100
+ custom_tools_config: PTCCustomToolConfig,
101
+ agent_tools: list[object],
102
+ agent_name: str = "",
103
+ ) -> int:
104
+ """Enrich custom tool definitions with metadata from agent's tool objects.
105
+
106
+ Matches custom tool definitions with actual tool objects by name (including
107
+ sanitized name matching) and enriches them with description and input_schema.
108
+
109
+ Note: This function modifies custom_tools_config.tools in-place.
110
+
111
+ Args:
112
+ custom_tools_config: The PTCCustomToolConfig to enrich.
113
+ agent_tools: List of tool objects from the agent.
114
+ agent_name: Agent name for logging context.
115
+
116
+ Returns:
117
+ Number of tools that were enriched.
118
+
119
+ Example:
120
+ >>> config = PTCCustomToolConfig(enabled=True, tools=[{"name": "multiply", ...}])
121
+ >>> count = enrich_custom_tools_from_agent(config, [MultiplyTool()], "my_agent")
122
+ >>> count
123
+ 1
124
+ """
125
+ if not custom_tools_config.enabled:
126
+ return 0
127
+
128
+ if not custom_tools_config.tools:
129
+ return 0
130
+
131
+ # Build lookup map
132
+ tool_lookup = build_tool_lookup(agent_tools)
133
+
134
+ # Enrich each tool definition
135
+ enriched_count = 0
136
+ log_prefix = f"Agent '{agent_name}': " if agent_name else ""
137
+
138
+ for i, tool_def in enumerate(custom_tools_config.tools):
139
+ def_name = tool_def.get("name", "")
140
+ matching_tool, canonical_name = match_tool_by_name(tool_def, tool_lookup)
141
+
142
+ if matching_tool is None:
143
+ logger.debug(
144
+ f"{log_prefix}No matching tool object found for custom tool '{def_name}', using existing definition"
145
+ )
146
+ continue
147
+
148
+ # Handle name correction if matched by sanitized name
149
+ if canonical_name and canonical_name != def_name:
150
+ tool_def_copy = dict(tool_def)
151
+ tool_def_copy["name"] = canonical_name
152
+ enriched = enrich_tool_def_with_metadata(tool_def_copy, matching_tool)
153
+ else:
154
+ enriched = enrich_tool_def_with_metadata(tool_def, matching_tool)
155
+
156
+ custom_tools_config.tools[i] = enriched
157
+ enriched_count += 1
158
+ logger.debug(f"{log_prefix}Enriched custom tool '{def_name}' with metadata")
159
+
160
+ if enriched_count > 0:
161
+ logger.info(f"{log_prefix}Enriched {enriched_count} custom tool(s) with metadata")
162
+
163
+ return enriched_count
@@ -0,0 +1,60 @@
1
+ from _typeshed import Incomplete
2
+ from aip_agents.ptc import PTCCustomToolConfig as PTCCustomToolConfig
3
+ from aip_agents.ptc.custom_tools import PTCToolDef as PTCToolDef, enrich_tool_def_with_metadata as enrich_tool_def_with_metadata
4
+ from aip_agents.ptc.naming import sanitize_function_name as sanitize_function_name
5
+
6
+ logger: Incomplete
7
+
8
+ def build_tool_lookup(tools: list[object]) -> dict[str, object]:
9
+ '''Build a lookup map from tool objects for name-based matching.
10
+
11
+ Creates entries for both original and sanitized names to support
12
+ fuzzy matching (e.g., "web-search" matches "web_search").
13
+
14
+ Args:
15
+ tools: List of tool objects with `name` attribute.
16
+
17
+ Returns:
18
+ Dict mapping tool names (and sanitized names) to tool objects.
19
+
20
+ Example:
21
+ >>> tools = [TimeTool(), WebSearchTool()] # names: "time_tool", "web-search"
22
+ >>> lookup = build_tool_lookup(tools)
23
+ >>> lookup.keys()
24
+ dict_keys([\'time_tool\', \'web-search\', \'web_search\'])
25
+ '''
26
+ def match_tool_by_name(tool_def: PTCToolDef, tool_lookup: dict[str, object]) -> tuple[object | None, str | None]:
27
+ '''Find a matching tool object for a tool definition.
28
+
29
+ Tries exact name match first, then falls back to sanitized name.
30
+
31
+ Args:
32
+ tool_def: Tool definition dict with "name" key.
33
+ tool_lookup: Lookup map from build_tool_lookup().
34
+
35
+ Returns:
36
+ Tuple of (matching_tool, canonical_name) or (None, None) if no match.
37
+ canonical_name is the tool object\'s actual name (may differ from tool_def name).
38
+ '''
39
+ def enrich_custom_tools_from_agent(custom_tools_config: PTCCustomToolConfig, agent_tools: list[object], agent_name: str = '') -> int:
40
+ '''Enrich custom tool definitions with metadata from agent\'s tool objects.
41
+
42
+ Matches custom tool definitions with actual tool objects by name (including
43
+ sanitized name matching) and enriches them with description and input_schema.
44
+
45
+ Note: This function modifies custom_tools_config.tools in-place.
46
+
47
+ Args:
48
+ custom_tools_config: The PTCCustomToolConfig to enrich.
49
+ agent_tools: List of tool objects from the agent.
50
+ agent_name: Agent name for logging context.
51
+
52
+ Returns:
53
+ Number of tools that were enriched.
54
+
55
+ Example:
56
+ >>> config = PTCCustomToolConfig(enabled=True, tools=[{"name": "multiply", ...}])
57
+ >>> count = enrich_custom_tools_from_agent(config, [MultiplyTool()], "my_agent")
58
+ >>> count
59
+ 1
60
+ '''
@@ -1,9 +1,205 @@
1
1
  """Defaults for PTC sandbox templates and packages."""
2
2
 
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from aip_agents.mcp.client.base_mcp_client import BaseMCPClient
9
+ from aip_agents.ptc.custom_tools import PTCCustomToolConfig
10
+
3
11
  DEFAULT_PTC_TEMPLATE = "aip-agents-ptc-v1"
12
+ AIP_AGENTS_BINARY_LOCAL = "aip-agents-binary[local]"
4
13
  DEFAULT_PTC_PACKAGES: tuple[str, ...] = (
5
- "aip-agents-binary[local]",
14
+ AIP_AGENTS_BINARY_LOCAL,
6
15
  "mcp",
7
16
  "httpx",
8
17
  "gllm-plugin-binary==0.0.7",
9
18
  )
19
+
20
+
21
+ def _has_mcp_tools(mcp_client: BaseMCPClient | None) -> bool:
22
+ """Check if MCP tools are present.
23
+
24
+ Args:
25
+ mcp_client: The MCP client to check.
26
+
27
+ Returns:
28
+ True if MCP client has servers configured.
29
+ """
30
+ return mcp_client is not None and bool(getattr(mcp_client, "servers", None))
31
+
32
+
33
+ def _has_custom_tools(custom_tools_config: PTCCustomToolConfig | None) -> bool:
34
+ """Check if custom tools are present and enabled.
35
+
36
+ Args:
37
+ custom_tools_config: The custom tools configuration to check.
38
+
39
+ Returns:
40
+ True if custom tools are enabled and tools list is non-empty.
41
+ """
42
+ return custom_tools_config is not None and custom_tools_config.enabled and bool(custom_tools_config.tools)
43
+
44
+
45
+ def _has_native_aip_tools(custom_tools_config: PTCCustomToolConfig | None) -> bool:
46
+ """Check if native AIP tools are present.
47
+
48
+ Native AIP tools are package-based custom tools with import_path starting with "aip_agents.tools.".
49
+
50
+ Args:
51
+ custom_tools_config: The custom tools configuration to check.
52
+
53
+ Returns:
54
+ True if at least one native AIP tool is found.
55
+ """
56
+ if not _has_custom_tools(custom_tools_config):
57
+ return False
58
+
59
+ assert custom_tools_config is not None
60
+ for tool in custom_tools_config.tools:
61
+ if tool.get("kind") == "package":
62
+ import_path = tool.get("import_path", "")
63
+ if import_path.startswith("aip_agents.tools."):
64
+ return True
65
+
66
+ return False
67
+
68
+
69
+ def _merge_requirements(packages: list[str], requirements: list[str]) -> None:
70
+ """Merge requirements into packages list, avoiding duplicates.
71
+
72
+ Args:
73
+ packages: Target list to merge into (modified in place).
74
+ requirements: Requirements to merge.
75
+ """
76
+ for req in requirements:
77
+ if req not in packages:
78
+ packages.append(req)
79
+
80
+
81
+ def _merge_custom_requirements(
82
+ packages: list[str],
83
+ custom_tools_config: PTCCustomToolConfig | None,
84
+ ) -> None:
85
+ """Merge custom tool requirements into packages list.
86
+
87
+ Args:
88
+ packages: Target list to merge into (modified in place).
89
+ custom_tools_config: Custom tools configuration.
90
+ """
91
+ if not _has_custom_tools(custom_tools_config):
92
+ return
93
+
94
+ assert custom_tools_config is not None
95
+ if custom_tools_config.requirements:
96
+ _merge_requirements(packages, custom_tools_config.requirements)
97
+
98
+
99
+ def _packages_for_default_template(
100
+ custom_tools_config: PTCCustomToolConfig | None,
101
+ user_ptc_packages: list[str] | None,
102
+ ) -> list[str] | None:
103
+ """Build package list when using the default template.
104
+
105
+ Args:
106
+ custom_tools_config: Custom tools configuration.
107
+ user_ptc_packages: Explicitly provided packages by user (None if not set).
108
+
109
+ Returns:
110
+ List of packages to install, or None to skip installation.
111
+ """
112
+ packages = list(user_ptc_packages) if user_ptc_packages is not None else []
113
+ _merge_custom_requirements(packages, custom_tools_config)
114
+ return packages if packages else None
115
+
116
+
117
+ def _packages_for_other_templates(
118
+ *,
119
+ mcp_client: BaseMCPClient | None,
120
+ custom_tools_config: PTCCustomToolConfig | None,
121
+ user_ptc_packages: list[str] | None,
122
+ ) -> list[str] | None:
123
+ """Build package list when using a non-default template.
124
+
125
+ Args:
126
+ mcp_client: The MCP client (check if has servers for MCP tools).
127
+ custom_tools_config: Custom tools configuration.
128
+ user_ptc_packages: Explicitly provided packages by user (None if not set).
129
+
130
+ Returns:
131
+ List of packages to install, or None to skip installation.
132
+ """
133
+ packages = list(user_ptc_packages) if user_ptc_packages is not None else []
134
+ base_packages = _build_base_packages(mcp_client=mcp_client, custom_tools_config=custom_tools_config)
135
+ _merge_requirements(packages, base_packages)
136
+ _merge_custom_requirements(packages, custom_tools_config)
137
+ return packages if packages else None
138
+
139
+
140
+ def _build_base_packages(
141
+ *,
142
+ mcp_client: BaseMCPClient | None,
143
+ custom_tools_config: PTCCustomToolConfig | None,
144
+ ) -> list[str]:
145
+ """Build base package list based on tool types.
146
+
147
+ Args:
148
+ mcp_client: The MCP client (check if has servers for MCP tools).
149
+ custom_tools_config: Custom tools configuration.
150
+
151
+ Returns:
152
+ List of base packages needed for the tool types present.
153
+ """
154
+ packages: list[str] = []
155
+
156
+ if _has_mcp_tools(mcp_client):
157
+ packages.extend(["mcp", "httpx"])
158
+
159
+ if _has_custom_tools(custom_tools_config):
160
+ packages.extend(["langchain", "gllm-plugin-binary==0.0.7"])
161
+
162
+ if _has_native_aip_tools(custom_tools_config):
163
+ if AIP_AGENTS_BINARY_LOCAL not in packages:
164
+ packages.append(AIP_AGENTS_BINARY_LOCAL)
165
+
166
+ return packages
167
+
168
+
169
+ def select_sandbox_packages(
170
+ *,
171
+ mcp_client: BaseMCPClient | None,
172
+ custom_tools_config: PTCCustomToolConfig | None,
173
+ template: str | None,
174
+ user_ptc_packages: list[str] | None,
175
+ ) -> list[str] | None:
176
+ """Select minimal packages to install in sandbox based on tool types.
177
+
178
+ This function implements the smart package selection logic:
179
+ - DEFAULT_PTC_TEMPLATE + no user packages: Skip install (template has all deps)
180
+ - DEFAULT_PTC_TEMPLATE + user packages: User packages + PTCCustomToolConfig.requirements
181
+ - Other template + no user packages: Build packages from tool types
182
+ - Other template + user packages: User packages + tool-type packages + PTCCustomToolConfig.requirements
183
+
184
+ Key principle: Tools won't work without their required packages. Always ensure:
185
+ - MCP tools → mcp + httpx installed
186
+ - Custom LangChain tools → langchain + gllm-plugin-binary installed
187
+ - Native AIP tools → aip-agents-binary[local] installed
188
+
189
+ Args:
190
+ mcp_client: The MCP client (check if has servers for MCP tools).
191
+ custom_tools_config: Custom tools configuration (includes tools list and .requirements field).
192
+ template: Sandbox template being used.
193
+ user_ptc_packages: Explicitly provided packages by user (None if not set).
194
+
195
+ Returns:
196
+ List of packages to install, or None to skip installation.
197
+ """
198
+ if template == DEFAULT_PTC_TEMPLATE:
199
+ return _packages_for_default_template(custom_tools_config, user_ptc_packages)
200
+
201
+ return _packages_for_other_templates(
202
+ mcp_client=mcp_client,
203
+ custom_tools_config=custom_tools_config,
204
+ user_ptc_packages=user_ptc_packages,
205
+ )
@@ -1,2 +1,30 @@
1
+ from aip_agents.mcp.client.base_mcp_client import BaseMCPClient as BaseMCPClient
2
+ from aip_agents.ptc.custom_tools import PTCCustomToolConfig as PTCCustomToolConfig
3
+
1
4
  DEFAULT_PTC_TEMPLATE: str
5
+ AIP_AGENTS_BINARY_LOCAL: str
2
6
  DEFAULT_PTC_PACKAGES: tuple[str, ...]
7
+
8
+ def select_sandbox_packages(*, mcp_client: BaseMCPClient | None, custom_tools_config: PTCCustomToolConfig | None, template: str | None, user_ptc_packages: list[str] | None) -> list[str] | None:
9
+ """Select minimal packages to install in sandbox based on tool types.
10
+
11
+ This function implements the smart package selection logic:
12
+ - DEFAULT_PTC_TEMPLATE + no user packages: Skip install (template has all deps)
13
+ - DEFAULT_PTC_TEMPLATE + user packages: User packages + PTCCustomToolConfig.requirements
14
+ - Other template + no user packages: Build packages from tool types
15
+ - Other template + user packages: User packages + tool-type packages + PTCCustomToolConfig.requirements
16
+
17
+ Key principle: Tools won't work without their required packages. Always ensure:
18
+ - MCP tools → mcp + httpx installed
19
+ - Custom LangChain tools → langchain + gllm-plugin-binary installed
20
+ - Native AIP tools → aip-agents-binary[local] installed
21
+
22
+ Args:
23
+ mcp_client: The MCP client (check if has servers for MCP tools).
24
+ custom_tools_config: Custom tools configuration (includes tools list and .requirements field).
25
+ template: Sandbox template being used.
26
+ user_ptc_packages: Explicitly provided packages by user (None if not set).
27
+
28
+ Returns:
29
+ List of packages to install, or None to skip installation.
30
+ """
@@ -6,6 +6,8 @@ Authors:
6
6
  Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
7
7
  """
8
8
 
9
+ from collections.abc import Callable
10
+
9
11
  from e2b_code_interpreter import AsyncSandbox, OutputMessage
10
12
 
11
13
  from aip_agents.sandbox.defaults import DEFAULT_PTC_PACKAGES, DEFAULT_PTC_TEMPLATE
@@ -13,6 +15,10 @@ from aip_agents.sandbox.types import SandboxExecutionResult
13
15
  from aip_agents.sandbox.validation import validate_package_names
14
16
  from aip_agents.utils.logger import get_logger
15
17
 
18
+ # Type alias for package selector callback.
19
+ # Called after sandbox creation with actual template status (None if template failed).
20
+ PackageSelector = Callable[[str | None], list[str] | None]
21
+
16
22
  logger = get_logger(__name__)
17
23
 
18
24
  SANDBOX_NOT_INITIALIZED_ERROR = "Sandbox not initialized"
@@ -40,15 +46,21 @@ class E2BSandboxRuntime:
40
46
  self,
41
47
  template: str | None = None,
42
48
  ptc_packages: list[str] | None = None,
49
+ package_selector: PackageSelector | None = None,
43
50
  ) -> None:
44
51
  """Initialize E2B sandbox runtime.
45
52
 
46
53
  Args:
47
54
  template: Optional E2B template ID for custom sandbox environments.
48
55
  ptc_packages: Packages to install in sandbox. If None or empty, skip install.
56
+ Overwritten by package_selector if provided.
57
+ package_selector: Optional callback to select packages after sandbox creation.
58
+ Called with actual template (None if template failed) to enable smart
59
+ package selection based on whether template was successfully created.
49
60
  """
50
61
  self._template = template
51
62
  self._ptc_packages = ptc_packages
63
+ self._package_selector = package_selector
52
64
  self._sandbox: AsyncSandbox | None = None
53
65
  self._sandbox_created_with_template = False
54
66
 
@@ -153,6 +165,22 @@ class E2BSandboxRuntime:
153
165
  self._sandbox_created_with_template = False
154
166
  await create_default_sandbox()
155
167
 
168
+ # Determine packages to install
169
+ # If package_selector is provided, use it with actual template status
170
+ # This enables smart package selection based on whether template succeeded
171
+ if self._package_selector is not None:
172
+ actual_template = template if self._sandbox_created_with_template else None
173
+ try:
174
+ self._ptc_packages = self._package_selector(actual_template)
175
+ logger.info(
176
+ f"Package selector called with actual_template={actual_template}, "
177
+ f"selected packages: {self._ptc_packages}"
178
+ )
179
+ except Exception as e:
180
+ logger.error(f"Package selector failed: {e}")
181
+ await self.cleanup()
182
+ raise
183
+
156
184
  # Install ptc_packages if non-empty
157
185
  if self._ptc_packages:
158
186
  if self._should_skip_default_ptc_install(template):