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,131 @@
|
|
|
1
|
+
"""Template builder for PTC sandbox templates.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for creating and managing E2B sandbox templates
|
|
4
|
+
for programmatic tool calling (PTC) environments.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from e2b import Template, default_build_logger
|
|
11
|
+
|
|
12
|
+
from aip_agents.sandbox.validation import validate_package_names
|
|
13
|
+
from aip_agents.utils.logger import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_ptc_template(base_template: str, ptc_packages: list[str] | None) -> Template:
|
|
19
|
+
"""Create a PTC template definition based on a base template.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
base_template: Base template alias to build from (e.g., "code-interpreter-v1").
|
|
23
|
+
ptc_packages: List of packages to install in the template.
|
|
24
|
+
If None or empty, skips pip install step.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Template: A configured template ready to be built.
|
|
28
|
+
"""
|
|
29
|
+
logger.info(f"Creating template from base: {base_template}")
|
|
30
|
+
template = Template().from_template(base_template)
|
|
31
|
+
|
|
32
|
+
if ptc_packages:
|
|
33
|
+
# Validate all packages before constructing command
|
|
34
|
+
validate_package_names(ptc_packages)
|
|
35
|
+
|
|
36
|
+
# Note: packages_str is safe because ptc_packages is a controlled list from
|
|
37
|
+
# configuration, not user input. Template.run_cmd() only accepts str.
|
|
38
|
+
packages_str = " ".join(ptc_packages)
|
|
39
|
+
logger.info(f"Installing packages: {packages_str}")
|
|
40
|
+
template.run_cmd(f"pip install -q {packages_str}")
|
|
41
|
+
|
|
42
|
+
return template
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _template_exists(template_id: str) -> bool:
|
|
46
|
+
"""Check if a template alias exists.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
template_id: The template alias to check.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
bool: True if alias exists, False otherwise.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
return Template.alias_exists(template_id)
|
|
56
|
+
except Exception:
|
|
57
|
+
logger.warning(f"Template alias check failed for: {template_id}")
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _build_template(template: Template, template_id: str) -> bool:
|
|
62
|
+
"""Build a template with the given alias.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
template: The template to build.
|
|
66
|
+
template_id: The alias to assign to the built template.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
bool: True if build succeeded, False otherwise.
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
logger.info(f"Building template: {template_id}")
|
|
73
|
+
Template.build(
|
|
74
|
+
template,
|
|
75
|
+
alias=template_id,
|
|
76
|
+
on_build_logs=default_build_logger(),
|
|
77
|
+
)
|
|
78
|
+
logger.info(f"Template built successfully: {template_id}")
|
|
79
|
+
return True
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.warning(f"Template build failed for {template_id}: {e}")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def ensure_ptc_template(
|
|
86
|
+
template_id: str,
|
|
87
|
+
base_template: str,
|
|
88
|
+
ptc_packages: list[str] | None,
|
|
89
|
+
force_rebuild: bool = False,
|
|
90
|
+
) -> str | None:
|
|
91
|
+
"""Ensure a PTC sandbox template exists, creating it if necessary.
|
|
92
|
+
|
|
93
|
+
This is an explicit helper that apps can call at startup to ensure the
|
|
94
|
+
template exists. It is never run implicitly by the SDK.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
template_id: Unique alias for the template (e.g., "aip-agents-ptc-v1").
|
|
98
|
+
base_template: Base template alias to build from
|
|
99
|
+
(e.g., "code-interpreter-v1").
|
|
100
|
+
ptc_packages: List of packages to install in the template.
|
|
101
|
+
If None or empty, skips pip install step.
|
|
102
|
+
force_rebuild: If True, rebuild even if alias exists.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
The template_id on success, None if creation failed.
|
|
106
|
+
Never raises exceptions.
|
|
107
|
+
"""
|
|
108
|
+
# Fast path: template already exists and we're not forcing rebuild
|
|
109
|
+
if not force_rebuild and _template_exists(template_id):
|
|
110
|
+
logger.info(f"Template already exists: {template_id}")
|
|
111
|
+
return template_id
|
|
112
|
+
|
|
113
|
+
# Create and build the template
|
|
114
|
+
try:
|
|
115
|
+
template = create_ptc_template(base_template, ptc_packages)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.warning(f"Template creation failed for {template_id}: {e}")
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
# Build the template
|
|
121
|
+
is_success = _build_template(template, template_id)
|
|
122
|
+
if is_success:
|
|
123
|
+
return template_id
|
|
124
|
+
|
|
125
|
+
# Build failed. Check if template exists anyway (race condition: another
|
|
126
|
+
# process may have built it while we were trying)
|
|
127
|
+
if _template_exists(template_id):
|
|
128
|
+
logger.info(f"Template already exists after failed build: {template_id}")
|
|
129
|
+
return template_id
|
|
130
|
+
|
|
131
|
+
return None
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from _typeshed import Incomplete
|
|
2
|
+
from aip_agents.sandbox.validation import validate_package_names as validate_package_names
|
|
3
|
+
from aip_agents.utils.logger import get_logger as get_logger
|
|
4
|
+
from e2b import Template
|
|
5
|
+
|
|
6
|
+
logger: Incomplete
|
|
7
|
+
|
|
8
|
+
def create_ptc_template(base_template: str, ptc_packages: list[str] | None) -> Template:
|
|
9
|
+
'''Create a PTC template definition based on a base template.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
base_template: Base template alias to build from (e.g., "code-interpreter-v1").
|
|
13
|
+
ptc_packages: List of packages to install in the template.
|
|
14
|
+
If None or empty, skips pip install step.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Template: A configured template ready to be built.
|
|
18
|
+
'''
|
|
19
|
+
def ensure_ptc_template(template_id: str, base_template: str, ptc_packages: list[str] | None, force_rebuild: bool = False) -> str | None:
|
|
20
|
+
'''Ensure a PTC sandbox template exists, creating it if necessary.
|
|
21
|
+
|
|
22
|
+
This is an explicit helper that apps can call at startup to ensure the
|
|
23
|
+
template exists. It is never run implicitly by the SDK.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
template_id: Unique alias for the template (e.g., "aip-agents-ptc-v1").
|
|
27
|
+
base_template: Base template alias to build from
|
|
28
|
+
(e.g., "code-interpreter-v1").
|
|
29
|
+
ptc_packages: List of packages to install in the template.
|
|
30
|
+
If None or empty, skips pip install step.
|
|
31
|
+
force_rebuild: If True, rebuild even if alias exists.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The template_id on success, None if creation failed.
|
|
35
|
+
Never raises exceptions.
|
|
36
|
+
'''
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Sandbox execution result types.
|
|
2
|
+
|
|
3
|
+
This module defines types for sandbox execution results.
|
|
4
|
+
|
|
5
|
+
Authors:
|
|
6
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SandboxExecutionResult:
|
|
14
|
+
"""Result of a sandbox code execution.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
stdout: Standard output from the execution.
|
|
18
|
+
stderr: Standard error from the execution.
|
|
19
|
+
exit_code: Exit code (0 for success, non-zero for failure).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
stdout: str
|
|
23
|
+
stderr: str
|
|
24
|
+
exit_code: int
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
@dataclass
|
|
4
|
+
class SandboxExecutionResult:
|
|
5
|
+
"""Result of a sandbox code execution.
|
|
6
|
+
|
|
7
|
+
Attributes:
|
|
8
|
+
stdout: Standard output from the execution.
|
|
9
|
+
stderr: Standard error from the execution.
|
|
10
|
+
exit_code: Exit code (0 for success, non-zero for failure).
|
|
11
|
+
"""
|
|
12
|
+
stdout: str
|
|
13
|
+
stderr: str
|
|
14
|
+
exit_code: int
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Validation utilities for sandbox operations.
|
|
2
|
+
|
|
3
|
+
This module provides validation functions for sandbox-related operations
|
|
4
|
+
such as package name validation.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
_PACKAGE_SPEC_PATTERN = re.compile(
|
|
13
|
+
r"^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?"
|
|
14
|
+
r"(?:\[[A-Za-z0-9._-]+(?:,[A-Za-z0-9._-]+)*\])?"
|
|
15
|
+
r"(?:"
|
|
16
|
+
r"(?:==|!=|<=|>=|~=|<|>)[0-9][A-Za-z0-9.*+!_-]*"
|
|
17
|
+
r"(?:,(?:==|!=|<=|>=|~=|<|>)[0-9][A-Za-z0-9.*+!_-]*)*"
|
|
18
|
+
r")?$"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def validate_package_name(package: str) -> bool:
|
|
23
|
+
"""Validate package name/specifier format for pip install.
|
|
24
|
+
|
|
25
|
+
Allows standard pip formats: package, package==version, package[extra].
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
package: Package name or specifier to validate.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
True if package name is valid, False otherwise.
|
|
32
|
+
"""
|
|
33
|
+
if not package:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
return bool(_PACKAGE_SPEC_PATTERN.fullmatch(package))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_package_names(packages: list[str]) -> None:
|
|
40
|
+
"""Validate all package names in a list.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
packages: List of package names or specifiers to validate.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If any package name is invalid.
|
|
47
|
+
"""
|
|
48
|
+
for pkg in packages:
|
|
49
|
+
if not validate_package_name(pkg):
|
|
50
|
+
raise ValueError(f"Invalid package name format: {pkg}")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
def validate_package_name(package: str) -> bool:
|
|
2
|
+
"""Validate package name/specifier format for pip install.
|
|
3
|
+
|
|
4
|
+
Allows standard pip formats: package, package==version, package[extra].
|
|
5
|
+
|
|
6
|
+
Args:
|
|
7
|
+
package: Package name or specifier to validate.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
True if package name is valid, False otherwise.
|
|
11
|
+
"""
|
|
12
|
+
def validate_package_names(packages: list[str]) -> None:
|
|
13
|
+
"""Validate all package names in a list.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
packages: List of package names or specifiers to validate.
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
ValueError: If any package name is invalid.
|
|
20
|
+
"""
|
aip_agents/tools/__init__.py
CHANGED
|
@@ -46,8 +46,10 @@ _register_optional("aip_agents.tools.code_sandbox", "E2BCodeSandboxTool")
|
|
|
46
46
|
_register_optional("aip_agents.tools.document_loader", "DocxReaderTool")
|
|
47
47
|
_register_optional("aip_agents.tools.document_loader", "ExcelReaderTool")
|
|
48
48
|
_register_optional("aip_agents.tools.document_loader", "PDFReaderTool")
|
|
49
|
+
_register_optional("aip_agents.tools.execute_ptc_code", "create_execute_ptc_code_tool")
|
|
49
50
|
|
|
50
51
|
if TYPE_CHECKING:
|
|
51
52
|
from aip_agents.tools.browser_use import BrowserUseTool
|
|
52
53
|
from aip_agents.tools.code_sandbox import E2BCodeSandboxTool
|
|
53
54
|
from aip_agents.tools.document_loader import DocxReaderTool, ExcelReaderTool, PDFReaderTool
|
|
55
|
+
from aip_agents.tools.execute_ptc_code import create_execute_ptc_code_tool
|
aip_agents/tools/__init__.pyi
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from aip_agents.tools.browser_use import BrowserUseTool as BrowserUseTool
|
|
2
2
|
from aip_agents.tools.code_sandbox import E2BCodeSandboxTool as E2BCodeSandboxTool
|
|
3
3
|
from aip_agents.tools.document_loader import DocxReaderTool as DocxReaderTool, ExcelReaderTool as ExcelReaderTool, PDFReaderTool as PDFReaderTool
|
|
4
|
+
from aip_agents.tools.execute_ptc_code import create_execute_ptc_code_tool as create_execute_ptc_code_tool
|
|
4
5
|
from aip_agents.tools.gl_connector import GLConnectorTool as GLConnectorTool
|
|
5
6
|
from aip_agents.tools.gl_connector_tools import BOSA_AUTOMATED_TOOLS as BOSA_AUTOMATED_TOOLS, GL_CONNECTORS_AUTOMATED_TOOLS as GL_CONNECTORS_AUTOMATED_TOOLS
|
|
6
7
|
from aip_agents.tools.time_tool import TimeTool as TimeTool
|
|
7
8
|
from aip_agents.tools.web_search import GoogleSerperTool as GoogleSerperTool
|
|
8
9
|
|
|
9
|
-
__all__ = ['BOSA_AUTOMATED_TOOLS', 'GL_CONNECTORS_AUTOMATED_TOOLS', 'GLConnectorTool', 'GoogleSerperTool', 'TimeTool', 'BrowserUseTool', 'E2BCodeSandboxTool', 'DocxReaderTool', 'ExcelReaderTool', 'PDFReaderTool']
|
|
10
|
+
__all__ = ['BOSA_AUTOMATED_TOOLS', 'GL_CONNECTORS_AUTOMATED_TOOLS', 'GLConnectorTool', 'GoogleSerperTool', 'TimeTool', 'BrowserUseTool', 'E2BCodeSandboxTool', 'DocxReaderTool', 'ExcelReaderTool', 'PDFReaderTool', 'create_execute_ptc_code_tool']
|
|
@@ -93,6 +93,7 @@ References:
|
|
|
93
93
|
"""
|
|
94
94
|
|
|
95
95
|
import asyncio
|
|
96
|
+
import copy
|
|
96
97
|
import json
|
|
97
98
|
from collections.abc import Callable
|
|
98
99
|
from typing import Any, Literal
|
|
@@ -556,13 +557,20 @@ class BrowserUseTool(BaseTool):
|
|
|
556
557
|
iframe_event = yield_iframe_activity(streaming_state.debug_url, "Receive streaming URL")
|
|
557
558
|
self._log_stream_event("iframe_start", iframe_event)
|
|
558
559
|
yield iframe_event
|
|
560
|
+
last_done_event: dict | None = None
|
|
559
561
|
async for event in self._stream_agent_with_markers(agent, streaming_state, recorder):
|
|
560
562
|
self._log_stream_event("agent_event", event)
|
|
563
|
+
tool_info = event.get("tool_info") if isinstance(event, dict) else None
|
|
564
|
+
tool_calls = tool_info.get("tool_calls", []) if isinstance(tool_info, dict) else []
|
|
565
|
+
if any(call.get("name") == "done" for call in tool_calls if isinstance(call, dict)):
|
|
566
|
+
last_done_event = event
|
|
561
567
|
yield event
|
|
562
568
|
recording_event = self._recording_event(streaming_state)
|
|
563
569
|
if recording_event:
|
|
564
570
|
self._log_stream_event("recording_event", recording_event)
|
|
565
571
|
yield recording_event
|
|
572
|
+
if last_done_event:
|
|
573
|
+
yield copy.deepcopy(last_done_event)
|
|
566
574
|
finally:
|
|
567
575
|
await self._release_session(session, client)
|
|
568
576
|
|
|
@@ -223,6 +223,8 @@ def create_step_response(
|
|
|
223
223
|
if is_done:
|
|
224
224
|
final_tool = _get_done_tool_for_final_response(agent, tool_calls_dict)
|
|
225
225
|
final_output = final_tool.get("output") or TASK_COMPLETED_MESSAGE
|
|
226
|
+
if not any(call.get("name") == "done" for call in tool_calls_dict):
|
|
227
|
+
tool_calls_dict.append(final_tool)
|
|
226
228
|
tool_info = {
|
|
227
229
|
"name": PRIMARY_TOOL_NAME,
|
|
228
230
|
"args": final_tool.get("args", {}),
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Execute PTC Code Tool.
|
|
2
|
+
|
|
3
|
+
This module provides a LangChain tool for executing Python code with MCP tool access
|
|
4
|
+
inside an E2B sandbox. The tool is designed for LLM-generated code that needs to call
|
|
5
|
+
multiple MCP tools programmatically.
|
|
6
|
+
|
|
7
|
+
Authors:
|
|
8
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import concurrent.futures
|
|
13
|
+
import json
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
from langchain_core.callbacks import (
|
|
17
|
+
AsyncCallbackManagerForToolRun,
|
|
18
|
+
CallbackManagerForToolRun,
|
|
19
|
+
)
|
|
20
|
+
from langchain_core.tools import BaseTool
|
|
21
|
+
|
|
22
|
+
from aip_agents.ptc.naming import sanitize_function_name
|
|
23
|
+
from aip_agents.tools.tool_config_injector import TOOL_CONFIGS_KEY
|
|
24
|
+
from aip_agents.utils.logger import get_logger
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from aip_agents.mcp.client.base_mcp_client import BaseMCPClient
|
|
28
|
+
from aip_agents.ptc.executor import PTCSandboxConfig, PTCSandboxExecutor
|
|
29
|
+
from aip_agents.sandbox.e2b_runtime import E2BSandboxRuntime
|
|
30
|
+
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _merge_config_layer(
|
|
35
|
+
merged: dict[str, dict[str, Any]],
|
|
36
|
+
source: dict[str, Any],
|
|
37
|
+
skip_tool_configs_key: bool = True,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Merge a single layer of tool configs into the merged dict.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
merged: Target dict to merge into (modified in place).
|
|
43
|
+
source: Source dict containing tool configs.
|
|
44
|
+
skip_tool_configs_key: Whether to skip the TOOL_CONFIGS_KEY entry.
|
|
45
|
+
"""
|
|
46
|
+
for name, config in source.items():
|
|
47
|
+
if skip_tool_configs_key and name == TOOL_CONFIGS_KEY:
|
|
48
|
+
continue
|
|
49
|
+
if not isinstance(config, dict):
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
sanitized = sanitize_function_name(name)
|
|
53
|
+
if sanitized in merged:
|
|
54
|
+
merged[sanitized].update(config)
|
|
55
|
+
else:
|
|
56
|
+
merged[sanitized] = dict(config)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def merge_tool_configs(
|
|
60
|
+
agent_configs: dict[str, Any] | None,
|
|
61
|
+
runtime_configs: dict[str, Any] | None,
|
|
62
|
+
) -> dict[str, dict[str, Any]]:
|
|
63
|
+
"""Merge agent-level and runtime tool configs with sanitized keys.
|
|
64
|
+
|
|
65
|
+
Merges tool configurations from two sources:
|
|
66
|
+
1. Agent-level defaults (from agent.tool_configs)
|
|
67
|
+
2. Runtime overrides (from RunnableConfig.metadata["tool_configs"])
|
|
68
|
+
|
|
69
|
+
Both sources support two formats (matching LangGraphReactAgent behavior):
|
|
70
|
+
- Direct per-tool keys: {"time_tool": {"timezone": "UTC"}}
|
|
71
|
+
- Nested structure: {"tool_configs": {"time_tool": {"timezone": "UTC"}}}
|
|
72
|
+
|
|
73
|
+
The nested "tool_configs" key has higher precedence than direct keys.
|
|
74
|
+
Tool names are sanitized to match sandbox expectations (e.g., "Time Tool" -> "time_tool").
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
agent_configs: Agent-level tool configs (may be None or contain nested dicts)
|
|
78
|
+
runtime_configs: Runtime overrides from metadata (may be None)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Merged dict with sanitized tool names as keys and config dicts as values.
|
|
82
|
+
Only includes entries that are dicts (non-dict values are agent-wide defaults).
|
|
83
|
+
"""
|
|
84
|
+
merged: dict[str, dict[str, Any]] = {}
|
|
85
|
+
|
|
86
|
+
# Layer 1: Agent-level per-tool configs (direct keys)
|
|
87
|
+
if agent_configs:
|
|
88
|
+
_merge_config_layer(merged, agent_configs, skip_tool_configs_key=True)
|
|
89
|
+
|
|
90
|
+
# Layer 2: Agent-level per-tool configs (nested tool_configs key)
|
|
91
|
+
if agent_configs:
|
|
92
|
+
nested_agent = agent_configs.get(TOOL_CONFIGS_KEY)
|
|
93
|
+
if isinstance(nested_agent, dict):
|
|
94
|
+
_merge_config_layer(merged, nested_agent, skip_tool_configs_key=False)
|
|
95
|
+
|
|
96
|
+
# Layer 3: Runtime per-tool configs (direct keys, override agent defaults)
|
|
97
|
+
if runtime_configs:
|
|
98
|
+
_merge_config_layer(merged, runtime_configs, skip_tool_configs_key=True)
|
|
99
|
+
|
|
100
|
+
# Layer 4: Runtime per-tool configs (nested tool_configs key, highest precedence)
|
|
101
|
+
if runtime_configs:
|
|
102
|
+
nested_runtime = runtime_configs.get(TOOL_CONFIGS_KEY)
|
|
103
|
+
if isinstance(nested_runtime, dict):
|
|
104
|
+
_merge_config_layer(merged, nested_runtime, skip_tool_configs_key=False)
|
|
105
|
+
|
|
106
|
+
return merged
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class PTCCodeTool(BaseTool):
|
|
110
|
+
"""Tool for executing Python code with MCP tool access in a sandbox.
|
|
111
|
+
|
|
112
|
+
This tool uses BaseTool to properly access runtime config via run_manager.metadata.
|
|
113
|
+
The config parameter is NOT exposed to the LLM schema - it's extracted from
|
|
114
|
+
the callback manager during execution.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
name: str = "execute_ptc_code"
|
|
118
|
+
description: str = (
|
|
119
|
+
"Execute Python code that can call MCP tools programmatically. "
|
|
120
|
+
"Import tools from the generated `tools` package (e.g., `from tools.yfinance import get_stock`) "
|
|
121
|
+
"and run normal Python code. Use print() to output results. "
|
|
122
|
+
"Returns JSON with ok, stdout, stderr, and exit_code keys. "
|
|
123
|
+
"This tool is useful for chaining multiple MCP tool calls with local data processing."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Internal attributes (not exposed to LLM)
|
|
127
|
+
_ptc_executor: "PTCSandboxExecutor" = None # type: ignore[assignment]
|
|
128
|
+
_ptc_runtime: "E2BSandboxRuntime" = None # type: ignore[assignment]
|
|
129
|
+
_agent_tool_configs: dict[str, Any] | None = None
|
|
130
|
+
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
executor: "PTCSandboxExecutor",
|
|
134
|
+
runtime: "E2BSandboxRuntime",
|
|
135
|
+
agent_tool_configs: dict[str, Any] | None = None,
|
|
136
|
+
**kwargs: Any,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Initialize the PTC code tool.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
executor: The PTC sandbox executor.
|
|
142
|
+
runtime: The E2B sandbox runtime.
|
|
143
|
+
agent_tool_configs: Optional agent-level tool configs.
|
|
144
|
+
**kwargs: Additional keyword arguments passed to BaseTool.
|
|
145
|
+
"""
|
|
146
|
+
super().__init__(**kwargs)
|
|
147
|
+
# Store as private attributes to avoid Pydantic field issues
|
|
148
|
+
object.__setattr__(self, "_ptc_executor", executor)
|
|
149
|
+
object.__setattr__(self, "_ptc_runtime", runtime)
|
|
150
|
+
object.__setattr__(self, "_agent_tool_configs", agent_tool_configs)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def args(self) -> dict[str, Any]:
|
|
154
|
+
"""Return the argument schema for the tool."""
|
|
155
|
+
return {
|
|
156
|
+
"code": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"description": (
|
|
159
|
+
"Python code to execute. Import MCP tools from the generated `tools` package, "
|
|
160
|
+
"for example: `from tools.yfinance import get_stock_history`. "
|
|
161
|
+
"The code runs in a sandboxed environment with access to all configured MCP tools. "
|
|
162
|
+
"Use print() to output results. The tool returns JSON with keys: "
|
|
163
|
+
"ok, stdout, stderr, exit_code."
|
|
164
|
+
),
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
def _run(
|
|
169
|
+
self,
|
|
170
|
+
code: str,
|
|
171
|
+
run_manager: CallbackManagerForToolRun | None = None,
|
|
172
|
+
) -> str:
|
|
173
|
+
"""Execute code synchronously (wraps async version)."""
|
|
174
|
+
# Extract runtime metadata from run_manager
|
|
175
|
+
runtime_metadata = None
|
|
176
|
+
if run_manager and hasattr(run_manager, "metadata"):
|
|
177
|
+
runtime_metadata = run_manager.metadata
|
|
178
|
+
|
|
179
|
+
# Run async version in sync context
|
|
180
|
+
try:
|
|
181
|
+
asyncio.get_running_loop()
|
|
182
|
+
except RuntimeError:
|
|
183
|
+
return asyncio.run(self._execute(code, runtime_metadata))
|
|
184
|
+
|
|
185
|
+
# Already in async context - run in thread
|
|
186
|
+
def run_in_new_loop() -> str:
|
|
187
|
+
new_loop = asyncio.new_event_loop()
|
|
188
|
+
asyncio.set_event_loop(new_loop)
|
|
189
|
+
try:
|
|
190
|
+
return new_loop.run_until_complete(self._execute(code, runtime_metadata))
|
|
191
|
+
finally:
|
|
192
|
+
new_loop.close()
|
|
193
|
+
|
|
194
|
+
with concurrent.futures.ThreadPoolExecutor() as executor_service:
|
|
195
|
+
future = executor_service.submit(run_in_new_loop)
|
|
196
|
+
return future.result()
|
|
197
|
+
|
|
198
|
+
async def _arun(
|
|
199
|
+
self,
|
|
200
|
+
code: str,
|
|
201
|
+
run_manager: AsyncCallbackManagerForToolRun | None = None,
|
|
202
|
+
) -> str:
|
|
203
|
+
"""Execute code asynchronously."""
|
|
204
|
+
# Extract runtime metadata from run_manager
|
|
205
|
+
runtime_metadata = None
|
|
206
|
+
if run_manager and hasattr(run_manager, "metadata"):
|
|
207
|
+
runtime_metadata = run_manager.metadata
|
|
208
|
+
|
|
209
|
+
return await self._execute(code, runtime_metadata)
|
|
210
|
+
|
|
211
|
+
async def _execute(
|
|
212
|
+
self,
|
|
213
|
+
code: str,
|
|
214
|
+
runtime_metadata: dict[str, Any] | None,
|
|
215
|
+
) -> str:
|
|
216
|
+
"""Internal execution logic."""
|
|
217
|
+
try:
|
|
218
|
+
logger.info("Executing PTC code in sandbox")
|
|
219
|
+
result = await self._ptc_executor.execute_code(code)
|
|
220
|
+
|
|
221
|
+
if result.exit_code == 0:
|
|
222
|
+
logger.info("PTC code execution completed successfully")
|
|
223
|
+
payload = {
|
|
224
|
+
"ok": True,
|
|
225
|
+
"stdout": result.stdout,
|
|
226
|
+
"stderr": "",
|
|
227
|
+
"exit_code": 0,
|
|
228
|
+
}
|
|
229
|
+
return json.dumps(payload)
|
|
230
|
+
|
|
231
|
+
logger.warning(f"PTC code execution failed with exit code {result.exit_code}")
|
|
232
|
+
payload = {
|
|
233
|
+
"ok": False,
|
|
234
|
+
"stdout": result.stdout,
|
|
235
|
+
"stderr": result.stderr,
|
|
236
|
+
"exit_code": result.exit_code,
|
|
237
|
+
}
|
|
238
|
+
return json.dumps(payload)
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
logger.error(f"PTC code execution failed: {e}")
|
|
242
|
+
payload = {
|
|
243
|
+
"ok": False,
|
|
244
|
+
"stdout": "",
|
|
245
|
+
"stderr": f"Execution failed: {type(e).__name__}: {e}",
|
|
246
|
+
"exit_code": 1,
|
|
247
|
+
}
|
|
248
|
+
return json.dumps(payload)
|
|
249
|
+
|
|
250
|
+
async def cleanup(self) -> None:
|
|
251
|
+
"""Clean up the sandbox runtime."""
|
|
252
|
+
await self._ptc_runtime.cleanup()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def create_execute_ptc_code_tool(
|
|
256
|
+
mcp_client: "BaseMCPClient | None",
|
|
257
|
+
config: "PTCSandboxConfig | None" = None, # noqa: F821
|
|
258
|
+
agent_tool_configs: dict[str, Any] | None = None,
|
|
259
|
+
) -> PTCCodeTool:
|
|
260
|
+
r"""Create a tool that executes Python code with MCP tool access.
|
|
261
|
+
|
|
262
|
+
The code runs inside an E2B sandbox with access to generated MCP tool modules.
|
|
263
|
+
This tool is designed for LLM-generated code that needs to call multiple tools
|
|
264
|
+
programmatically in a single execution.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
mcp_client: The MCP client with configured servers.
|
|
268
|
+
config: Optional sandbox executor configuration.
|
|
269
|
+
agent_tool_configs: Optional agent-level tool configs (from agent.tool_configs).
|
|
270
|
+
These are merged with runtime overrides from RunnableConfig.metadata.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
PTCCodeTool configured for PTC code execution.
|
|
274
|
+
|
|
275
|
+
Example:
|
|
276
|
+
```python
|
|
277
|
+
from aip_agents.mcp.client import LangchainMCPClient
|
|
278
|
+
from aip_agents.tools.execute_ptc_code import create_execute_ptc_code_tool
|
|
279
|
+
|
|
280
|
+
mcp_client = LangchainMCPClient()
|
|
281
|
+
await mcp_client.add_server("yfinance", {...})
|
|
282
|
+
|
|
283
|
+
tool = create_execute_ptc_code_tool(mcp_client)
|
|
284
|
+
result = await tool.ainvoke({"code": "from tools.yfinance import get_stock\\nprint(get_stock('AAPL'))"})
|
|
285
|
+
```
|
|
286
|
+
"""
|
|
287
|
+
# Import here to avoid circular dependencies and allow lazy loading
|
|
288
|
+
from aip_agents.ptc.executor import PTCSandboxConfig, PTCSandboxExecutor
|
|
289
|
+
from aip_agents.sandbox.e2b_runtime import E2BSandboxRuntime
|
|
290
|
+
|
|
291
|
+
# Use provided config or create default
|
|
292
|
+
sandbox_config = config or PTCSandboxConfig()
|
|
293
|
+
|
|
294
|
+
# Create runtime and executor
|
|
295
|
+
runtime = E2BSandboxRuntime(
|
|
296
|
+
template=sandbox_config.sandbox_template,
|
|
297
|
+
ptc_packages=sandbox_config.ptc_packages,
|
|
298
|
+
)
|
|
299
|
+
executor = PTCSandboxExecutor(mcp_client, runtime, sandbox_config)
|
|
300
|
+
|
|
301
|
+
return PTCCodeTool(
|
|
302
|
+
executor=executor,
|
|
303
|
+
runtime=runtime,
|
|
304
|
+
agent_tool_configs=agent_tool_configs,
|
|
305
|
+
)
|