aip-agents-binary 0.5.25__py3-none-macosx_13_0_arm64.whl → 0.6.8__py3-none-macosx_13_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aip_agents/agent/__init__.py +44 -4
- aip_agents/agent/base_langgraph_agent.py +163 -74
- aip_agents/agent/base_langgraph_agent.pyi +3 -2
- aip_agents/agent/langgraph_memory_enhancer_agent.py +368 -34
- aip_agents/agent/langgraph_memory_enhancer_agent.pyi +3 -2
- aip_agents/agent/langgraph_react_agent.py +329 -22
- aip_agents/agent/langgraph_react_agent.pyi +41 -2
- aip_agents/examples/hello_world_ptc.py +49 -0
- aip_agents/examples/hello_world_ptc.pyi +5 -0
- aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
- aip_agents/examples/hello_world_ptc_custom_tools.pyi +7 -0
- aip_agents/examples/hello_world_tool_output_client.py +9 -0
- aip_agents/examples/tools/multiply_tool.py +43 -0
- aip_agents/examples/tools/multiply_tool.pyi +18 -0
- aip_agents/guardrails/engines/base.py +6 -6
- aip_agents/mcp/client/__init__.py +38 -2
- aip_agents/mcp/client/connection_manager.py +36 -1
- aip_agents/mcp/client/connection_manager.pyi +3 -0
- aip_agents/mcp/client/persistent_session.py +318 -68
- aip_agents/mcp/client/persistent_session.pyi +9 -0
- aip_agents/mcp/client/transports.py +37 -2
- aip_agents/mcp/client/transports.pyi +9 -0
- aip_agents/memory/adapters/base_adapter.py +98 -0
- aip_agents/memory/adapters/base_adapter.pyi +25 -0
- aip_agents/ptc/__init__.py +87 -0
- aip_agents/ptc/__init__.pyi +14 -0
- aip_agents/ptc/custom_tools.py +473 -0
- aip_agents/ptc/custom_tools.pyi +184 -0
- aip_agents/ptc/custom_tools_payload.py +400 -0
- aip_agents/ptc/custom_tools_payload.pyi +31 -0
- aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
- aip_agents/ptc/custom_tools_templates/__init__.pyi +0 -0
- aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +23 -0
- aip_agents/ptc/custom_tools_templates/custom_init.py.template +15 -0
- aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +60 -0
- aip_agents/ptc/custom_tools_templates/custom_registry.py.template +87 -0
- aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
- aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +19 -0
- aip_agents/ptc/doc_gen.py +122 -0
- aip_agents/ptc/doc_gen.pyi +40 -0
- aip_agents/ptc/exceptions.py +57 -0
- aip_agents/ptc/exceptions.pyi +37 -0
- aip_agents/ptc/executor.py +261 -0
- aip_agents/ptc/executor.pyi +99 -0
- aip_agents/ptc/mcp/__init__.py +45 -0
- aip_agents/ptc/mcp/__init__.pyi +7 -0
- aip_agents/ptc/mcp/sandbox_bridge.py +668 -0
- aip_agents/ptc/mcp/sandbox_bridge.pyi +47 -0
- aip_agents/ptc/mcp/templates/__init__.py +1 -0
- aip_agents/ptc/mcp/templates/__init__.pyi +0 -0
- aip_agents/ptc/mcp/templates/mcp_client.py.template +239 -0
- aip_agents/ptc/naming.py +196 -0
- aip_agents/ptc/naming.pyi +85 -0
- aip_agents/ptc/payload.py +26 -0
- aip_agents/ptc/payload.pyi +15 -0
- aip_agents/ptc/prompt_builder.py +673 -0
- aip_agents/ptc/prompt_builder.pyi +59 -0
- aip_agents/ptc/ptc_helper.py +16 -0
- aip_agents/ptc/ptc_helper.pyi +1 -0
- aip_agents/ptc/sandbox_bridge.py +256 -0
- aip_agents/ptc/sandbox_bridge.pyi +38 -0
- aip_agents/ptc/template_utils.py +33 -0
- aip_agents/ptc/template_utils.pyi +13 -0
- aip_agents/ptc/templates/__init__.py +1 -0
- aip_agents/ptc/templates/__init__.pyi +0 -0
- aip_agents/ptc/templates/ptc_helper.py.template +134 -0
- aip_agents/ptc/tool_def_helpers.py +101 -0
- aip_agents/ptc/tool_def_helpers.pyi +38 -0
- aip_agents/ptc/tool_enrichment.py +163 -0
- aip_agents/ptc/tool_enrichment.pyi +60 -0
- aip_agents/sandbox/__init__.py +43 -0
- aip_agents/sandbox/__init__.pyi +5 -0
- aip_agents/sandbox/defaults.py +205 -0
- aip_agents/sandbox/defaults.pyi +30 -0
- aip_agents/sandbox/e2b_runtime.py +295 -0
- aip_agents/sandbox/e2b_runtime.pyi +57 -0
- aip_agents/sandbox/template_builder.py +131 -0
- aip_agents/sandbox/template_builder.pyi +36 -0
- aip_agents/sandbox/types.py +24 -0
- aip_agents/sandbox/types.pyi +14 -0
- aip_agents/sandbox/validation.py +50 -0
- aip_agents/sandbox/validation.pyi +20 -0
- aip_agents/sentry/sentry.py +29 -8
- aip_agents/sentry/sentry.pyi +3 -2
- aip_agents/tools/__init__.py +13 -2
- aip_agents/tools/__init__.pyi +3 -1
- aip_agents/tools/browser_use/browser_use_tool.py +8 -0
- aip_agents/tools/browser_use/streaming.py +2 -0
- aip_agents/tools/date_range_tool.py +554 -0
- aip_agents/tools/date_range_tool.pyi +21 -0
- aip_agents/tools/execute_ptc_code.py +357 -0
- aip_agents/tools/execute_ptc_code.pyi +90 -0
- aip_agents/tools/memory_search/__init__.py +8 -1
- aip_agents/tools/memory_search/__init__.pyi +3 -3
- aip_agents/tools/memory_search/mem0.py +114 -1
- aip_agents/tools/memory_search/mem0.pyi +11 -1
- aip_agents/tools/memory_search/schema.py +33 -0
- aip_agents/tools/memory_search/schema.pyi +10 -0
- aip_agents/tools/memory_search_tool.py +8 -0
- aip_agents/tools/memory_search_tool.pyi +2 -2
- aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +26 -1
- aip_agents/utils/langgraph/tool_output_management.py +80 -0
- aip_agents/utils/langgraph/tool_output_management.pyi +37 -0
- {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/METADATA +9 -19
- {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/RECORD +107 -41
- {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/WHEEL +1 -1
- aip_agents/examples/demo_memory_recall.py +0 -401
- aip_agents/examples/demo_memory_recall.pyi +0 -58
- {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/top_level.txt +0 -0
|
@@ -106,6 +106,31 @@ class BaseMemoryAdapter(BaseMemory):
|
|
|
106
106
|
Returns:
|
|
107
107
|
List of memory hits matching the criteria.
|
|
108
108
|
"""
|
|
109
|
+
def delete_by_query(self, *, query: str, user_id: str, top_k: int | None = None, threshold: float | None = 0.3, filters: dict[str, Any] | None = None) -> list[dict[str, Any]]:
|
|
110
|
+
"""Delete memories matching a query.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
query: The search query string used to find memories to delete.
|
|
114
|
+
user_id: User identifier for the deletion scope.
|
|
115
|
+
top_k: Maximum number of memories to delete.
|
|
116
|
+
threshold: Minimum similarity threshold for deletion.
|
|
117
|
+
filters: Optional filters to apply to the deletion scope.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
List of deleted memory hits.
|
|
121
|
+
"""
|
|
122
|
+
def delete(self, *, memory_ids: list[str] | None, user_id: str, metadata: dict[str, Any] | None = None, categories: list[str] | None = None) -> Any:
|
|
123
|
+
"""Delete memories by IDs or by user scope when IDs are None.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
memory_ids: Optional list of memory IDs to delete.
|
|
127
|
+
user_id: User identifier for the deletion scope.
|
|
128
|
+
metadata: Optional metadata filters to constrain deletion.
|
|
129
|
+
categories: Optional categories to filter by (best-effort).
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Backend-specific delete result or None on failure.
|
|
133
|
+
"""
|
|
109
134
|
def save_interaction(self, *, user_text: str, ai_text: str, user_id: str) -> None:
|
|
110
135
|
"""Save a user-AI interaction as memories.
|
|
111
136
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""PTC (Programmatic Tool Calling) core module.
|
|
2
|
+
|
|
3
|
+
This module provides core PTC functionality, including executor, prompt builder,
|
|
4
|
+
sandbox bridge, and custom tool configuration validation.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from aip_agents.ptc.custom_tools import (
|
|
11
|
+
PTCCustomToolConfig,
|
|
12
|
+
PTCCustomToolValidationError,
|
|
13
|
+
PTCFileToolDef,
|
|
14
|
+
PTCPackageToolDef,
|
|
15
|
+
PTCToolDef,
|
|
16
|
+
enrich_tool_def_with_metadata,
|
|
17
|
+
extract_tool_metadata,
|
|
18
|
+
validate_custom_tool_config,
|
|
19
|
+
)
|
|
20
|
+
from aip_agents.ptc.custom_tools_payload import (
|
|
21
|
+
CustomToolPayloadResult,
|
|
22
|
+
build_custom_tools_payload,
|
|
23
|
+
)
|
|
24
|
+
from aip_agents.ptc.exceptions import PTCError, PTCToolError
|
|
25
|
+
from aip_agents.ptc.prompt_builder import PromptConfig, build_ptc_prompt, compute_ptc_prompt_hash
|
|
26
|
+
from aip_agents.ptc.tool_def_helpers import file_tool, package_tool
|
|
27
|
+
from aip_agents.ptc.tool_enrichment import (
|
|
28
|
+
build_tool_lookup,
|
|
29
|
+
enrich_custom_tools_from_agent,
|
|
30
|
+
match_tool_by_name,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
# Exceptions
|
|
35
|
+
"PTCError",
|
|
36
|
+
"PTCToolError",
|
|
37
|
+
"PTCCustomToolValidationError",
|
|
38
|
+
# Executor
|
|
39
|
+
"PTCSandboxConfig",
|
|
40
|
+
"PTCSandboxExecutor",
|
|
41
|
+
# Custom tools
|
|
42
|
+
"PTCCustomToolConfig",
|
|
43
|
+
"PTCToolDef",
|
|
44
|
+
"PTCPackageToolDef",
|
|
45
|
+
"PTCFileToolDef",
|
|
46
|
+
"validate_custom_tool_config",
|
|
47
|
+
"extract_tool_metadata",
|
|
48
|
+
"enrich_tool_def_with_metadata",
|
|
49
|
+
# Tool enrichment
|
|
50
|
+
"build_tool_lookup",
|
|
51
|
+
"match_tool_by_name",
|
|
52
|
+
"enrich_custom_tools_from_agent",
|
|
53
|
+
# Tool definition helpers
|
|
54
|
+
"package_tool",
|
|
55
|
+
"file_tool",
|
|
56
|
+
# Custom tools payload
|
|
57
|
+
"CustomToolPayloadResult",
|
|
58
|
+
"build_custom_tools_payload",
|
|
59
|
+
# Prompt builder
|
|
60
|
+
"PromptConfig",
|
|
61
|
+
"build_ptc_prompt",
|
|
62
|
+
"compute_ptc_prompt_hash",
|
|
63
|
+
# Sandbox bridge
|
|
64
|
+
"build_sandbox_payload",
|
|
65
|
+
"wrap_ptc_code",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def __getattr__(name: str):
|
|
70
|
+
"""Lazy import to avoid circular dependencies."""
|
|
71
|
+
if name == "PTCSandboxConfig":
|
|
72
|
+
from aip_agents.ptc.executor import PTCSandboxConfig
|
|
73
|
+
|
|
74
|
+
return PTCSandboxConfig
|
|
75
|
+
elif name == "PTCSandboxExecutor":
|
|
76
|
+
from aip_agents.ptc.executor import PTCSandboxExecutor
|
|
77
|
+
|
|
78
|
+
return PTCSandboxExecutor
|
|
79
|
+
elif name == "build_sandbox_payload":
|
|
80
|
+
from aip_agents.ptc.sandbox_bridge import build_sandbox_payload
|
|
81
|
+
|
|
82
|
+
return build_sandbox_payload
|
|
83
|
+
elif name == "wrap_ptc_code":
|
|
84
|
+
from aip_agents.ptc.sandbox_bridge import wrap_ptc_code
|
|
85
|
+
|
|
86
|
+
return wrap_ptc_code
|
|
87
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from aip_agents.ptc.custom_tools import PTCCustomToolConfig as PTCCustomToolConfig, PTCCustomToolValidationError as PTCCustomToolValidationError, PTCFileToolDef as PTCFileToolDef, PTCPackageToolDef as PTCPackageToolDef, PTCToolDef as PTCToolDef, enrich_tool_def_with_metadata as enrich_tool_def_with_metadata, extract_tool_metadata as extract_tool_metadata, validate_custom_tool_config as validate_custom_tool_config
|
|
2
|
+
from aip_agents.ptc.custom_tools_payload import CustomToolPayloadResult as CustomToolPayloadResult, build_custom_tools_payload as build_custom_tools_payload
|
|
3
|
+
from aip_agents.ptc.exceptions import PTCError as PTCError, PTCToolError as PTCToolError
|
|
4
|
+
from aip_agents.ptc.prompt_builder import PromptConfig as PromptConfig, build_ptc_prompt as build_ptc_prompt, compute_ptc_prompt_hash as compute_ptc_prompt_hash
|
|
5
|
+
from aip_agents.ptc.tool_def_helpers import file_tool as file_tool, package_tool as package_tool
|
|
6
|
+
from aip_agents.ptc.tool_enrichment import build_tool_lookup as build_tool_lookup, enrich_custom_tools_from_agent as enrich_custom_tools_from_agent, match_tool_by_name as match_tool_by_name
|
|
7
|
+
|
|
8
|
+
__all__ = ['PTCError', 'PTCToolError', 'PTCCustomToolValidationError', 'PTCSandboxConfig', 'PTCSandboxExecutor', 'PTCCustomToolConfig', 'PTCToolDef', 'PTCPackageToolDef', 'PTCFileToolDef', 'validate_custom_tool_config', 'extract_tool_metadata', 'enrich_tool_def_with_metadata', 'build_tool_lookup', 'match_tool_by_name', 'enrich_custom_tools_from_agent', 'package_tool', 'file_tool', 'CustomToolPayloadResult', 'build_custom_tools_payload', 'PromptConfig', 'build_ptc_prompt', 'compute_ptc_prompt_hash', 'build_sandbox_payload', 'wrap_ptc_code']
|
|
9
|
+
|
|
10
|
+
# Names in __all__ with no definition:
|
|
11
|
+
# PTCSandboxConfig
|
|
12
|
+
# PTCSandboxExecutor
|
|
13
|
+
# build_sandbox_payload
|
|
14
|
+
# wrap_ptc_code
|
|
@@ -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]
|