aip-agents-binary 0.5.21__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 +169 -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 +424 -35
- aip_agents/agent/langgraph_react_agent.pyi +46 -2
- aip_agents/examples/{hello_world_langgraph_bosa_twitter.py → hello_world_langgraph_gl_connector_twitter.py} +10 -7
- aip_agents/examples/hello_world_langgraph_gl_connector_twitter.pyi +5 -0
- 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_sentry.py +2 -2
- 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/__init__.py +83 -0
- aip_agents/guardrails/__init__.pyi +6 -0
- aip_agents/guardrails/engines/__init__.py +69 -0
- aip_agents/guardrails/engines/__init__.pyi +4 -0
- aip_agents/guardrails/engines/base.py +90 -0
- aip_agents/guardrails/engines/base.pyi +61 -0
- aip_agents/guardrails/engines/nemo.py +101 -0
- aip_agents/guardrails/engines/nemo.pyi +46 -0
- aip_agents/guardrails/engines/phrase_matcher.py +113 -0
- aip_agents/guardrails/engines/phrase_matcher.pyi +48 -0
- aip_agents/guardrails/exceptions.py +39 -0
- aip_agents/guardrails/exceptions.pyi +23 -0
- aip_agents/guardrails/manager.py +163 -0
- aip_agents/guardrails/manager.pyi +42 -0
- aip_agents/guardrails/middleware.py +199 -0
- aip_agents/guardrails/middleware.pyi +87 -0
- aip_agents/guardrails/schemas.py +63 -0
- aip_agents/guardrails/schemas.pyi +43 -0
- aip_agents/guardrails/utils.py +45 -0
- aip_agents/guardrails/utils.pyi +19 -0
- 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 -65
- aip_agents/mcp/client/persistent_session.pyi +9 -0
- aip_agents/mcp/client/transports.py +52 -4
- 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/middleware/base.py +8 -0
- aip_agents/middleware/base.pyi +4 -0
- aip_agents/middleware/manager.py +22 -0
- aip_agents/middleware/manager.pyi +4 -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/__init__.py +1 -1
- aip_agents/sentry/sentry.py +33 -12
- aip_agents/sentry/sentry.pyi +5 -4
- aip_agents/tools/__init__.py +20 -3
- aip_agents/tools/__init__.pyi +4 -2
- aip_agents/tools/browser_use/browser_use_tool.py +8 -0
- aip_agents/tools/browser_use/streaming.py +2 -0
- aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +80 -31
- aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.pyi +25 -9
- aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +6 -6
- aip_agents/tools/constants.py +24 -12
- aip_agents/tools/constants.pyi +14 -11
- 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/gl_connector/__init__.py +1 -1
- aip_agents/tools/gl_connector/tool.py +62 -30
- aip_agents/tools/gl_connector/tool.pyi +3 -3
- aip_agents/tools/gl_connector_tools.py +119 -0
- aip_agents/tools/gl_connector_tools.pyi +39 -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.21.dist-info → aip_agents_binary-0.6.8.dist-info}/METADATA +14 -22
- {aip_agents_binary-0.5.21.dist-info → aip_agents_binary-0.6.8.dist-info}/RECORD +144 -58
- {aip_agents_binary-0.5.21.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/examples/hello_world_langgraph_bosa_twitter.pyi +0 -5
- aip_agents/tools/bosa_tools.py +0 -105
- aip_agents/tools/bosa_tools.pyi +0 -37
- {aip_agents_binary-0.5.21.dist-info → aip_agents_binary-0.6.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""Custom LangChain tools configuration for PTC sandbox.
|
|
2
|
+
|
|
3
|
+
This module provides configuration classes for defining custom LangChain tools
|
|
4
|
+
that can be used inside the PTC sandbox alongside MCP tools.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Literal, TypedDict
|
|
16
|
+
|
|
17
|
+
from aip_agents.ptc.exceptions import PTCError
|
|
18
|
+
from aip_agents.ptc.naming import is_valid_identifier, sanitize_function_name
|
|
19
|
+
from aip_agents.utils.logger import get_logger
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PTCPackageToolDef(TypedDict, total=False):
|
|
25
|
+
"""Definition for a package-based tool.
|
|
26
|
+
|
|
27
|
+
Required fields:
|
|
28
|
+
name: Tool name (used for imports and config lookup).
|
|
29
|
+
kind: Must be "package".
|
|
30
|
+
import_path: Dotted import path (e.g., "aip_agents.tools.time_tool").
|
|
31
|
+
class_name: Class name to import (e.g., "TimeTool").
|
|
32
|
+
|
|
33
|
+
Optional fields:
|
|
34
|
+
package_path: Path to source directory to bundle.
|
|
35
|
+
If omitted, the tool must already be available in sandbox (e.g., from site-packages).
|
|
36
|
+
description: Tool description (derived from tool object at construction time).
|
|
37
|
+
input_schema: JSON schema for tool input (derived from tool object at construction time).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
name: str
|
|
41
|
+
kind: Literal["package"]
|
|
42
|
+
import_path: str
|
|
43
|
+
class_name: str
|
|
44
|
+
package_path: str # Optional
|
|
45
|
+
description: str # Optional - derived from tool object
|
|
46
|
+
input_schema: dict # Optional - derived from tool object
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PTCFileToolDef(TypedDict, total=False):
|
|
50
|
+
"""Definition for a single-file tool.
|
|
51
|
+
|
|
52
|
+
Required fields:
|
|
53
|
+
name: Tool name (used for imports and config lookup).
|
|
54
|
+
kind: Must be "file".
|
|
55
|
+
file_path: Path to the Python file containing the tool.
|
|
56
|
+
class_name: Class name to import from the file.
|
|
57
|
+
|
|
58
|
+
Optional fields:
|
|
59
|
+
description: Tool description (derived from tool object at construction time).
|
|
60
|
+
input_schema: JSON schema for tool input (derived from tool object at construction time).
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
name: str
|
|
64
|
+
kind: Literal["file"]
|
|
65
|
+
file_path: str
|
|
66
|
+
class_name: str
|
|
67
|
+
description: str # Optional - derived from tool object
|
|
68
|
+
input_schema: dict # Optional - derived from tool object
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Union type for tool definitions
|
|
72
|
+
PTCToolDef = PTCPackageToolDef | PTCFileToolDef
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PTCCustomToolValidationError(PTCError):
|
|
76
|
+
"""Error raised when custom tool configuration is invalid."""
|
|
77
|
+
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class PTCCustomToolConfig:
|
|
83
|
+
"""Configuration for custom LangChain tools in PTC sandbox.
|
|
84
|
+
|
|
85
|
+
This config controls which user-defined LangChain tools are available
|
|
86
|
+
in the sandbox for use by PTC code.
|
|
87
|
+
|
|
88
|
+
Attributes:
|
|
89
|
+
enabled: Whether custom tools are enabled.
|
|
90
|
+
bundle_roots: List of allowed root directories for bundling tool sources.
|
|
91
|
+
Paths outside these roots will cause validation errors.
|
|
92
|
+
requirements: List of pip requirements to install in sandbox.
|
|
93
|
+
tools: List of tool definitions (package or file tools).
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
>>> config = PTCCustomToolConfig(
|
|
97
|
+
... enabled=True,
|
|
98
|
+
... bundle_roots=["/app/tools"],
|
|
99
|
+
... requirements=["pydantic>=2.0"],
|
|
100
|
+
... tools=[
|
|
101
|
+
... {
|
|
102
|
+
... "name": "time_tool",
|
|
103
|
+
... "kind": "package",
|
|
104
|
+
... "import_path": "aip_agents.tools.time_tool",
|
|
105
|
+
... "class_name": "TimeTool",
|
|
106
|
+
... },
|
|
107
|
+
... ],
|
|
108
|
+
... )
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
enabled: bool = False
|
|
112
|
+
bundle_roots: list[str] = field(default_factory=list)
|
|
113
|
+
requirements: list[str] = field(default_factory=list)
|
|
114
|
+
tools: list[PTCToolDef] = field(default_factory=list)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _validate_class_name(class_name: str, tool_name: str) -> None:
|
|
118
|
+
"""Validate a class name used in generated code.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
class_name: Class name to validate.
|
|
122
|
+
tool_name: Tool name for error messages.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
PTCCustomToolValidationError: If class name is invalid.
|
|
126
|
+
"""
|
|
127
|
+
if not is_valid_identifier(class_name):
|
|
128
|
+
raise PTCCustomToolValidationError(
|
|
129
|
+
f"Tool '{tool_name}' has invalid class_name '{class_name}'. Must be a valid Python identifier."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _validate_package_tool(tool: PTCToolDef, name: str) -> None:
|
|
134
|
+
"""Validate a package tool definition.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
tool: Package tool definition to validate.
|
|
138
|
+
name: Tool name for error messages.
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
PTCCustomToolValidationError: If package tool definition is invalid.
|
|
142
|
+
"""
|
|
143
|
+
import_path = tool.get("import_path", "")
|
|
144
|
+
if not import_path:
|
|
145
|
+
raise PTCCustomToolValidationError(f"Package tool '{name}' missing required 'import_path' field")
|
|
146
|
+
if import_path.startswith("."):
|
|
147
|
+
raise PTCCustomToolValidationError(f"Package tool '{name}' import_path must be absolute: '{import_path}'")
|
|
148
|
+
if any(not part.isidentifier() for part in import_path.split(".")):
|
|
149
|
+
raise PTCCustomToolValidationError(f"Package tool '{name}' has invalid import_path: '{import_path}'")
|
|
150
|
+
class_name = tool.get("class_name", "")
|
|
151
|
+
if not class_name:
|
|
152
|
+
raise PTCCustomToolValidationError(f"Package tool '{name}' missing required 'class_name' field")
|
|
153
|
+
_validate_class_name(class_name, name)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _validate_file_tool(tool: PTCToolDef, name: str) -> None:
|
|
157
|
+
"""Validate a file tool definition.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
tool: File tool definition to validate.
|
|
161
|
+
name: Tool name for error messages.
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
PTCCustomToolValidationError: If file tool definition is invalid.
|
|
165
|
+
"""
|
|
166
|
+
if not tool.get("file_path"):
|
|
167
|
+
raise PTCCustomToolValidationError(f"File tool '{name}' missing required 'file_path' field")
|
|
168
|
+
class_name = tool.get("class_name", "")
|
|
169
|
+
if not class_name:
|
|
170
|
+
raise PTCCustomToolValidationError(f"File tool '{name}' missing required 'class_name' field")
|
|
171
|
+
_validate_class_name(class_name, name)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def validate_tool_def(tool: PTCToolDef) -> None:
|
|
175
|
+
"""Validate a tool definition has required fields.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
tool: Tool definition to validate.
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
PTCCustomToolValidationError: If tool definition is invalid.
|
|
182
|
+
"""
|
|
183
|
+
name = tool.get("name")
|
|
184
|
+
kind = tool.get("kind")
|
|
185
|
+
|
|
186
|
+
if not name:
|
|
187
|
+
raise PTCCustomToolValidationError("Tool definition missing required 'name' field")
|
|
188
|
+
|
|
189
|
+
if kind not in ("package", "file"):
|
|
190
|
+
raise PTCCustomToolValidationError(f"Tool '{name}' has invalid kind '{kind}'. Must be 'package' or 'file'.")
|
|
191
|
+
|
|
192
|
+
if kind == "package":
|
|
193
|
+
_validate_package_tool(tool, name)
|
|
194
|
+
elif kind == "file":
|
|
195
|
+
_validate_file_tool(tool, name)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def validate_path_within_bundle_roots(
|
|
199
|
+
path: str | Path,
|
|
200
|
+
bundle_roots: list[str],
|
|
201
|
+
tool_name: str,
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Validate that a path is within one of the bundle roots.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
path: Path to validate.
|
|
207
|
+
bundle_roots: List of allowed root directories.
|
|
208
|
+
tool_name: Tool name for error messages.
|
|
209
|
+
|
|
210
|
+
Raises:
|
|
211
|
+
PTCCustomToolValidationError: If path is outside all bundle roots.
|
|
212
|
+
"""
|
|
213
|
+
if not bundle_roots:
|
|
214
|
+
raise PTCCustomToolValidationError(
|
|
215
|
+
f"Tool '{tool_name}' requires bundle_roots to be set when using package_path or file_path"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Resolve real path to prevent symlink escapes
|
|
219
|
+
real_path = Path(path).resolve()
|
|
220
|
+
|
|
221
|
+
for root in bundle_roots:
|
|
222
|
+
real_root = Path(root).resolve()
|
|
223
|
+
try:
|
|
224
|
+
real_path.relative_to(real_root)
|
|
225
|
+
return # Path is within this root
|
|
226
|
+
except ValueError:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
raise PTCCustomToolValidationError(
|
|
230
|
+
f"Tool '{tool_name}' path '{path}' is outside all bundle_roots. Allowed roots: {bundle_roots}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def detect_relative_imports(file_path: str | Path) -> list[str]:
|
|
235
|
+
"""Detect relative imports in a Python file using AST.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
file_path: Path to the Python file to scan.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
List of relative import statements found.
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
PTCCustomToolValidationError: If file cannot be read or parsed.
|
|
245
|
+
"""
|
|
246
|
+
path = Path(file_path)
|
|
247
|
+
if not path.exists():
|
|
248
|
+
raise PTCCustomToolValidationError(f"File not found: {file_path}")
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
source = path.read_text(encoding="utf-8")
|
|
252
|
+
except Exception as exc:
|
|
253
|
+
raise PTCCustomToolValidationError(f"Cannot read file '{file_path}': {exc}") from exc
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
tree = ast.parse(source, filename=str(path))
|
|
257
|
+
except SyntaxError as exc:
|
|
258
|
+
raise PTCCustomToolValidationError(f"Syntax error in file '{file_path}': {exc}") from exc
|
|
259
|
+
|
|
260
|
+
relative_imports: list[str] = []
|
|
261
|
+
for node in ast.walk(tree):
|
|
262
|
+
if isinstance(node, ast.ImportFrom):
|
|
263
|
+
# level > 0 indicates relative import (e.g., from . import X, from ..foo import Y)
|
|
264
|
+
if node.level > 0:
|
|
265
|
+
module = node.module or ""
|
|
266
|
+
dots = "." * node.level
|
|
267
|
+
import_str = f"from {dots}{module} import ..."
|
|
268
|
+
relative_imports.append(import_str)
|
|
269
|
+
|
|
270
|
+
return relative_imports
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def check_name_collisions(tools: list[PTCToolDef]) -> None:
|
|
274
|
+
"""Check for tool name collisions after sanitization.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
tools: List of tool definitions.
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
PTCCustomToolValidationError: If two tools sanitize to the same name.
|
|
281
|
+
"""
|
|
282
|
+
sanitized_to_original: dict[str, list[str]] = {}
|
|
283
|
+
|
|
284
|
+
for tool in tools:
|
|
285
|
+
name = tool.get("name", "")
|
|
286
|
+
sanitized = sanitize_function_name(name)
|
|
287
|
+
|
|
288
|
+
if sanitized not in sanitized_to_original:
|
|
289
|
+
sanitized_to_original[sanitized] = []
|
|
290
|
+
sanitized_to_original[sanitized].append(name)
|
|
291
|
+
|
|
292
|
+
# Check for collisions
|
|
293
|
+
for sanitized, originals in sanitized_to_original.items():
|
|
294
|
+
if len(originals) > 1:
|
|
295
|
+
raise PTCCustomToolValidationError(
|
|
296
|
+
f"Tool name collision: tools {originals} all sanitize to '{sanitized}'. Please use unique tool names."
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _validate_tool_paths(tool: dict, bundle_roots: list[str]) -> None:
|
|
301
|
+
"""Validate paths for a single tool and check for relative imports.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
tool: Tool definition dict with kind, name, and path fields.
|
|
305
|
+
bundle_roots: List of allowed bundle root directories.
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
PTCCustomToolValidationError: If paths are invalid or file uses relative imports.
|
|
309
|
+
"""
|
|
310
|
+
kind = tool.get("kind")
|
|
311
|
+
name = tool.get("name", "")
|
|
312
|
+
|
|
313
|
+
if kind == "package":
|
|
314
|
+
package_path = tool.get("package_path")
|
|
315
|
+
if package_path:
|
|
316
|
+
path = Path(package_path)
|
|
317
|
+
if not path.exists() or not path.is_dir():
|
|
318
|
+
raise PTCCustomToolValidationError(
|
|
319
|
+
f"Package tool '{name}' package_path does not exist or is not a directory: '{package_path}'"
|
|
320
|
+
)
|
|
321
|
+
validate_path_within_bundle_roots(package_path, bundle_roots, name)
|
|
322
|
+
|
|
323
|
+
elif kind == "file":
|
|
324
|
+
file_path = tool.get("file_path")
|
|
325
|
+
if file_path:
|
|
326
|
+
validate_path_within_bundle_roots(file_path, bundle_roots, name)
|
|
327
|
+
|
|
328
|
+
relative_imports = detect_relative_imports(file_path)
|
|
329
|
+
if relative_imports:
|
|
330
|
+
raise PTCCustomToolValidationError(
|
|
331
|
+
f"File tool '{name}' uses relative imports which are not supported: "
|
|
332
|
+
f"{relative_imports}. Use a package tool with package_path instead."
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def validate_custom_tool_config(config: PTCCustomToolConfig) -> None:
|
|
337
|
+
"""Validate the complete custom tool configuration.
|
|
338
|
+
|
|
339
|
+
This runs all validation checks:
|
|
340
|
+
- Tool definitions have required fields
|
|
341
|
+
- Paths are within bundle roots
|
|
342
|
+
- File tools don't use relative imports
|
|
343
|
+
- No name collisions
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
config: Custom tool configuration to validate.
|
|
347
|
+
|
|
348
|
+
Raises:
|
|
349
|
+
PTCCustomToolValidationError: If configuration is invalid.
|
|
350
|
+
"""
|
|
351
|
+
if not config.enabled or not config.tools:
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
# Validate each tool definition
|
|
355
|
+
for tool in config.tools:
|
|
356
|
+
validate_tool_def(tool)
|
|
357
|
+
|
|
358
|
+
# Check for name collisions
|
|
359
|
+
check_name_collisions(config.tools)
|
|
360
|
+
|
|
361
|
+
# Validate paths and check for relative imports
|
|
362
|
+
for tool in config.tools:
|
|
363
|
+
_validate_tool_paths(tool, config.bundle_roots)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def extract_tool_metadata(tool: object) -> dict:
|
|
367
|
+
"""Extract metadata from a LangChain BaseTool instance.
|
|
368
|
+
|
|
369
|
+
Extracts name, description, and input_schema from the tool object.
|
|
370
|
+
This is called at agent construction time to populate tool definitions.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
tool: A LangChain BaseTool instance (or compatible object).
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Dict with keys:
|
|
377
|
+
- name: str (tool name)
|
|
378
|
+
- description: str (tool description, empty if not available)
|
|
379
|
+
- input_schema: dict (JSON schema, empty object schema if not available)
|
|
380
|
+
|
|
381
|
+
Note:
|
|
382
|
+
If schema inference fails, returns an empty object schema
|
|
383
|
+
({"type": "object", "properties": {}}) so prompt/index output
|
|
384
|
+
falls back to tool(**kwargs).
|
|
385
|
+
"""
|
|
386
|
+
result: dict = {
|
|
387
|
+
"name": "",
|
|
388
|
+
"description": "",
|
|
389
|
+
"input_schema": {"type": "object", "properties": {}},
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
# Extract name
|
|
393
|
+
if hasattr(tool, "name"):
|
|
394
|
+
result["name"] = str(tool.name)
|
|
395
|
+
|
|
396
|
+
# Extract description
|
|
397
|
+
if hasattr(tool, "description"):
|
|
398
|
+
desc = tool.description
|
|
399
|
+
if desc:
|
|
400
|
+
# Clean up multiline description - normalize whitespace
|
|
401
|
+
result["description"] = " ".join(str(desc).split())
|
|
402
|
+
|
|
403
|
+
# Extract input_schema from args_schema (Pydantic model)
|
|
404
|
+
if hasattr(tool, "args_schema") and tool.args_schema is not None:
|
|
405
|
+
try:
|
|
406
|
+
args_schema = tool.args_schema
|
|
407
|
+
# Pydantic v2 uses model_json_schema()
|
|
408
|
+
if hasattr(args_schema, "model_json_schema"):
|
|
409
|
+
schema = args_schema.model_json_schema()
|
|
410
|
+
# Remove Pydantic-specific keys that aren't needed
|
|
411
|
+
schema.pop("title", None)
|
|
412
|
+
schema.pop("$defs", None)
|
|
413
|
+
schema.pop("definitions", None)
|
|
414
|
+
result["input_schema"] = schema
|
|
415
|
+
# Pydantic v1 fallback uses schema()
|
|
416
|
+
elif hasattr(args_schema, "schema"):
|
|
417
|
+
schema = args_schema.schema()
|
|
418
|
+
schema.pop("title", None)
|
|
419
|
+
schema.pop("definitions", None)
|
|
420
|
+
result["input_schema"] = schema
|
|
421
|
+
except Exception:
|
|
422
|
+
# If schema extraction fails, keep the empty object schema
|
|
423
|
+
pass
|
|
424
|
+
|
|
425
|
+
return result
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def enrich_tool_def_with_metadata(tool_def: PTCToolDef, tool: object) -> PTCToolDef:
|
|
429
|
+
"""Enrich a tool definition with metadata extracted from the tool object.
|
|
430
|
+
|
|
431
|
+
Always derives description and input_schema from the tool object, overwriting
|
|
432
|
+
any user-supplied values. This ensures the tool object is the source of truth.
|
|
433
|
+
Called at agent construction time.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
tool_def: The tool definition to enrich.
|
|
437
|
+
tool: The LangChain BaseTool instance.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
The tool definition with description and input_schema populated from the tool object.
|
|
441
|
+
|
|
442
|
+
Raises:
|
|
443
|
+
PTCCustomToolValidationError: If tool name in metadata does not match tool_def name.
|
|
444
|
+
"""
|
|
445
|
+
metadata = extract_tool_metadata(tool)
|
|
446
|
+
|
|
447
|
+
# Create a mutable copy
|
|
448
|
+
enriched: dict = dict(tool_def)
|
|
449
|
+
|
|
450
|
+
# Validate name matches if metadata provides one
|
|
451
|
+
if metadata["name"] and metadata["name"] != enriched.get("name"):
|
|
452
|
+
raise PTCCustomToolValidationError(
|
|
453
|
+
f"Tool name mismatch: tool_def name '{enriched.get('name')}' does not match "
|
|
454
|
+
f"tool object name '{metadata['name']}'"
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
# Log if we're overwriting existing values
|
|
458
|
+
if "description" in enriched and enriched["description"]:
|
|
459
|
+
logger.debug(
|
|
460
|
+
f"Overwriting user-supplied description for tool '{enriched.get('name', 'unknown')}' "
|
|
461
|
+
f"with derived metadata from tool object"
|
|
462
|
+
)
|
|
463
|
+
if "input_schema" in enriched and enriched["input_schema"]:
|
|
464
|
+
logger.debug(
|
|
465
|
+
f"Overwriting user-supplied input_schema for tool '{enriched.get('name', 'unknown')}' "
|
|
466
|
+
f"with derived metadata from tool object"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Always overwrite with derived metadata (tool object is source of truth)
|
|
470
|
+
enriched["description"] = metadata["description"]
|
|
471
|
+
enriched["input_schema"] = metadata["input_schema"]
|
|
472
|
+
|
|
473
|
+
return enriched # type: ignore[return-value]
|
|
@@ -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
|
+
"""
|