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.
Files changed (149) hide show
  1. aip_agents/agent/__init__.py +44 -4
  2. aip_agents/agent/base_langgraph_agent.py +169 -74
  3. aip_agents/agent/base_langgraph_agent.pyi +3 -2
  4. aip_agents/agent/langgraph_memory_enhancer_agent.py +368 -34
  5. aip_agents/agent/langgraph_memory_enhancer_agent.pyi +3 -2
  6. aip_agents/agent/langgraph_react_agent.py +424 -35
  7. aip_agents/agent/langgraph_react_agent.pyi +46 -2
  8. aip_agents/examples/{hello_world_langgraph_bosa_twitter.py → hello_world_langgraph_gl_connector_twitter.py} +10 -7
  9. aip_agents/examples/hello_world_langgraph_gl_connector_twitter.pyi +5 -0
  10. aip_agents/examples/hello_world_ptc.py +49 -0
  11. aip_agents/examples/hello_world_ptc.pyi +5 -0
  12. aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
  13. aip_agents/examples/hello_world_ptc_custom_tools.pyi +7 -0
  14. aip_agents/examples/hello_world_sentry.py +2 -2
  15. aip_agents/examples/hello_world_tool_output_client.py +9 -0
  16. aip_agents/examples/tools/multiply_tool.py +43 -0
  17. aip_agents/examples/tools/multiply_tool.pyi +18 -0
  18. aip_agents/guardrails/__init__.py +83 -0
  19. aip_agents/guardrails/__init__.pyi +6 -0
  20. aip_agents/guardrails/engines/__init__.py +69 -0
  21. aip_agents/guardrails/engines/__init__.pyi +4 -0
  22. aip_agents/guardrails/engines/base.py +90 -0
  23. aip_agents/guardrails/engines/base.pyi +61 -0
  24. aip_agents/guardrails/engines/nemo.py +101 -0
  25. aip_agents/guardrails/engines/nemo.pyi +46 -0
  26. aip_agents/guardrails/engines/phrase_matcher.py +113 -0
  27. aip_agents/guardrails/engines/phrase_matcher.pyi +48 -0
  28. aip_agents/guardrails/exceptions.py +39 -0
  29. aip_agents/guardrails/exceptions.pyi +23 -0
  30. aip_agents/guardrails/manager.py +163 -0
  31. aip_agents/guardrails/manager.pyi +42 -0
  32. aip_agents/guardrails/middleware.py +199 -0
  33. aip_agents/guardrails/middleware.pyi +87 -0
  34. aip_agents/guardrails/schemas.py +63 -0
  35. aip_agents/guardrails/schemas.pyi +43 -0
  36. aip_agents/guardrails/utils.py +45 -0
  37. aip_agents/guardrails/utils.pyi +19 -0
  38. aip_agents/mcp/client/__init__.py +38 -2
  39. aip_agents/mcp/client/connection_manager.py +36 -1
  40. aip_agents/mcp/client/connection_manager.pyi +3 -0
  41. aip_agents/mcp/client/persistent_session.py +318 -65
  42. aip_agents/mcp/client/persistent_session.pyi +9 -0
  43. aip_agents/mcp/client/transports.py +52 -4
  44. aip_agents/mcp/client/transports.pyi +9 -0
  45. aip_agents/memory/adapters/base_adapter.py +98 -0
  46. aip_agents/memory/adapters/base_adapter.pyi +25 -0
  47. aip_agents/middleware/base.py +8 -0
  48. aip_agents/middleware/base.pyi +4 -0
  49. aip_agents/middleware/manager.py +22 -0
  50. aip_agents/middleware/manager.pyi +4 -0
  51. aip_agents/ptc/__init__.py +87 -0
  52. aip_agents/ptc/__init__.pyi +14 -0
  53. aip_agents/ptc/custom_tools.py +473 -0
  54. aip_agents/ptc/custom_tools.pyi +184 -0
  55. aip_agents/ptc/custom_tools_payload.py +400 -0
  56. aip_agents/ptc/custom_tools_payload.pyi +31 -0
  57. aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
  58. aip_agents/ptc/custom_tools_templates/__init__.pyi +0 -0
  59. aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +23 -0
  60. aip_agents/ptc/custom_tools_templates/custom_init.py.template +15 -0
  61. aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +60 -0
  62. aip_agents/ptc/custom_tools_templates/custom_registry.py.template +87 -0
  63. aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
  64. aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +19 -0
  65. aip_agents/ptc/doc_gen.py +122 -0
  66. aip_agents/ptc/doc_gen.pyi +40 -0
  67. aip_agents/ptc/exceptions.py +57 -0
  68. aip_agents/ptc/exceptions.pyi +37 -0
  69. aip_agents/ptc/executor.py +261 -0
  70. aip_agents/ptc/executor.pyi +99 -0
  71. aip_agents/ptc/mcp/__init__.py +45 -0
  72. aip_agents/ptc/mcp/__init__.pyi +7 -0
  73. aip_agents/ptc/mcp/sandbox_bridge.py +668 -0
  74. aip_agents/ptc/mcp/sandbox_bridge.pyi +47 -0
  75. aip_agents/ptc/mcp/templates/__init__.py +1 -0
  76. aip_agents/ptc/mcp/templates/__init__.pyi +0 -0
  77. aip_agents/ptc/mcp/templates/mcp_client.py.template +239 -0
  78. aip_agents/ptc/naming.py +196 -0
  79. aip_agents/ptc/naming.pyi +85 -0
  80. aip_agents/ptc/payload.py +26 -0
  81. aip_agents/ptc/payload.pyi +15 -0
  82. aip_agents/ptc/prompt_builder.py +673 -0
  83. aip_agents/ptc/prompt_builder.pyi +59 -0
  84. aip_agents/ptc/ptc_helper.py +16 -0
  85. aip_agents/ptc/ptc_helper.pyi +1 -0
  86. aip_agents/ptc/sandbox_bridge.py +256 -0
  87. aip_agents/ptc/sandbox_bridge.pyi +38 -0
  88. aip_agents/ptc/template_utils.py +33 -0
  89. aip_agents/ptc/template_utils.pyi +13 -0
  90. aip_agents/ptc/templates/__init__.py +1 -0
  91. aip_agents/ptc/templates/__init__.pyi +0 -0
  92. aip_agents/ptc/templates/ptc_helper.py.template +134 -0
  93. aip_agents/ptc/tool_def_helpers.py +101 -0
  94. aip_agents/ptc/tool_def_helpers.pyi +38 -0
  95. aip_agents/ptc/tool_enrichment.py +163 -0
  96. aip_agents/ptc/tool_enrichment.pyi +60 -0
  97. aip_agents/sandbox/__init__.py +43 -0
  98. aip_agents/sandbox/__init__.pyi +5 -0
  99. aip_agents/sandbox/defaults.py +205 -0
  100. aip_agents/sandbox/defaults.pyi +30 -0
  101. aip_agents/sandbox/e2b_runtime.py +295 -0
  102. aip_agents/sandbox/e2b_runtime.pyi +57 -0
  103. aip_agents/sandbox/template_builder.py +131 -0
  104. aip_agents/sandbox/template_builder.pyi +36 -0
  105. aip_agents/sandbox/types.py +24 -0
  106. aip_agents/sandbox/types.pyi +14 -0
  107. aip_agents/sandbox/validation.py +50 -0
  108. aip_agents/sandbox/validation.pyi +20 -0
  109. aip_agents/sentry/__init__.py +1 -1
  110. aip_agents/sentry/sentry.py +33 -12
  111. aip_agents/sentry/sentry.pyi +5 -4
  112. aip_agents/tools/__init__.py +20 -3
  113. aip_agents/tools/__init__.pyi +4 -2
  114. aip_agents/tools/browser_use/browser_use_tool.py +8 -0
  115. aip_agents/tools/browser_use/streaming.py +2 -0
  116. aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +80 -31
  117. aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.pyi +25 -9
  118. aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +6 -6
  119. aip_agents/tools/constants.py +24 -12
  120. aip_agents/tools/constants.pyi +14 -11
  121. aip_agents/tools/date_range_tool.py +554 -0
  122. aip_agents/tools/date_range_tool.pyi +21 -0
  123. aip_agents/tools/execute_ptc_code.py +357 -0
  124. aip_agents/tools/execute_ptc_code.pyi +90 -0
  125. aip_agents/tools/gl_connector/__init__.py +1 -1
  126. aip_agents/tools/gl_connector/tool.py +62 -30
  127. aip_agents/tools/gl_connector/tool.pyi +3 -3
  128. aip_agents/tools/gl_connector_tools.py +119 -0
  129. aip_agents/tools/gl_connector_tools.pyi +39 -0
  130. aip_agents/tools/memory_search/__init__.py +8 -1
  131. aip_agents/tools/memory_search/__init__.pyi +3 -3
  132. aip_agents/tools/memory_search/mem0.py +114 -1
  133. aip_agents/tools/memory_search/mem0.pyi +11 -1
  134. aip_agents/tools/memory_search/schema.py +33 -0
  135. aip_agents/tools/memory_search/schema.pyi +10 -0
  136. aip_agents/tools/memory_search_tool.py +8 -0
  137. aip_agents/tools/memory_search_tool.pyi +2 -2
  138. aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +26 -1
  139. aip_agents/utils/langgraph/tool_output_management.py +80 -0
  140. aip_agents/utils/langgraph/tool_output_management.pyi +37 -0
  141. {aip_agents_binary-0.5.21.dist-info → aip_agents_binary-0.6.8.dist-info}/METADATA +14 -22
  142. {aip_agents_binary-0.5.21.dist-info → aip_agents_binary-0.6.8.dist-info}/RECORD +144 -58
  143. {aip_agents_binary-0.5.21.dist-info → aip_agents_binary-0.6.8.dist-info}/WHEEL +1 -1
  144. aip_agents/examples/demo_memory_recall.py +0 -401
  145. aip_agents/examples/demo_memory_recall.pyi +0 -58
  146. aip_agents/examples/hello_world_langgraph_bosa_twitter.pyi +0 -5
  147. aip_agents/tools/bosa_tools.py +0 -105
  148. aip_agents/tools/bosa_tools.pyi +0 -37
  149. {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
+ """