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,184 @@
|
|
|
1
|
+
from _typeshed import Incomplete
|
|
2
|
+
from aip_agents.ptc.exceptions import PTCError as PTCError
|
|
3
|
+
from aip_agents.ptc.naming import is_valid_identifier as is_valid_identifier, sanitize_function_name as sanitize_function_name
|
|
4
|
+
from aip_agents.utils.logger import get_logger as get_logger
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal, TypedDict
|
|
8
|
+
|
|
9
|
+
logger: Incomplete
|
|
10
|
+
|
|
11
|
+
class PTCPackageToolDef(TypedDict, total=False):
|
|
12
|
+
'''Definition for a package-based tool.
|
|
13
|
+
|
|
14
|
+
Required fields:
|
|
15
|
+
name: Tool name (used for imports and config lookup).
|
|
16
|
+
kind: Must be "package".
|
|
17
|
+
import_path: Dotted import path (e.g., "aip_agents.tools.time_tool").
|
|
18
|
+
class_name: Class name to import (e.g., "TimeTool").
|
|
19
|
+
|
|
20
|
+
Optional fields:
|
|
21
|
+
package_path: Path to source directory to bundle.
|
|
22
|
+
If omitted, the tool must already be available in sandbox (e.g., from site-packages).
|
|
23
|
+
description: Tool description (derived from tool object at construction time).
|
|
24
|
+
input_schema: JSON schema for tool input (derived from tool object at construction time).
|
|
25
|
+
'''
|
|
26
|
+
name: str
|
|
27
|
+
kind: Literal['package']
|
|
28
|
+
import_path: str
|
|
29
|
+
class_name: str
|
|
30
|
+
package_path: str
|
|
31
|
+
description: str
|
|
32
|
+
input_schema: dict
|
|
33
|
+
|
|
34
|
+
class PTCFileToolDef(TypedDict, total=False):
|
|
35
|
+
'''Definition for a single-file tool.
|
|
36
|
+
|
|
37
|
+
Required fields:
|
|
38
|
+
name: Tool name (used for imports and config lookup).
|
|
39
|
+
kind: Must be "file".
|
|
40
|
+
file_path: Path to the Python file containing the tool.
|
|
41
|
+
class_name: Class name to import from the file.
|
|
42
|
+
|
|
43
|
+
Optional fields:
|
|
44
|
+
description: Tool description (derived from tool object at construction time).
|
|
45
|
+
input_schema: JSON schema for tool input (derived from tool object at construction time).
|
|
46
|
+
'''
|
|
47
|
+
name: str
|
|
48
|
+
kind: Literal['file']
|
|
49
|
+
file_path: str
|
|
50
|
+
class_name: str
|
|
51
|
+
description: str
|
|
52
|
+
input_schema: dict
|
|
53
|
+
PTCToolDef = PTCPackageToolDef | PTCFileToolDef
|
|
54
|
+
|
|
55
|
+
class PTCCustomToolValidationError(PTCError):
|
|
56
|
+
"""Error raised when custom tool configuration is invalid."""
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class PTCCustomToolConfig:
|
|
60
|
+
'''Configuration for custom LangChain tools in PTC sandbox.
|
|
61
|
+
|
|
62
|
+
This config controls which user-defined LangChain tools are available
|
|
63
|
+
in the sandbox for use by PTC code.
|
|
64
|
+
|
|
65
|
+
Attributes:
|
|
66
|
+
enabled: Whether custom tools are enabled.
|
|
67
|
+
bundle_roots: List of allowed root directories for bundling tool sources.
|
|
68
|
+
Paths outside these roots will cause validation errors.
|
|
69
|
+
requirements: List of pip requirements to install in sandbox.
|
|
70
|
+
tools: List of tool definitions (package or file tools).
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
>>> config = PTCCustomToolConfig(
|
|
74
|
+
... enabled=True,
|
|
75
|
+
... bundle_roots=["/app/tools"],
|
|
76
|
+
... requirements=["pydantic>=2.0"],
|
|
77
|
+
... tools=[
|
|
78
|
+
... {
|
|
79
|
+
... "name": "time_tool",
|
|
80
|
+
... "kind": "package",
|
|
81
|
+
... "import_path": "aip_agents.tools.time_tool",
|
|
82
|
+
... "class_name": "TimeTool",
|
|
83
|
+
... },
|
|
84
|
+
... ],
|
|
85
|
+
... )
|
|
86
|
+
'''
|
|
87
|
+
enabled: bool = ...
|
|
88
|
+
bundle_roots: list[str] = field(default_factory=list)
|
|
89
|
+
requirements: list[str] = field(default_factory=list)
|
|
90
|
+
tools: list[PTCToolDef] = field(default_factory=list)
|
|
91
|
+
|
|
92
|
+
def validate_tool_def(tool: PTCToolDef) -> None:
|
|
93
|
+
"""Validate a tool definition has required fields.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
tool: Tool definition to validate.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
PTCCustomToolValidationError: If tool definition is invalid.
|
|
100
|
+
"""
|
|
101
|
+
def validate_path_within_bundle_roots(path: str | Path, bundle_roots: list[str], tool_name: str) -> None:
|
|
102
|
+
"""Validate that a path is within one of the bundle roots.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
path: Path to validate.
|
|
106
|
+
bundle_roots: List of allowed root directories.
|
|
107
|
+
tool_name: Tool name for error messages.
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
PTCCustomToolValidationError: If path is outside all bundle roots.
|
|
111
|
+
"""
|
|
112
|
+
def detect_relative_imports(file_path: str | Path) -> list[str]:
|
|
113
|
+
"""Detect relative imports in a Python file using AST.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
file_path: Path to the Python file to scan.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
List of relative import statements found.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
PTCCustomToolValidationError: If file cannot be read or parsed.
|
|
123
|
+
"""
|
|
124
|
+
def check_name_collisions(tools: list[PTCToolDef]) -> None:
|
|
125
|
+
"""Check for tool name collisions after sanitization.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
tools: List of tool definitions.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
PTCCustomToolValidationError: If two tools sanitize to the same name.
|
|
132
|
+
"""
|
|
133
|
+
def validate_custom_tool_config(config: PTCCustomToolConfig) -> None:
|
|
134
|
+
"""Validate the complete custom tool configuration.
|
|
135
|
+
|
|
136
|
+
This runs all validation checks:
|
|
137
|
+
- Tool definitions have required fields
|
|
138
|
+
- Paths are within bundle roots
|
|
139
|
+
- File tools don't use relative imports
|
|
140
|
+
- No name collisions
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
config: Custom tool configuration to validate.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
PTCCustomToolValidationError: If configuration is invalid.
|
|
147
|
+
"""
|
|
148
|
+
def extract_tool_metadata(tool: object) -> dict:
|
|
149
|
+
'''Extract metadata from a LangChain BaseTool instance.
|
|
150
|
+
|
|
151
|
+
Extracts name, description, and input_schema from the tool object.
|
|
152
|
+
This is called at agent construction time to populate tool definitions.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
tool: A LangChain BaseTool instance (or compatible object).
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Dict with keys:
|
|
159
|
+
- name: str (tool name)
|
|
160
|
+
- description: str (tool description, empty if not available)
|
|
161
|
+
- input_schema: dict (JSON schema, empty object schema if not available)
|
|
162
|
+
|
|
163
|
+
Note:
|
|
164
|
+
If schema inference fails, returns an empty object schema
|
|
165
|
+
({"type": "object", "properties": {}}) so prompt/index output
|
|
166
|
+
falls back to tool(**kwargs).
|
|
167
|
+
'''
|
|
168
|
+
def enrich_tool_def_with_metadata(tool_def: PTCToolDef, tool: object) -> PTCToolDef:
|
|
169
|
+
"""Enrich a tool definition with metadata extracted from the tool object.
|
|
170
|
+
|
|
171
|
+
Always derives description and input_schema from the tool object, overwriting
|
|
172
|
+
any user-supplied values. This ensures the tool object is the source of truth.
|
|
173
|
+
Called at agent construction time.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
tool_def: The tool definition to enrich.
|
|
177
|
+
tool: The LangChain BaseTool instance.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
The tool definition with description and input_schema populated from the tool object.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
PTCCustomToolValidationError: If tool name in metadata does not match tool_def name.
|
|
184
|
+
"""
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Custom LangChain tools payload generation for PTC sandbox.
|
|
2
|
+
|
|
3
|
+
This module generates the sandbox payload (wrapper modules, registry, sources)
|
|
4
|
+
that allows LLM-generated code to call custom LangChain tools inside the sandbox.
|
|
5
|
+
|
|
6
|
+
The payload includes:
|
|
7
|
+
- tools/custom/__init__.py: Package init re-exporting all tool wrappers
|
|
8
|
+
- tools/custom/<tool_name>.py: Per-tool wrapper module with sync call function
|
|
9
|
+
- tools/custom_sources/__init__.py: Package init for file tool sources
|
|
10
|
+
- tools/custom_sources/<tool_name>/__init__.py: File tool source (copied from user)
|
|
11
|
+
- tools/custom_registry.py: Factory functions to build tool instances
|
|
12
|
+
- tools/custom_invoke.py: Safe invoke helper for calling tool.ainvoke
|
|
13
|
+
- tools/custom_defaults.json: Per-run tool config (uploaded each run)
|
|
14
|
+
|
|
15
|
+
Authors:
|
|
16
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from aip_agents.ptc.custom_tools import (
|
|
26
|
+
PTCCustomToolConfig,
|
|
27
|
+
PTCToolDef,
|
|
28
|
+
)
|
|
29
|
+
from aip_agents.ptc.naming import sanitize_function_name
|
|
30
|
+
from aip_agents.ptc.payload import SandboxPayload
|
|
31
|
+
from aip_agents.ptc.template_utils import render_template
|
|
32
|
+
from aip_agents.utils.logger import get_logger
|
|
33
|
+
|
|
34
|
+
_TEMPLATE_PACKAGE = "aip_agents.ptc.custom_tools_templates"
|
|
35
|
+
|
|
36
|
+
logger = get_logger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class CustomToolPayloadResult:
|
|
41
|
+
"""Result of custom tool payload generation.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
payload: The sandbox payload with files to upload.
|
|
45
|
+
tool_names: List of sanitized tool names available in sandbox.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
payload: SandboxPayload
|
|
49
|
+
tool_names: list[str] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _validate_tool_configs(
|
|
53
|
+
tool_configs: dict[str, dict] | None,
|
|
54
|
+
configured_tools: list[PTCToolDef],
|
|
55
|
+
) -> dict[str, dict]:
|
|
56
|
+
"""Validate tool_configs and return sanitized values.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
tool_configs: Optional per-tool config values to validate.
|
|
60
|
+
configured_tools: List of configured tool definitions.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Validated tool_configs with only known tools and JSON-serializable dict values.
|
|
64
|
+
"""
|
|
65
|
+
if not tool_configs:
|
|
66
|
+
return {}
|
|
67
|
+
|
|
68
|
+
if not isinstance(tool_configs, dict):
|
|
69
|
+
logger.warning("tool_configs must be a dict mapping tool names to dict values")
|
|
70
|
+
return {}
|
|
71
|
+
|
|
72
|
+
configured_names = {t.get("name", "") for t in configured_tools}
|
|
73
|
+
validated: dict[str, dict] = {}
|
|
74
|
+
|
|
75
|
+
for tool_name, tool_config in tool_configs.items():
|
|
76
|
+
if not isinstance(tool_name, str):
|
|
77
|
+
logger.warning(f"tool_configs contains non-string tool name: {tool_name!r}")
|
|
78
|
+
continue
|
|
79
|
+
if tool_name not in configured_names:
|
|
80
|
+
logger.warning(f"tool_configs contains config for unknown tool: {tool_name}")
|
|
81
|
+
continue
|
|
82
|
+
if not isinstance(tool_config, dict):
|
|
83
|
+
logger.warning(f"tool_configs for tool '{tool_name}' must be a dict")
|
|
84
|
+
continue
|
|
85
|
+
if any(not isinstance(key, str) for key in tool_config.keys()):
|
|
86
|
+
logger.warning(f"tool_configs for tool '{tool_name}' must use string keys")
|
|
87
|
+
continue
|
|
88
|
+
try:
|
|
89
|
+
json.dumps(tool_config)
|
|
90
|
+
except (TypeError, ValueError) as exc:
|
|
91
|
+
logger.warning(f"tool_configs for tool '{tool_name}' must be JSON-serializable: {exc}")
|
|
92
|
+
continue
|
|
93
|
+
validated[tool_name] = tool_config
|
|
94
|
+
|
|
95
|
+
return validated
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def build_custom_tools_payload(
|
|
99
|
+
config: PTCCustomToolConfig,
|
|
100
|
+
tool_configs: dict[str, dict] | None = None,
|
|
101
|
+
) -> CustomToolPayloadResult:
|
|
102
|
+
"""Build sandbox payload for custom LangChain tools.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
config: Custom tool configuration with tool definitions.
|
|
106
|
+
tool_configs: Optional per-tool config values to write to defaults.json.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
CustomToolPayloadResult with payload and available tool names.
|
|
110
|
+
"""
|
|
111
|
+
result = CustomToolPayloadResult(payload=SandboxPayload())
|
|
112
|
+
|
|
113
|
+
if not config.enabled or not config.tools:
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
# Collect sanitized tool names
|
|
117
|
+
sanitized_tools: list[tuple[str, PTCToolDef]] = []
|
|
118
|
+
for tool in config.tools:
|
|
119
|
+
name = tool.get("name", "")
|
|
120
|
+
sanitized_name = sanitize_function_name(name)
|
|
121
|
+
sanitized_tools.append((sanitized_name, tool))
|
|
122
|
+
|
|
123
|
+
# Sort for deterministic output
|
|
124
|
+
sanitized_tools.sort(key=lambda x: x[0])
|
|
125
|
+
result.tool_names = [name for name, _ in sanitized_tools]
|
|
126
|
+
|
|
127
|
+
validated_tool_configs = _validate_tool_configs(tool_configs, config.tools)
|
|
128
|
+
|
|
129
|
+
# Generate tools/custom/__init__.py
|
|
130
|
+
result.payload.files["tools/custom/__init__.py"] = _generate_custom_init(sanitized_tools)
|
|
131
|
+
|
|
132
|
+
# Generate tools/custom/<tool_name>.py for each tool
|
|
133
|
+
for sanitized_name, tool in sanitized_tools:
|
|
134
|
+
wrapper_content = _generate_tool_wrapper(sanitized_name, tool)
|
|
135
|
+
result.payload.files[f"tools/custom/{sanitized_name}.py"] = wrapper_content
|
|
136
|
+
|
|
137
|
+
# Generate tools/custom_sources/__init__.py
|
|
138
|
+
file_tools = [(name, tool) for name, tool in sanitized_tools if tool.get("kind") == "file"]
|
|
139
|
+
result.payload.files["tools/custom_sources/__init__.py"] = _generate_custom_sources_init(file_tools)
|
|
140
|
+
|
|
141
|
+
# Generate tools/custom_sources/<tool_name>/__init__.py for file tools
|
|
142
|
+
for sanitized_name, tool in file_tools:
|
|
143
|
+
source_content = _read_file_tool_source(tool)
|
|
144
|
+
result.payload.files[f"tools/custom_sources/{sanitized_name}/__init__.py"] = source_content
|
|
145
|
+
|
|
146
|
+
# Generate tools/custom_registry.py
|
|
147
|
+
result.payload.files["tools/custom_registry.py"] = _generate_registry(sanitized_tools)
|
|
148
|
+
|
|
149
|
+
# Generate tools/custom_invoke.py
|
|
150
|
+
result.payload.files["tools/custom_invoke.py"] = _generate_invoke_helper()
|
|
151
|
+
|
|
152
|
+
# Generate tools/custom_defaults.json (per-run file, always re-uploaded)
|
|
153
|
+
defaults = validated_tool_configs
|
|
154
|
+
result.payload.per_run_files["tools/custom_defaults.json"] = json.dumps(defaults, indent=2)
|
|
155
|
+
|
|
156
|
+
# Bundle package sources from package_path
|
|
157
|
+
for sanitized_name, tool in sanitized_tools:
|
|
158
|
+
if tool.get("kind") == "package":
|
|
159
|
+
package_path = tool.get("package_path")
|
|
160
|
+
if package_path:
|
|
161
|
+
_bundle_package_source(result.payload, package_path, config.bundle_roots)
|
|
162
|
+
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _generate_custom_init(sanitized_tools: list[tuple[str, PTCToolDef]]) -> str:
|
|
167
|
+
"""Generate tools/custom/__init__.py content.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
sanitized_tools: List of (sanitized_name, tool_def) tuples.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Python source code for the __init__.py module.
|
|
174
|
+
"""
|
|
175
|
+
# Sort for deterministic output
|
|
176
|
+
sorted_names = sorted(name for name, _ in sanitized_tools)
|
|
177
|
+
|
|
178
|
+
imports = "\n".join(f"from tools.custom.{name} import {name}" for name in sorted_names)
|
|
179
|
+
all_list = ", ".join(f'"{name}"' for name in sorted_names)
|
|
180
|
+
|
|
181
|
+
return render_template(
|
|
182
|
+
_TEMPLATE_PACKAGE,
|
|
183
|
+
"custom_init.py.template",
|
|
184
|
+
{
|
|
185
|
+
"imports": imports,
|
|
186
|
+
"all_list": all_list,
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _generate_tool_wrapper(sanitized_name: str, tool: PTCToolDef) -> str:
|
|
192
|
+
"""Generate tools/custom/<tool_name>.py wrapper module.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
sanitized_name: Sanitized tool name for function/module.
|
|
196
|
+
tool: Tool definition.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Python source code for the wrapper module.
|
|
200
|
+
"""
|
|
201
|
+
original_name = tool.get("name", sanitized_name)
|
|
202
|
+
|
|
203
|
+
return render_template(
|
|
204
|
+
_TEMPLATE_PACKAGE,
|
|
205
|
+
"custom_wrapper.py.template",
|
|
206
|
+
{
|
|
207
|
+
"original_name": original_name,
|
|
208
|
+
"sanitized_name": sanitized_name,
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _generate_custom_sources_init(file_tools: list[tuple[str, PTCToolDef]]) -> str:
|
|
214
|
+
"""Generate tools/custom_sources/__init__.py content.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
file_tools: List of (sanitized_name, tool_def) tuples for file tools.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Python source code for the __init__.py module.
|
|
221
|
+
"""
|
|
222
|
+
sorted_names = sorted(name for name, _ in file_tools)
|
|
223
|
+
all_list = ", ".join(f'"{name}"' for name in sorted_names)
|
|
224
|
+
|
|
225
|
+
return render_template(
|
|
226
|
+
_TEMPLATE_PACKAGE,
|
|
227
|
+
"custom_sources_init.py.template",
|
|
228
|
+
{"all_list": all_list},
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _read_file_tool_source(tool: PTCToolDef) -> str:
|
|
233
|
+
"""Read source code from a file tool.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
tool: File tool definition with file_path.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Source code content.
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
FileNotFoundError: If file does not exist.
|
|
243
|
+
"""
|
|
244
|
+
file_path = tool.get("file_path", "")
|
|
245
|
+
path = Path(file_path)
|
|
246
|
+
return path.read_text(encoding="utf-8")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _generate_registry(sanitized_tools: list[tuple[str, PTCToolDef]]) -> str:
|
|
250
|
+
"""Generate tools/custom_registry.py content.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
sanitized_tools: List of (sanitized_name, tool_def) tuples.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Python source code for the registry module.
|
|
257
|
+
"""
|
|
258
|
+
# Generate import statements
|
|
259
|
+
imports: list[str] = []
|
|
260
|
+
build_functions: list[str] = []
|
|
261
|
+
|
|
262
|
+
for sanitized_name, tool in sorted(sanitized_tools, key=lambda x: x[0]):
|
|
263
|
+
kind = tool.get("kind")
|
|
264
|
+
class_name = tool.get("class_name", "")
|
|
265
|
+
|
|
266
|
+
if kind == "package":
|
|
267
|
+
import_path = tool.get("import_path", "")
|
|
268
|
+
imports.append(f"from {import_path} import {class_name}")
|
|
269
|
+
elif kind == "file":
|
|
270
|
+
imports.append(f"from tools.custom_sources.{sanitized_name} import {class_name}")
|
|
271
|
+
|
|
272
|
+
# Generate build function
|
|
273
|
+
build_func = _generate_build_function(sanitized_name, class_name)
|
|
274
|
+
build_functions.append(build_func)
|
|
275
|
+
|
|
276
|
+
imports_str = "\n".join(imports)
|
|
277
|
+
build_functions_str = "\n\n".join(build_functions)
|
|
278
|
+
|
|
279
|
+
return render_template(
|
|
280
|
+
_TEMPLATE_PACKAGE,
|
|
281
|
+
"custom_registry.py.template",
|
|
282
|
+
{
|
|
283
|
+
"imports": imports_str,
|
|
284
|
+
"build_functions": build_functions_str,
|
|
285
|
+
},
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _generate_build_function(sanitized_name: str, class_name: str) -> str:
|
|
290
|
+
"""Generate a build function for a single tool.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
sanitized_name: Sanitized tool name.
|
|
294
|
+
class_name: Tool class name.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Python source code for the build function.
|
|
298
|
+
"""
|
|
299
|
+
return render_template(
|
|
300
|
+
_TEMPLATE_PACKAGE,
|
|
301
|
+
"custom_build_function.py.template",
|
|
302
|
+
{
|
|
303
|
+
"sanitized_name": sanitized_name,
|
|
304
|
+
"class_name": class_name,
|
|
305
|
+
},
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _generate_invoke_helper() -> str:
|
|
310
|
+
"""Generate tools/custom_invoke.py content.
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Python source code for the invoke helper module.
|
|
314
|
+
"""
|
|
315
|
+
return render_template(_TEMPLATE_PACKAGE, "custom_invoke.py.template")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _bundle_package_source(
|
|
319
|
+
payload: SandboxPayload,
|
|
320
|
+
package_path: str,
|
|
321
|
+
bundle_roots: list[str],
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Bundle package source files into payload.
|
|
324
|
+
|
|
325
|
+
Copies Python files from package_path into the payload, preserving
|
|
326
|
+
the directory structure relative to the source root.
|
|
327
|
+
|
|
328
|
+
For standard layouts, files are relative to the bundle root.
|
|
329
|
+
For src/ layouts, files are relative to the src/ directory so that
|
|
330
|
+
imports work correctly (e.g., `from my_pkg import ...`).
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
payload: Sandbox payload to add files to.
|
|
334
|
+
package_path: Path to the package directory.
|
|
335
|
+
bundle_roots: List of allowed bundle roots.
|
|
336
|
+
"""
|
|
337
|
+
pkg_path = Path(package_path).resolve()
|
|
338
|
+
|
|
339
|
+
# Find which bundle root contains this path
|
|
340
|
+
bundle_root = None
|
|
341
|
+
for root in bundle_roots:
|
|
342
|
+
root_path = Path(root).resolve()
|
|
343
|
+
try:
|
|
344
|
+
pkg_path.relative_to(root_path)
|
|
345
|
+
bundle_root = root_path
|
|
346
|
+
break
|
|
347
|
+
except ValueError:
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
if bundle_root is None:
|
|
351
|
+
# Path validation should have caught this earlier
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
# Check for src/ subdirectory (common package layout)
|
|
355
|
+
# When src/ exists, use it as the base for relative paths so imports work
|
|
356
|
+
src_path = pkg_path / "src"
|
|
357
|
+
if src_path.is_dir():
|
|
358
|
+
# For src/ layout: copy from src/ and calculate paths relative to src/
|
|
359
|
+
# This ensures `from my_pkg import ...` works when packages/ is on sys.path
|
|
360
|
+
_copy_python_files(payload, src_path, src_path)
|
|
361
|
+
else:
|
|
362
|
+
# For flat layout: copy from pkg_path relative to bundle_root
|
|
363
|
+
_copy_python_files(payload, pkg_path, bundle_root)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _copy_python_files(
|
|
367
|
+
payload: SandboxPayload,
|
|
368
|
+
source_dir: Path,
|
|
369
|
+
bundle_root: Path,
|
|
370
|
+
) -> None:
|
|
371
|
+
"""Copy Python files from source directory to payload.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
payload: Sandbox payload to add files to.
|
|
375
|
+
source_dir: Source directory to copy from.
|
|
376
|
+
bundle_root: Bundle root for relative path calculation.
|
|
377
|
+
"""
|
|
378
|
+
for py_file in source_dir.rglob("*.py"):
|
|
379
|
+
# Skip __pycache__ and similar
|
|
380
|
+
if "__pycache__" in py_file.parts:
|
|
381
|
+
continue
|
|
382
|
+
if ".egg-info" in str(py_file):
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
# Calculate relative path from bundle root
|
|
386
|
+
try:
|
|
387
|
+
rel_path = py_file.relative_to(bundle_root)
|
|
388
|
+
except ValueError:
|
|
389
|
+
# File is not under bundle root
|
|
390
|
+
continue
|
|
391
|
+
|
|
392
|
+
# Read and add to payload
|
|
393
|
+
try:
|
|
394
|
+
content = py_file.read_text(encoding="utf-8")
|
|
395
|
+
# Use packages/ prefix for bundled sources
|
|
396
|
+
payload_path = f"packages/{rel_path.as_posix()}"
|
|
397
|
+
payload.files[payload_path] = content
|
|
398
|
+
except Exception as exc:
|
|
399
|
+
logger.warning(f"Skipping unreadable file '{py_file}': {exc}")
|
|
400
|
+
continue
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from _typeshed import Incomplete
|
|
2
|
+
from aip_agents.ptc.custom_tools import PTCCustomToolConfig as PTCCustomToolConfig, PTCToolDef as PTCToolDef
|
|
3
|
+
from aip_agents.ptc.naming import sanitize_function_name as sanitize_function_name
|
|
4
|
+
from aip_agents.ptc.payload import SandboxPayload as SandboxPayload
|
|
5
|
+
from aip_agents.ptc.template_utils import render_template as render_template
|
|
6
|
+
from aip_agents.utils.logger import get_logger as get_logger
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
logger: Incomplete
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CustomToolPayloadResult:
|
|
13
|
+
"""Result of custom tool payload generation.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
payload: The sandbox payload with files to upload.
|
|
17
|
+
tool_names: List of sanitized tool names available in sandbox.
|
|
18
|
+
"""
|
|
19
|
+
payload: SandboxPayload
|
|
20
|
+
tool_names: list[str] = field(default_factory=list)
|
|
21
|
+
|
|
22
|
+
def build_custom_tools_payload(config: PTCCustomToolConfig, tool_configs: dict[str, dict] | None = None) -> CustomToolPayloadResult:
|
|
23
|
+
"""Build sandbox payload for custom LangChain tools.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config: Custom tool configuration with tool definitions.
|
|
27
|
+
tool_configs: Optional per-tool config values to write to defaults.json.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
CustomToolPayloadResult with payload and available tool names.
|
|
31
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Templates for custom LangChain tool payload generation."""
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
def build_$sanitized_name() -> BaseTool:
|
|
2
|
+
"""Build an instance of $class_name.
|
|
3
|
+
|
|
4
|
+
Returns:
|
|
5
|
+
Configured tool instance.
|
|
6
|
+
"""
|
|
7
|
+
# Check if the class is already an instance (module-level singleton)
|
|
8
|
+
tool_or_class = $class_name
|
|
9
|
+
if isinstance(tool_or_class, BaseTool):
|
|
10
|
+
tool = tool_or_class
|
|
11
|
+
else:
|
|
12
|
+
try:
|
|
13
|
+
tool = tool_or_class()
|
|
14
|
+
except TypeError as e:
|
|
15
|
+
raise TypeError(
|
|
16
|
+
f"Failed to instantiate $class_name(). "
|
|
17
|
+
"Constructor args are not supported in MVP. "
|
|
18
|
+
"Use tool_config_schema for runtime configuration. "
|
|
19
|
+
f"Original error: {e}"
|
|
20
|
+
) from e
|
|
21
|
+
|
|
22
|
+
_configure_tool(tool, "$sanitized_name")
|
|
23
|
+
return tool
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Generated custom tools package for PTC sandbox execution.
|
|
2
|
+
|
|
3
|
+
This package provides access to custom LangChain tools configured for this agent.
|
|
4
|
+
Import tools directly:
|
|
5
|
+
|
|
6
|
+
from tools.custom import tool_name
|
|
7
|
+
|
|
8
|
+
Or from specific modules:
|
|
9
|
+
|
|
10
|
+
from tools.custom.tool_name import tool_name
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
$imports
|
|
14
|
+
|
|
15
|
+
__all__ = [$all_list]
|