aip-agents-binary 0.6.5__py3-none-macosx_13_0_arm64.whl → 0.6.7__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 (47) hide show
  1. aip_agents/agent/langgraph_react_agent.py +66 -19
  2. aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
  3. aip_agents/examples/hello_world_ptc_custom_tools.pyi +7 -0
  4. aip_agents/examples/tools/multiply_tool.py +43 -0
  5. aip_agents/examples/tools/multiply_tool.pyi +18 -0
  6. aip_agents/memory/adapters/base_adapter.py +25 -21
  7. aip_agents/memory/adapters/base_adapter.pyi +7 -8
  8. aip_agents/ptc/__init__.py +42 -3
  9. aip_agents/ptc/__init__.pyi +5 -1
  10. aip_agents/ptc/custom_tools.py +473 -0
  11. aip_agents/ptc/custom_tools.pyi +184 -0
  12. aip_agents/ptc/custom_tools_payload.py +400 -0
  13. aip_agents/ptc/custom_tools_payload.pyi +31 -0
  14. aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
  15. aip_agents/ptc/custom_tools_templates/__init__.pyi +0 -0
  16. aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +23 -0
  17. aip_agents/ptc/custom_tools_templates/custom_init.py.template +15 -0
  18. aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +60 -0
  19. aip_agents/ptc/custom_tools_templates/custom_registry.py.template +87 -0
  20. aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
  21. aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +19 -0
  22. aip_agents/ptc/exceptions.py +18 -0
  23. aip_agents/ptc/exceptions.pyi +15 -0
  24. aip_agents/ptc/executor.py +151 -33
  25. aip_agents/ptc/executor.pyi +34 -8
  26. aip_agents/ptc/naming.py +13 -1
  27. aip_agents/ptc/naming.pyi +9 -0
  28. aip_agents/ptc/prompt_builder.py +118 -16
  29. aip_agents/ptc/prompt_builder.pyi +12 -8
  30. aip_agents/ptc/sandbox_bridge.py +206 -8
  31. aip_agents/ptc/sandbox_bridge.pyi +18 -5
  32. aip_agents/ptc/tool_def_helpers.py +101 -0
  33. aip_agents/ptc/tool_def_helpers.pyi +38 -0
  34. aip_agents/ptc/tool_enrichment.py +163 -0
  35. aip_agents/ptc/tool_enrichment.pyi +60 -0
  36. aip_agents/sandbox/defaults.py +197 -1
  37. aip_agents/sandbox/defaults.pyi +28 -0
  38. aip_agents/sandbox/e2b_runtime.py +28 -0
  39. aip_agents/sandbox/e2b_runtime.pyi +7 -1
  40. aip_agents/sandbox/template_builder.py +2 -2
  41. aip_agents/tools/execute_ptc_code.py +59 -10
  42. aip_agents/tools/execute_ptc_code.pyi +5 -5
  43. aip_agents/tools/memory_search/mem0.py +8 -2
  44. {aip_agents_binary-0.6.5.dist-info → aip_agents_binary-0.6.7.dist-info}/METADATA +3 -3
  45. {aip_agents_binary-0.6.5.dist-info → aip_agents_binary-0.6.7.dist-info}/RECORD +47 -27
  46. {aip_agents_binary-0.6.5.dist-info → aip_agents_binary-0.6.7.dist-info}/WHEEL +0 -0
  47. {aip_agents_binary-0.6.5.dist-info → aip_agents_binary-0.6.7.dist-info}/top_level.txt +0 -0
@@ -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}"
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]
@@ -0,0 +1,60 @@
1
+ """Safe invoke helper for custom tools.
2
+
3
+ This module provides a sync wrapper around tool.ainvoke for use in PTC code.
4
+ """
5
+
6
+ import asyncio
7
+ from typing import Any
8
+
9
+ from langchain_core.tools import BaseTool
10
+
11
+
12
+ def run_tool(tool: BaseTool, kwargs: dict[str, Any]) -> Any:
13
+ """Run a tool's ainvoke method synchronously.
14
+
15
+ Args:
16
+ tool: Tool instance to invoke.
17
+ kwargs: Arguments to pass to the tool.
18
+
19
+ Returns:
20
+ Tool execution result.
21
+ """
22
+ return _run_async_safely(tool.ainvoke(kwargs))
23
+
24
+
25
+ def _run_async_safely(coro) -> Any:
26
+ """Run an async coroutine safely in sync context.
27
+
28
+ Handles the case where we may or may not be in an async context.
29
+
30
+ Args:
31
+ coro: Coroutine to run.
32
+
33
+ Returns:
34
+ Result of the coroutine.
35
+ """
36
+ try:
37
+ asyncio.get_running_loop()
38
+ except RuntimeError:
39
+ # No running loop - create new one
40
+ return asyncio.run(coro)
41
+
42
+ # Already in async context - run in a separate thread with a new loop
43
+ import concurrent.futures
44
+
45
+ def run_in_new_loop():
46
+ """Run coroutine in a new event loop in a thread."""
47
+ new_loop = asyncio.new_event_loop()
48
+ asyncio.set_event_loop(new_loop)
49
+ try:
50
+ result = new_loop.run_until_complete(coro)
51
+ new_loop.run_until_complete(new_loop.shutdown_asyncgens())
52
+ if hasattr(new_loop, "shutdown_default_executor"):
53
+ new_loop.run_until_complete(new_loop.shutdown_default_executor())
54
+ return result
55
+ finally:
56
+ new_loop.close()
57
+
58
+ with concurrent.futures.ThreadPoolExecutor() as executor:
59
+ future = executor.submit(run_in_new_loop)
60
+ return future.result()
@@ -0,0 +1,87 @@
1
+ """Generated registry for custom tools.
2
+
3
+ This module provides factory functions to build tool instances from metadata.
4
+ """
5
+
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from langchain_core.tools import BaseTool
11
+
12
+ $imports
13
+
14
+ _DEFAULTS_PATH = Path(__file__).with_name("custom_defaults.json")
15
+
16
+ # Attribute name for tool config schema
17
+ TOOL_CONFIG_SCHEMA_ATTR = "tool_config_schema"
18
+
19
+
20
+ def _load_defaults() -> dict[str, Any]:
21
+ """Load tool config defaults from JSON file."""
22
+ if not _DEFAULTS_PATH.exists():
23
+ return {}
24
+ return json.loads(_DEFAULTS_PATH.read_text(encoding="utf-8"))
25
+
26
+
27
+ def _inject_config_methods(tool: BaseTool, config_schema: type) -> None:
28
+ """Inject configuration methods into a tool instance.
29
+
30
+ Args:
31
+ tool: Tool instance to inject methods into.
32
+ config_schema: Pydantic model class for configuration validation.
33
+ """
34
+ CONFIG_SCHEMA_ATTR = "_config_schema"
35
+ CONFIG_ATTR = "_config"
36
+
37
+ # Add configuration attributes
38
+ object.__setattr__(tool, CONFIG_SCHEMA_ATTR, config_schema)
39
+ object.__setattr__(tool, CONFIG_ATTR, None)
40
+
41
+ def set_tool_config(config: Any) -> None:
42
+ """Set the tool's agent-level default configuration with validation."""
43
+ if config is None:
44
+ object.__setattr__(tool, CONFIG_ATTR, None)
45
+ return
46
+
47
+ config_schema_obj = getattr(tool, CONFIG_SCHEMA_ATTR)
48
+ if isinstance(config, dict):
49
+ object.__setattr__(tool, CONFIG_ATTR, config_schema_obj(**config))
50
+ elif isinstance(config, config_schema_obj):
51
+ object.__setattr__(tool, CONFIG_ATTR, config)
52
+ else:
53
+ raise ValueError(f"Config must be an instance of {config_schema_obj.__name__} or dict")
54
+
55
+ def get_tool_config(config: Any = None) -> Any:
56
+ """Get the effective tool configuration."""
57
+ return getattr(tool, CONFIG_ATTR)
58
+
59
+ # Inject methods
60
+ object.__setattr__(tool, "set_tool_config", set_tool_config)
61
+ object.__setattr__(tool, "get_tool_config", get_tool_config)
62
+
63
+
64
+ def _configure_tool(tool: BaseTool, tool_name: str) -> None:
65
+ """Configure tool with defaults if it has tool_config_schema.
66
+
67
+ Args:
68
+ tool: Tool instance to configure.
69
+ tool_name: Name of the tool for defaults lookup.
70
+ """
71
+ defaults_by_tool = _load_defaults()
72
+ has_defaults = tool_name in defaults_by_tool
73
+ defaults = defaults_by_tool.get(tool_name)
74
+
75
+ # Check if tool has tool_config_schema
76
+ if hasattr(tool, TOOL_CONFIG_SCHEMA_ATTR):
77
+ # Inject config methods if not present
78
+ if not hasattr(tool, "get_tool_config"):
79
+ config_schema = getattr(tool, TOOL_CONFIG_SCHEMA_ATTR)
80
+ _inject_config_methods(tool, config_schema)
81
+
82
+ # Set tool config when defaults explicitly provided (even empty dict)
83
+ if has_defaults and hasattr(tool, "set_tool_config"):
84
+ tool.set_tool_config(defaults)
85
+
86
+
87
+ $build_functions
@@ -0,0 +1,7 @@
1
+ """Generated custom tool sources package.
2
+
3
+ This package contains the source code for file-based custom tools.
4
+ Each tool is in its own subpackage.
5
+ """
6
+
7
+ __all__ = [$all_list]
@@ -0,0 +1,19 @@
1
+ """Generated wrapper for custom tool: $original_name
2
+
3
+ This module provides a sync wrapper for calling the tool in PTC code.
4
+ """
5
+
6
+ from tools.custom_registry import build_$sanitized_name
7
+ from tools.custom_invoke import run_tool
8
+
9
+
10
+ def $sanitized_name(**kwargs):
11
+ """Call the $original_name tool.
12
+
13
+ Args:
14
+ **kwargs: Arguments to pass to the tool.
15
+
16
+ Returns:
17
+ Tool execution result.
18
+ """
19
+ return run_tool(build_$sanitized_name(), kwargs)
@@ -37,3 +37,21 @@ class PTCToolError(PTCError):
37
37
  super().__init__(message)
38
38
  self.server_name = server_name
39
39
  self.tool_name = tool_name
40
+
41
+
42
+ class PTCPayloadConflictError(PTCError):
43
+ """Error when merging payloads with conflicting file paths.
44
+
45
+ Attributes:
46
+ conflicts: Set of conflicting file paths.
47
+ """
48
+
49
+ def __init__(self, message: str, conflicts: set[str]) -> None:
50
+ """Initialize PTCPayloadConflictError.
51
+
52
+ Args:
53
+ message: Error message.
54
+ conflicts: Set of conflicting file paths.
55
+ """
56
+ super().__init__(message)
57
+ self.conflicts = conflicts
@@ -20,3 +20,18 @@ class PTCToolError(PTCError):
20
20
  server_name: The MCP server name (optional).
21
21
  tool_name: The tool name (optional).
22
22
  """
23
+
24
+ class PTCPayloadConflictError(PTCError):
25
+ """Error when merging payloads with conflicting file paths.
26
+
27
+ Attributes:
28
+ conflicts: Set of conflicting file paths.
29
+ """
30
+ conflicts: Incomplete
31
+ def __init__(self, message: str, conflicts: set[str]) -> None:
32
+ """Initialize PTCPayloadConflictError.
33
+
34
+ Args:
35
+ message: Error message.
36
+ conflicts: Set of conflicting file paths.
37
+ """