aip-agents-binary 0.5.25__py3-none-macosx_13_0_arm64.whl → 0.6.8__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.
- aip_agents/agent/__init__.py +44 -4
- aip_agents/agent/base_langgraph_agent.py +163 -74
- aip_agents/agent/base_langgraph_agent.pyi +3 -2
- aip_agents/agent/langgraph_memory_enhancer_agent.py +368 -34
- aip_agents/agent/langgraph_memory_enhancer_agent.pyi +3 -2
- aip_agents/agent/langgraph_react_agent.py +329 -22
- aip_agents/agent/langgraph_react_agent.pyi +41 -2
- aip_agents/examples/hello_world_ptc.py +49 -0
- aip_agents/examples/hello_world_ptc.pyi +5 -0
- aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
- aip_agents/examples/hello_world_ptc_custom_tools.pyi +7 -0
- aip_agents/examples/hello_world_tool_output_client.py +9 -0
- aip_agents/examples/tools/multiply_tool.py +43 -0
- aip_agents/examples/tools/multiply_tool.pyi +18 -0
- aip_agents/guardrails/engines/base.py +6 -6
- aip_agents/mcp/client/__init__.py +38 -2
- 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 +37 -2
- aip_agents/mcp/client/transports.pyi +9 -0
- aip_agents/memory/adapters/base_adapter.py +98 -0
- aip_agents/memory/adapters/base_adapter.pyi +25 -0
- aip_agents/ptc/__init__.py +87 -0
- aip_agents/ptc/__init__.pyi +14 -0
- aip_agents/ptc/custom_tools.py +473 -0
- aip_agents/ptc/custom_tools.pyi +184 -0
- aip_agents/ptc/custom_tools_payload.py +400 -0
- aip_agents/ptc/custom_tools_payload.pyi +31 -0
- aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
- aip_agents/ptc/custom_tools_templates/__init__.pyi +0 -0
- aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +23 -0
- aip_agents/ptc/custom_tools_templates/custom_init.py.template +15 -0
- aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +60 -0
- aip_agents/ptc/custom_tools_templates/custom_registry.py.template +87 -0
- aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
- aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +19 -0
- aip_agents/ptc/doc_gen.py +122 -0
- aip_agents/ptc/doc_gen.pyi +40 -0
- aip_agents/ptc/exceptions.py +57 -0
- aip_agents/ptc/exceptions.pyi +37 -0
- aip_agents/ptc/executor.py +261 -0
- aip_agents/ptc/executor.pyi +99 -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 +196 -0
- aip_agents/ptc/naming.pyi +85 -0
- aip_agents/ptc/payload.py +26 -0
- aip_agents/ptc/payload.pyi +15 -0
- aip_agents/ptc/prompt_builder.py +673 -0
- aip_agents/ptc/prompt_builder.pyi +59 -0
- aip_agents/ptc/ptc_helper.py +16 -0
- aip_agents/ptc/ptc_helper.pyi +1 -0
- aip_agents/ptc/sandbox_bridge.py +256 -0
- aip_agents/ptc/sandbox_bridge.pyi +38 -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/ptc/tool_def_helpers.py +101 -0
- aip_agents/ptc/tool_def_helpers.pyi +38 -0
- aip_agents/ptc/tool_enrichment.py +163 -0
- aip_agents/ptc/tool_enrichment.pyi +60 -0
- aip_agents/sandbox/__init__.py +43 -0
- aip_agents/sandbox/__init__.pyi +5 -0
- aip_agents/sandbox/defaults.py +205 -0
- aip_agents/sandbox/defaults.pyi +30 -0
- aip_agents/sandbox/e2b_runtime.py +295 -0
- aip_agents/sandbox/e2b_runtime.pyi +57 -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/sentry/sentry.py +29 -8
- aip_agents/sentry/sentry.pyi +3 -2
- aip_agents/tools/__init__.py +13 -2
- aip_agents/tools/__init__.pyi +3 -1
- aip_agents/tools/browser_use/browser_use_tool.py +8 -0
- aip_agents/tools/browser_use/streaming.py +2 -0
- aip_agents/tools/date_range_tool.py +554 -0
- aip_agents/tools/date_range_tool.pyi +21 -0
- aip_agents/tools/execute_ptc_code.py +357 -0
- aip_agents/tools/execute_ptc_code.pyi +90 -0
- aip_agents/tools/memory_search/__init__.py +8 -1
- aip_agents/tools/memory_search/__init__.pyi +3 -3
- aip_agents/tools/memory_search/mem0.py +114 -1
- aip_agents/tools/memory_search/mem0.pyi +11 -1
- aip_agents/tools/memory_search/schema.py +33 -0
- aip_agents/tools/memory_search/schema.pyi +10 -0
- aip_agents/tools/memory_search_tool.py +8 -0
- aip_agents/tools/memory_search_tool.pyi +2 -2
- 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.25.dist-info → aip_agents_binary-0.6.8.dist-info}/METADATA +9 -19
- {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/RECORD +107 -41
- {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/WHEEL +1 -1
- aip_agents/examples/demo_memory_recall.py +0 -401
- aip_agents/examples/demo_memory_recall.pyi +0 -58
- {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Safe invoke helper for custom tools.
|
|
2
|
+
|
|
3
|
+
This module provides a sync wrapper around tool.ainvoke for use in PTC code.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from langchain_core.tools import BaseTool
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_tool(tool: BaseTool, kwargs: dict[str, Any]) -> Any:
|
|
13
|
+
"""Run a tool's ainvoke method synchronously.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
tool: Tool instance to invoke.
|
|
17
|
+
kwargs: Arguments to pass to the tool.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Tool execution result.
|
|
21
|
+
"""
|
|
22
|
+
return _run_async_safely(tool.ainvoke(kwargs))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _run_async_safely(coro) -> Any:
|
|
26
|
+
"""Run an async coroutine safely in sync context.
|
|
27
|
+
|
|
28
|
+
Handles the case where we may or may not be in an async context.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
coro: Coroutine to run.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Result of the coroutine.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
asyncio.get_running_loop()
|
|
38
|
+
except RuntimeError:
|
|
39
|
+
# No running loop - create new one
|
|
40
|
+
return asyncio.run(coro)
|
|
41
|
+
|
|
42
|
+
# Already in async context - run in a separate thread with a new loop
|
|
43
|
+
import concurrent.futures
|
|
44
|
+
|
|
45
|
+
def run_in_new_loop():
|
|
46
|
+
"""Run coroutine in a new event loop in a thread."""
|
|
47
|
+
new_loop = asyncio.new_event_loop()
|
|
48
|
+
asyncio.set_event_loop(new_loop)
|
|
49
|
+
try:
|
|
50
|
+
result = new_loop.run_until_complete(coro)
|
|
51
|
+
new_loop.run_until_complete(new_loop.shutdown_asyncgens())
|
|
52
|
+
if hasattr(new_loop, "shutdown_default_executor"):
|
|
53
|
+
new_loop.run_until_complete(new_loop.shutdown_default_executor())
|
|
54
|
+
return result
|
|
55
|
+
finally:
|
|
56
|
+
new_loop.close()
|
|
57
|
+
|
|
58
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
59
|
+
future = executor.submit(run_in_new_loop)
|
|
60
|
+
return future.result()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Generated registry for custom tools.
|
|
2
|
+
|
|
3
|
+
This module provides factory functions to build tool instances from metadata.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from langchain_core.tools import BaseTool
|
|
11
|
+
|
|
12
|
+
$imports
|
|
13
|
+
|
|
14
|
+
_DEFAULTS_PATH = Path(__file__).with_name("custom_defaults.json")
|
|
15
|
+
|
|
16
|
+
# Attribute name for tool config schema
|
|
17
|
+
TOOL_CONFIG_SCHEMA_ATTR = "tool_config_schema"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_defaults() -> dict[str, Any]:
|
|
21
|
+
"""Load tool config defaults from JSON file."""
|
|
22
|
+
if not _DEFAULTS_PATH.exists():
|
|
23
|
+
return {}
|
|
24
|
+
return json.loads(_DEFAULTS_PATH.read_text(encoding="utf-8"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _inject_config_methods(tool: BaseTool, config_schema: type) -> None:
|
|
28
|
+
"""Inject configuration methods into a tool instance.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
tool: Tool instance to inject methods into.
|
|
32
|
+
config_schema: Pydantic model class for configuration validation.
|
|
33
|
+
"""
|
|
34
|
+
CONFIG_SCHEMA_ATTR = "_config_schema"
|
|
35
|
+
CONFIG_ATTR = "_config"
|
|
36
|
+
|
|
37
|
+
# Add configuration attributes
|
|
38
|
+
object.__setattr__(tool, CONFIG_SCHEMA_ATTR, config_schema)
|
|
39
|
+
object.__setattr__(tool, CONFIG_ATTR, None)
|
|
40
|
+
|
|
41
|
+
def set_tool_config(config: Any) -> None:
|
|
42
|
+
"""Set the tool's agent-level default configuration with validation."""
|
|
43
|
+
if config is None:
|
|
44
|
+
object.__setattr__(tool, CONFIG_ATTR, None)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
config_schema_obj = getattr(tool, CONFIG_SCHEMA_ATTR)
|
|
48
|
+
if isinstance(config, dict):
|
|
49
|
+
object.__setattr__(tool, CONFIG_ATTR, config_schema_obj(**config))
|
|
50
|
+
elif isinstance(config, config_schema_obj):
|
|
51
|
+
object.__setattr__(tool, CONFIG_ATTR, config)
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError(f"Config must be an instance of {config_schema_obj.__name__} or dict")
|
|
54
|
+
|
|
55
|
+
def get_tool_config(config: Any = None) -> Any:
|
|
56
|
+
"""Get the effective tool configuration."""
|
|
57
|
+
return getattr(tool, CONFIG_ATTR)
|
|
58
|
+
|
|
59
|
+
# Inject methods
|
|
60
|
+
object.__setattr__(tool, "set_tool_config", set_tool_config)
|
|
61
|
+
object.__setattr__(tool, "get_tool_config", get_tool_config)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _configure_tool(tool: BaseTool, tool_name: str) -> None:
|
|
65
|
+
"""Configure tool with defaults if it has tool_config_schema.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
tool: Tool instance to configure.
|
|
69
|
+
tool_name: Name of the tool for defaults lookup.
|
|
70
|
+
"""
|
|
71
|
+
defaults_by_tool = _load_defaults()
|
|
72
|
+
has_defaults = tool_name in defaults_by_tool
|
|
73
|
+
defaults = defaults_by_tool.get(tool_name)
|
|
74
|
+
|
|
75
|
+
# Check if tool has tool_config_schema
|
|
76
|
+
if hasattr(tool, TOOL_CONFIG_SCHEMA_ATTR):
|
|
77
|
+
# Inject config methods if not present
|
|
78
|
+
if not hasattr(tool, "get_tool_config"):
|
|
79
|
+
config_schema = getattr(tool, TOOL_CONFIG_SCHEMA_ATTR)
|
|
80
|
+
_inject_config_methods(tool, config_schema)
|
|
81
|
+
|
|
82
|
+
# Set tool config when defaults explicitly provided (even empty dict)
|
|
83
|
+
if has_defaults and hasattr(tool, "set_tool_config"):
|
|
84
|
+
tool.set_tool_config(defaults)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
$build_functions
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Generated wrapper for custom tool: $original_name
|
|
2
|
+
|
|
3
|
+
This module provides a sync wrapper for calling the tool in PTC code.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from tools.custom_registry import build_$sanitized_name
|
|
7
|
+
from tools.custom_invoke import run_tool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def $sanitized_name(**kwargs):
|
|
11
|
+
"""Call the $original_name tool.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
**kwargs: Arguments to pass to the tool.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Tool execution result.
|
|
18
|
+
"""
|
|
19
|
+
return run_tool(build_$sanitized_name(), kwargs)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Documentation generation utilities for PTC.
|
|
2
|
+
|
|
3
|
+
Shared constants and helpers for generating tool documentation in sandbox payloads.
|
|
4
|
+
|
|
5
|
+
Authors:
|
|
6
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from aip_agents.ptc.naming import sanitize_function_name
|
|
12
|
+
|
|
13
|
+
# Documentation limits (fixed constants per plan)
|
|
14
|
+
DOC_DESC_LIMIT = 120 # Tool description trim limit
|
|
15
|
+
DOC_PARAM_DESC_LIMIT = 80 # Parameter description trim limit
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def json_type_to_display(json_type: Any) -> str:
|
|
19
|
+
"""Convert JSON type to display string.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
json_type: JSON schema type.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Human-readable type string.
|
|
26
|
+
"""
|
|
27
|
+
if isinstance(json_type, list):
|
|
28
|
+
return "any"
|
|
29
|
+
type_map = {
|
|
30
|
+
"string": "str",
|
|
31
|
+
"integer": "int",
|
|
32
|
+
"number": "float",
|
|
33
|
+
"boolean": "bool",
|
|
34
|
+
"array": "list",
|
|
35
|
+
"object": "dict",
|
|
36
|
+
"null": "None",
|
|
37
|
+
}
|
|
38
|
+
return type_map.get(str(json_type), "any")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def trim_text(text: str | None, limit: int) -> str:
|
|
42
|
+
"""Trim text to limit with ellipsis if needed.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
text: Text to trim.
|
|
46
|
+
limit: Maximum length before trimming.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Trimmed text.
|
|
50
|
+
"""
|
|
51
|
+
if not text:
|
|
52
|
+
return ""
|
|
53
|
+
if len(text) > limit:
|
|
54
|
+
return text[:limit] + "..."
|
|
55
|
+
return text
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def render_tool_doc(
|
|
59
|
+
func_name: str,
|
|
60
|
+
signature: str,
|
|
61
|
+
description: str,
|
|
62
|
+
schema: dict[str, Any],
|
|
63
|
+
is_stub: bool = False,
|
|
64
|
+
example_code: str | None = None,
|
|
65
|
+
example_heading: str = "## Example",
|
|
66
|
+
) -> str:
|
|
67
|
+
"""Render markdown documentation for a tool.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
func_name: Sanitized function name.
|
|
71
|
+
signature: Full function signature.
|
|
72
|
+
description: Tool description.
|
|
73
|
+
schema: Input schema for parameters.
|
|
74
|
+
is_stub: Whether this is a stub documentation.
|
|
75
|
+
example_code: Optional example code block content (without ```python).
|
|
76
|
+
example_heading: Heading for the example section.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Markdown documentation string.
|
|
80
|
+
"""
|
|
81
|
+
if is_stub:
|
|
82
|
+
desc = "Details unavailable because tool definitions are not loaded yet."
|
|
83
|
+
else:
|
|
84
|
+
desc = trim_text(description, DOC_DESC_LIMIT) or "No description available."
|
|
85
|
+
|
|
86
|
+
lines = [
|
|
87
|
+
f"# {func_name}",
|
|
88
|
+
"",
|
|
89
|
+
f"**Description:** {desc}",
|
|
90
|
+
"",
|
|
91
|
+
f"**Signature:** `{signature}`",
|
|
92
|
+
"",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
# Add parameters section
|
|
96
|
+
properties = schema.get("properties", {})
|
|
97
|
+
required = set(schema.get("required", []))
|
|
98
|
+
|
|
99
|
+
if properties:
|
|
100
|
+
lines.append("## Parameters")
|
|
101
|
+
lines.append("")
|
|
102
|
+
for prop_name, prop_schema in sorted(properties.items()):
|
|
103
|
+
safe_param = sanitize_function_name(prop_name)
|
|
104
|
+
prop_type = json_type_to_display(prop_schema.get("type", "any"))
|
|
105
|
+
is_required = "required" if prop_name in required else "optional"
|
|
106
|
+
|
|
107
|
+
# Trim param description
|
|
108
|
+
raw_param_desc = prop_schema.get("description", "")
|
|
109
|
+
param_desc = trim_text(raw_param_desc, DOC_PARAM_DESC_LIMIT)
|
|
110
|
+
|
|
111
|
+
lines.append(f"- **{safe_param}** ({prop_type}, {is_required}): {param_desc}")
|
|
112
|
+
lines.append("")
|
|
113
|
+
|
|
114
|
+
# Add example section
|
|
115
|
+
if example_code:
|
|
116
|
+
lines.append(example_heading)
|
|
117
|
+
lines.append("")
|
|
118
|
+
lines.append("```python")
|
|
119
|
+
lines.append(example_code)
|
|
120
|
+
lines.append("```")
|
|
121
|
+
|
|
122
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from aip_agents.ptc.naming import sanitize_function_name as sanitize_function_name
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
DOC_DESC_LIMIT: int
|
|
5
|
+
DOC_PARAM_DESC_LIMIT: int
|
|
6
|
+
|
|
7
|
+
def json_type_to_display(json_type: Any) -> str:
|
|
8
|
+
"""Convert JSON type to display string.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
json_type: JSON schema type.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Human-readable type string.
|
|
15
|
+
"""
|
|
16
|
+
def trim_text(text: str | None, limit: int) -> str:
|
|
17
|
+
"""Trim text to limit with ellipsis if needed.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
text: Text to trim.
|
|
21
|
+
limit: Maximum length before trimming.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Trimmed text.
|
|
25
|
+
"""
|
|
26
|
+
def render_tool_doc(func_name: str, signature: str, description: str, schema: dict[str, Any], is_stub: bool = False, example_code: str | None = None, example_heading: str = '## Example') -> str:
|
|
27
|
+
"""Render markdown documentation for a tool.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
func_name: Sanitized function name.
|
|
31
|
+
signature: Full function signature.
|
|
32
|
+
description: Tool description.
|
|
33
|
+
schema: Input schema for parameters.
|
|
34
|
+
is_stub: Whether this is a stub documentation.
|
|
35
|
+
example_code: Optional example code block content (without ```python).
|
|
36
|
+
example_heading: Heading for the example section.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Markdown documentation string.
|
|
40
|
+
"""
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""PTC-specific exceptions.
|
|
2
|
+
|
|
3
|
+
This module defines exceptions for Programmatic Tool Calling operations.
|
|
4
|
+
|
|
5
|
+
Authors:
|
|
6
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PTCError(Exception):
|
|
11
|
+
"""Base exception for PTC errors."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PTCToolError(PTCError):
|
|
17
|
+
"""Error during tool execution.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
server_name: The MCP server where the error occurred.
|
|
21
|
+
tool_name: The tool that failed.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
message: str,
|
|
27
|
+
server_name: str | None = None,
|
|
28
|
+
tool_name: str | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Initialize PTCToolError.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
message: Error message.
|
|
34
|
+
server_name: The MCP server name (optional).
|
|
35
|
+
tool_name: The tool name (optional).
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(message)
|
|
38
|
+
self.server_name = server_name
|
|
39
|
+
self.tool_name = tool_name
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PTCPayloadConflictError(PTCError):
|
|
43
|
+
"""Error when merging payloads with conflicting file paths.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
conflicts: Set of conflicting file paths.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, message: str, conflicts: set[str]) -> None:
|
|
50
|
+
"""Initialize PTCPayloadConflictError.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
message: Error message.
|
|
54
|
+
conflicts: Set of conflicting file paths.
|
|
55
|
+
"""
|
|
56
|
+
super().__init__(message)
|
|
57
|
+
self.conflicts = conflicts
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from _typeshed import Incomplete
|
|
2
|
+
|
|
3
|
+
class PTCError(Exception):
|
|
4
|
+
"""Base exception for PTC errors."""
|
|
5
|
+
|
|
6
|
+
class PTCToolError(PTCError):
|
|
7
|
+
"""Error during tool execution.
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
server_name: The MCP server where the error occurred.
|
|
11
|
+
tool_name: The tool that failed.
|
|
12
|
+
"""
|
|
13
|
+
server_name: Incomplete
|
|
14
|
+
tool_name: Incomplete
|
|
15
|
+
def __init__(self, message: str, server_name: str | None = None, tool_name: str | None = None) -> None:
|
|
16
|
+
"""Initialize PTCToolError.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
message: Error message.
|
|
20
|
+
server_name: The MCP server name (optional).
|
|
21
|
+
tool_name: The tool name (optional).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
class PTCPayloadConflictError(PTCError):
|
|
25
|
+
"""Error when merging payloads with conflicting file paths.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
conflicts: Set of conflicting file paths.
|
|
29
|
+
"""
|
|
30
|
+
conflicts: Incomplete
|
|
31
|
+
def __init__(self, message: str, conflicts: set[str]) -> None:
|
|
32
|
+
"""Initialize PTCPayloadConflictError.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
message: Error message.
|
|
36
|
+
conflicts: Set of conflicting file paths.
|
|
37
|
+
"""
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""PTC Executor implementations.
|
|
2
|
+
|
|
3
|
+
This module provides the sandboxed executor for Programmatic Tool Calling.
|
|
4
|
+
|
|
5
|
+
Authors:
|
|
6
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from aip_agents.mcp.client.base_mcp_client import BaseMCPClient
|
|
16
|
+
from aip_agents.ptc.custom_tools import PTCCustomToolConfig
|
|
17
|
+
from aip_agents.ptc.exceptions import PTCToolError
|
|
18
|
+
from aip_agents.ptc.prompt_builder import PromptConfig
|
|
19
|
+
from aip_agents.sandbox.defaults import DEFAULT_PTC_PACKAGES, DEFAULT_PTC_TEMPLATE
|
|
20
|
+
from aip_agents.utils.logger import get_logger
|
|
21
|
+
|
|
22
|
+
# Lazy import to avoid circular dependencies
|
|
23
|
+
# These are only needed for PTCSandboxExecutor
|
|
24
|
+
try:
|
|
25
|
+
from aip_agents.ptc.sandbox_bridge import build_sandbox_payload, wrap_ptc_code
|
|
26
|
+
from aip_agents.sandbox.e2b_runtime import E2BSandboxRuntime
|
|
27
|
+
from aip_agents.sandbox.types import SandboxExecutionResult
|
|
28
|
+
|
|
29
|
+
_SANDBOX_DEPS_AVAILABLE = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
_SANDBOX_DEPS_AVAILABLE = False
|
|
32
|
+
|
|
33
|
+
logger = get_logger(__name__)
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from aip_agents.ptc.payload import SandboxPayload
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class PTCSandboxConfig:
|
|
41
|
+
"""Configuration for PTC sandbox executor.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
enabled: Whether PTC is enabled. When False, PTC is disabled.
|
|
45
|
+
default_tool_timeout: Default timeout per tool call in seconds.
|
|
46
|
+
sandbox_template: Optional E2B sandbox template ID.
|
|
47
|
+
sandbox_timeout: Sandbox execution timeout in seconds (hard cap/TTL).
|
|
48
|
+
ptc_packages: List of packages to install in sandbox. Defaults to DEFAULT_PTC_PACKAGES.
|
|
49
|
+
prompt: Prompt configuration for PTC usage guidance.
|
|
50
|
+
custom_tools: Configuration for custom LangChain tools in sandbox.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
enabled: bool = False
|
|
54
|
+
default_tool_timeout: float = 60.0
|
|
55
|
+
sandbox_template: str | None = DEFAULT_PTC_TEMPLATE
|
|
56
|
+
sandbox_timeout: float = 300.0
|
|
57
|
+
ptc_packages: list[str] | None = field(default_factory=lambda: list(DEFAULT_PTC_PACKAGES))
|
|
58
|
+
prompt: PromptConfig = field(default_factory=PromptConfig)
|
|
59
|
+
custom_tools: PTCCustomToolConfig = field(default_factory=PTCCustomToolConfig)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PTCSandboxExecutor:
|
|
63
|
+
r"""Executes PTC code inside an E2B sandbox.
|
|
64
|
+
|
|
65
|
+
This executor is used for LLM-generated code that requires sandboxing.
|
|
66
|
+
It builds a sandbox payload (MCP server config + generated tool modules)
|
|
67
|
+
and executes the code using the E2B runtime.
|
|
68
|
+
|
|
69
|
+
Static bundle caching:
|
|
70
|
+
The executor tracks whether the static bundle (wrappers, registry, sources)
|
|
71
|
+
has been uploaded. On the first run, it uploads the full payload. On subsequent
|
|
72
|
+
runs, it only uploads per-run files (e.g., tools/custom_defaults.json) to
|
|
73
|
+
reduce upload overhead.
|
|
74
|
+
|
|
75
|
+
If the sandbox is destroyed/recreated, call reset_static_bundle() to force
|
|
76
|
+
a full re-upload on the next execution.
|
|
77
|
+
|
|
78
|
+
Thread-safety:
|
|
79
|
+
The static bundle upload is guarded by an asyncio.Lock. Concurrent calls will
|
|
80
|
+
serialize while the initial upload completes. After the bundle is cached,
|
|
81
|
+
executions proceed without locking.
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
runtime = E2BSandboxRuntime()
|
|
85
|
+
executor = PTCSandboxExecutor(mcp_client, runtime)
|
|
86
|
+
result = await executor.execute_code("from tools.yfinance import get_stock\\nprint(get_stock('AAPL'))")
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
mcp_client: BaseMCPClient | None,
|
|
92
|
+
runtime: E2BSandboxRuntime,
|
|
93
|
+
config: PTCSandboxConfig | None = None,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Initialize PTCSandboxExecutor.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
mcp_client: The MCP client with configured servers. Can be None for custom-only configs.
|
|
99
|
+
runtime: The E2B sandbox runtime instance.
|
|
100
|
+
config: Optional sandbox executor configuration.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
ImportError: If sandbox dependencies are not available.
|
|
104
|
+
"""
|
|
105
|
+
if not _SANDBOX_DEPS_AVAILABLE:
|
|
106
|
+
raise ImportError(
|
|
107
|
+
"Sandbox dependencies not available. "
|
|
108
|
+
"PTCSandboxExecutor requires sandbox_bridge and e2b_runtime modules."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self._mcp_client = mcp_client
|
|
112
|
+
self._runtime = runtime
|
|
113
|
+
self._config = config or PTCSandboxConfig()
|
|
114
|
+
self._static_bundle_uploaded = False
|
|
115
|
+
self._bundle_lock = asyncio.Lock()
|
|
116
|
+
|
|
117
|
+
def _reset_bundle_state_if_inactive(self) -> None:
|
|
118
|
+
"""Reset cached bundle state when the runtime is inactive."""
|
|
119
|
+
if self._runtime.is_active:
|
|
120
|
+
return
|
|
121
|
+
if not self._static_bundle_uploaded:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
logger.info("Runtime is inactive, resetting static bundle state")
|
|
125
|
+
self._static_bundle_uploaded = False
|
|
126
|
+
|
|
127
|
+
def _should_include_packages_path(self) -> bool:
|
|
128
|
+
"""Check if the packages path should be added to sys.path."""
|
|
129
|
+
custom_tools = self._config.custom_tools
|
|
130
|
+
if not custom_tools.enabled or not custom_tools.tools:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
return any(tool.get("package_path") or tool.get("kind") == "file" for tool in custom_tools.tools)
|
|
134
|
+
|
|
135
|
+
async def _build_payload(self, tool_configs: dict[str, dict] | None) -> SandboxPayload:
|
|
136
|
+
"""Build the sandbox payload for execution."""
|
|
137
|
+
logger.info("Building sandbox payload")
|
|
138
|
+
return await build_sandbox_payload(
|
|
139
|
+
self._mcp_client,
|
|
140
|
+
self._config.default_tool_timeout,
|
|
141
|
+
custom_tools_config=self._config.custom_tools,
|
|
142
|
+
tool_configs=tool_configs,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def _wrap_code(self, code: str) -> str:
|
|
146
|
+
"""Wrap code with required imports and setup."""
|
|
147
|
+
logger.info("Wrapping PTC code")
|
|
148
|
+
return wrap_ptc_code(code, include_packages_path=self._should_include_packages_path())
|
|
149
|
+
|
|
150
|
+
async def _execute_payload(
|
|
151
|
+
self,
|
|
152
|
+
payload: SandboxPayload,
|
|
153
|
+
wrapped_code: str,
|
|
154
|
+
*,
|
|
155
|
+
upload_static_bundle: bool,
|
|
156
|
+
) -> SandboxExecutionResult:
|
|
157
|
+
"""Execute the wrapped code with the provided payload."""
|
|
158
|
+
if upload_static_bundle:
|
|
159
|
+
logger.info("Uploading static bundle and per-run files")
|
|
160
|
+
files_to_upload = {**payload.files, **payload.per_run_files}
|
|
161
|
+
else:
|
|
162
|
+
logger.debug("Static bundle already uploaded, uploading only per-run files")
|
|
163
|
+
files_to_upload = payload.per_run_files
|
|
164
|
+
|
|
165
|
+
logger.info(f"Executing code in sandbox (timeout: {self._config.sandbox_timeout}s)")
|
|
166
|
+
return await self._runtime.execute(
|
|
167
|
+
code=wrapped_code,
|
|
168
|
+
timeout=self._config.sandbox_timeout,
|
|
169
|
+
files=files_to_upload if files_to_upload else None,
|
|
170
|
+
env=payload.env,
|
|
171
|
+
template=self._config.sandbox_template,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
async def _execute_with_bundle(
|
|
175
|
+
self,
|
|
176
|
+
payload: SandboxPayload,
|
|
177
|
+
wrapped_code: str,
|
|
178
|
+
) -> SandboxExecutionResult:
|
|
179
|
+
"""Execute code using the cached static bundle when possible."""
|
|
180
|
+
if self._static_bundle_uploaded:
|
|
181
|
+
return await self._execute_payload(
|
|
182
|
+
payload,
|
|
183
|
+
wrapped_code,
|
|
184
|
+
upload_static_bundle=False,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
async with self._bundle_lock:
|
|
188
|
+
if self._static_bundle_uploaded:
|
|
189
|
+
return await self._execute_payload(
|
|
190
|
+
payload,
|
|
191
|
+
wrapped_code,
|
|
192
|
+
upload_static_bundle=False,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
result = await self._execute_payload(
|
|
196
|
+
payload,
|
|
197
|
+
wrapped_code,
|
|
198
|
+
upload_static_bundle=True,
|
|
199
|
+
)
|
|
200
|
+
self._update_static_bundle_cache(result)
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
def _update_static_bundle_cache(self, result: SandboxExecutionResult) -> None:
|
|
204
|
+
"""Update cached bundle state after a successful upload."""
|
|
205
|
+
if result.exit_code != 0:
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
self._static_bundle_uploaded = True
|
|
209
|
+
logger.debug("Static bundle successfully uploaded and cached")
|
|
210
|
+
|
|
211
|
+
def _log_execution_result(self, result: SandboxExecutionResult) -> None:
|
|
212
|
+
"""Log execution results based on exit status."""
|
|
213
|
+
if result.exit_code == 0:
|
|
214
|
+
logger.info("Sandbox execution completed successfully")
|
|
215
|
+
else:
|
|
216
|
+
logger.warning(f"Sandbox execution failed with exit code {result.exit_code}")
|
|
217
|
+
|
|
218
|
+
async def execute_code(
|
|
219
|
+
self,
|
|
220
|
+
code: str,
|
|
221
|
+
tool_configs: dict[str, dict] | None = None,
|
|
222
|
+
) -> SandboxExecutionResult:
|
|
223
|
+
"""Execute code inside the sandbox with MCP access.
|
|
224
|
+
|
|
225
|
+
This method:
|
|
226
|
+
1. Builds the sandbox payload (MCP config + generated tool modules + custom tools)
|
|
227
|
+
2. Wraps the user code with necessary imports and setup
|
|
228
|
+
3. Executes the code in the E2B sandbox
|
|
229
|
+
4. Returns the execution result (stdout/stderr/exit_code)
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
code: Python code to execute in the sandbox.
|
|
233
|
+
tool_configs: Optional per-tool config values for custom LangChain tools.
|
|
234
|
+
These are merged with agent defaults and written to custom_defaults.json.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
SandboxExecutionResult with stdout, stderr, and exit_code.
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
PTCToolError: If sandbox execution fails.
|
|
241
|
+
"""
|
|
242
|
+
try:
|
|
243
|
+
self._reset_bundle_state_if_inactive()
|
|
244
|
+
payload = await self._build_payload(tool_configs)
|
|
245
|
+
wrapped_code = self._wrap_code(code)
|
|
246
|
+
result = await self._execute_with_bundle(payload, wrapped_code)
|
|
247
|
+
self._log_execution_result(result)
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
except Exception as exc:
|
|
251
|
+
logger.error(f"Sandbox execution failed: {exc}")
|
|
252
|
+
raise PTCToolError(f"Sandbox execution failed: {exc}") from exc
|
|
253
|
+
|
|
254
|
+
def reset_static_bundle(self) -> None:
|
|
255
|
+
"""Reset the static bundle upload state.
|
|
256
|
+
|
|
257
|
+
Call this method when the sandbox is destroyed/recreated to force
|
|
258
|
+
a full re-upload of the static bundle on the next execution.
|
|
259
|
+
"""
|
|
260
|
+
self._static_bundle_uploaded = False
|
|
261
|
+
logger.debug("Static bundle state reset, next execution will re-upload full payload")
|