aip-agents-binary 0.5.25__py3-none-macosx_13_0_arm64.whl → 0.6.8__py3-none-macosx_13_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aip_agents/agent/__init__.py +44 -4
- aip_agents/agent/base_langgraph_agent.py +163 -74
- aip_agents/agent/base_langgraph_agent.pyi +3 -2
- aip_agents/agent/langgraph_memory_enhancer_agent.py +368 -34
- aip_agents/agent/langgraph_memory_enhancer_agent.pyi +3 -2
- aip_agents/agent/langgraph_react_agent.py +329 -22
- aip_agents/agent/langgraph_react_agent.pyi +41 -2
- aip_agents/examples/hello_world_ptc.py +49 -0
- aip_agents/examples/hello_world_ptc.pyi +5 -0
- aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
- aip_agents/examples/hello_world_ptc_custom_tools.pyi +7 -0
- aip_agents/examples/hello_world_tool_output_client.py +9 -0
- aip_agents/examples/tools/multiply_tool.py +43 -0
- aip_agents/examples/tools/multiply_tool.pyi +18 -0
- aip_agents/guardrails/engines/base.py +6 -6
- aip_agents/mcp/client/__init__.py +38 -2
- aip_agents/mcp/client/connection_manager.py +36 -1
- aip_agents/mcp/client/connection_manager.pyi +3 -0
- aip_agents/mcp/client/persistent_session.py +318 -68
- aip_agents/mcp/client/persistent_session.pyi +9 -0
- aip_agents/mcp/client/transports.py +37 -2
- aip_agents/mcp/client/transports.pyi +9 -0
- aip_agents/memory/adapters/base_adapter.py +98 -0
- aip_agents/memory/adapters/base_adapter.pyi +25 -0
- aip_agents/ptc/__init__.py +87 -0
- aip_agents/ptc/__init__.pyi +14 -0
- aip_agents/ptc/custom_tools.py +473 -0
- aip_agents/ptc/custom_tools.pyi +184 -0
- aip_agents/ptc/custom_tools_payload.py +400 -0
- aip_agents/ptc/custom_tools_payload.pyi +31 -0
- aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
- aip_agents/ptc/custom_tools_templates/__init__.pyi +0 -0
- aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +23 -0
- aip_agents/ptc/custom_tools_templates/custom_init.py.template +15 -0
- aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +60 -0
- aip_agents/ptc/custom_tools_templates/custom_registry.py.template +87 -0
- aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
- aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +19 -0
- aip_agents/ptc/doc_gen.py +122 -0
- aip_agents/ptc/doc_gen.pyi +40 -0
- aip_agents/ptc/exceptions.py +57 -0
- aip_agents/ptc/exceptions.pyi +37 -0
- aip_agents/ptc/executor.py +261 -0
- aip_agents/ptc/executor.pyi +99 -0
- aip_agents/ptc/mcp/__init__.py +45 -0
- aip_agents/ptc/mcp/__init__.pyi +7 -0
- aip_agents/ptc/mcp/sandbox_bridge.py +668 -0
- aip_agents/ptc/mcp/sandbox_bridge.pyi +47 -0
- aip_agents/ptc/mcp/templates/__init__.py +1 -0
- aip_agents/ptc/mcp/templates/__init__.pyi +0 -0
- aip_agents/ptc/mcp/templates/mcp_client.py.template +239 -0
- aip_agents/ptc/naming.py +196 -0
- aip_agents/ptc/naming.pyi +85 -0
- aip_agents/ptc/payload.py +26 -0
- aip_agents/ptc/payload.pyi +15 -0
- aip_agents/ptc/prompt_builder.py +673 -0
- aip_agents/ptc/prompt_builder.pyi +59 -0
- aip_agents/ptc/ptc_helper.py +16 -0
- aip_agents/ptc/ptc_helper.pyi +1 -0
- aip_agents/ptc/sandbox_bridge.py +256 -0
- aip_agents/ptc/sandbox_bridge.pyi +38 -0
- aip_agents/ptc/template_utils.py +33 -0
- aip_agents/ptc/template_utils.pyi +13 -0
- aip_agents/ptc/templates/__init__.py +1 -0
- aip_agents/ptc/templates/__init__.pyi +0 -0
- aip_agents/ptc/templates/ptc_helper.py.template +134 -0
- aip_agents/ptc/tool_def_helpers.py +101 -0
- aip_agents/ptc/tool_def_helpers.pyi +38 -0
- aip_agents/ptc/tool_enrichment.py +163 -0
- aip_agents/ptc/tool_enrichment.pyi +60 -0
- aip_agents/sandbox/__init__.py +43 -0
- aip_agents/sandbox/__init__.pyi +5 -0
- aip_agents/sandbox/defaults.py +205 -0
- aip_agents/sandbox/defaults.pyi +30 -0
- aip_agents/sandbox/e2b_runtime.py +295 -0
- aip_agents/sandbox/e2b_runtime.pyi +57 -0
- aip_agents/sandbox/template_builder.py +131 -0
- aip_agents/sandbox/template_builder.pyi +36 -0
- aip_agents/sandbox/types.py +24 -0
- aip_agents/sandbox/types.pyi +14 -0
- aip_agents/sandbox/validation.py +50 -0
- aip_agents/sandbox/validation.pyi +20 -0
- aip_agents/sentry/sentry.py +29 -8
- aip_agents/sentry/sentry.pyi +3 -2
- aip_agents/tools/__init__.py +13 -2
- aip_agents/tools/__init__.pyi +3 -1
- aip_agents/tools/browser_use/browser_use_tool.py +8 -0
- aip_agents/tools/browser_use/streaming.py +2 -0
- aip_agents/tools/date_range_tool.py +554 -0
- aip_agents/tools/date_range_tool.pyi +21 -0
- aip_agents/tools/execute_ptc_code.py +357 -0
- aip_agents/tools/execute_ptc_code.pyi +90 -0
- aip_agents/tools/memory_search/__init__.py +8 -1
- aip_agents/tools/memory_search/__init__.pyi +3 -3
- aip_agents/tools/memory_search/mem0.py +114 -1
- aip_agents/tools/memory_search/mem0.pyi +11 -1
- aip_agents/tools/memory_search/schema.py +33 -0
- aip_agents/tools/memory_search/schema.pyi +10 -0
- aip_agents/tools/memory_search_tool.py +8 -0
- aip_agents/tools/memory_search_tool.pyi +2 -2
- aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +26 -1
- aip_agents/utils/langgraph/tool_output_management.py +80 -0
- aip_agents/utils/langgraph/tool_output_management.pyi +37 -0
- {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/METADATA +9 -19
- {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/RECORD +107 -41
- {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/WHEEL +1 -1
- aip_agents/examples/demo_memory_recall.py +0 -401
- aip_agents/examples/demo_memory_recall.pyi +0 -58
- {aip_agents_binary-0.5.25.dist-info → aip_agents_binary-0.6.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
"""PTC Prompt Builder.
|
|
2
|
+
|
|
3
|
+
Generates usage guidance prompts for PTC that help the LLM correctly use
|
|
4
|
+
the execute_ptc_code tool with proper import patterns and parameter naming.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
14
|
+
|
|
15
|
+
from aip_agents.ptc.custom_tools import PTCCustomToolConfig
|
|
16
|
+
from aip_agents.ptc.naming import (
|
|
17
|
+
example_value_from_schema,
|
|
18
|
+
sanitize_function_name,
|
|
19
|
+
sanitize_module_name_with_reserved,
|
|
20
|
+
sanitize_param_name,
|
|
21
|
+
schema_to_params,
|
|
22
|
+
)
|
|
23
|
+
from aip_agents.utils.logger import get_logger
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from aip_agents.mcp.client.base_mcp_client import BaseMCPClient
|
|
27
|
+
|
|
28
|
+
logger = get_logger(__name__)
|
|
29
|
+
|
|
30
|
+
# Prompt mode type alias
|
|
31
|
+
PromptMode = Literal["minimal", "index", "full", "auto"]
|
|
32
|
+
|
|
33
|
+
# Markdown constants
|
|
34
|
+
PYTHON_BLOCK_START = "```python"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class PromptConfig:
|
|
39
|
+
"""Configuration for PTC prompt generation.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
mode: Prompt mode - minimal, index, full, or auto.
|
|
43
|
+
auto_threshold: Total tool count threshold for auto mode (default 10).
|
|
44
|
+
include_example: Whether to include example code in prompt.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
mode: PromptMode = "auto"
|
|
48
|
+
auto_threshold: int = 10
|
|
49
|
+
include_example: bool = True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Shared PTC usage rules block (DRY: used in both placeholder and full prompts)
|
|
53
|
+
PTC_USAGE_RULES = """## PTC (Programmatic Tool Calling) Usage
|
|
54
|
+
|
|
55
|
+
When using `execute_ptc_code`, follow these rules:
|
|
56
|
+
|
|
57
|
+
1. **Import patterns**:
|
|
58
|
+
- MCP tools: `from tools.<server> import <tool_name>`
|
|
59
|
+
- Custom tools: `from tools.custom import <tool_name>`
|
|
60
|
+
2. **Output**: Only `print()` output is returned to you. Always print results.
|
|
61
|
+
3. **Parameter names**: All parameters are lowercase with underscores.
|
|
62
|
+
- Example: `userId` becomes `userid`, `user-id` becomes `user_id`
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def build_ptc_prompt(
|
|
67
|
+
mcp_client: BaseMCPClient | None = None,
|
|
68
|
+
config: PromptConfig | None = None,
|
|
69
|
+
custom_tools_config: PTCCustomToolConfig | None = None,
|
|
70
|
+
) -> str:
|
|
71
|
+
"""Build PTC usage guidance prompt from MCP and custom tool configurations.
|
|
72
|
+
|
|
73
|
+
Generates a short usage block that includes:
|
|
74
|
+
- The import patterns: MCP (`from tools.<server> import <tool>`) and
|
|
75
|
+
custom (`from tools.custom import <tool>`)
|
|
76
|
+
- Rule: use `print()`; only printed output returns
|
|
77
|
+
- Rule: parameter names are sanitized to lowercase/underscored
|
|
78
|
+
- Prompt mode content (minimal/index/full)
|
|
79
|
+
- Examples based on the resolved prompt mode
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
mcp_client: The MCP client with configured servers. Can be None if only custom tools.
|
|
83
|
+
config: Prompt configuration. If None, uses default PromptConfig.
|
|
84
|
+
custom_tools_config: Optional custom LangChain tools configuration.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
PTC usage guidance prompt string.
|
|
88
|
+
"""
|
|
89
|
+
if config is None:
|
|
90
|
+
config = PromptConfig()
|
|
91
|
+
|
|
92
|
+
# Collect MCP server info (sorted for deterministic output)
|
|
93
|
+
server_infos: list[dict[str, Any]] = []
|
|
94
|
+
if mcp_client and mcp_client.servers:
|
|
95
|
+
for server_name in sorted(mcp_client.servers.keys()):
|
|
96
|
+
tools = _get_server_tools(mcp_client, server_name)
|
|
97
|
+
server_infos.append({"name": server_name, "tools": tools})
|
|
98
|
+
|
|
99
|
+
# Collect custom tool info
|
|
100
|
+
custom_tool_infos: list[dict[str, Any]] = []
|
|
101
|
+
if custom_tools_config and custom_tools_config.enabled and custom_tools_config.tools:
|
|
102
|
+
custom_tool_infos = _get_custom_tool_infos(custom_tools_config)
|
|
103
|
+
|
|
104
|
+
# Check if we have any tools
|
|
105
|
+
if not server_infos and not custom_tool_infos:
|
|
106
|
+
return _build_placeholder_prompt()
|
|
107
|
+
|
|
108
|
+
# Resolve mode and build appropriate prompt
|
|
109
|
+
resolved_mode = _resolve_mode(config, server_infos, custom_tool_infos)
|
|
110
|
+
|
|
111
|
+
if resolved_mode == "minimal":
|
|
112
|
+
return _build_minimal_prompt(server_infos, config.include_example, custom_tool_infos)
|
|
113
|
+
elif resolved_mode == "index":
|
|
114
|
+
return _build_index_prompt(server_infos, config.include_example, custom_tool_infos)
|
|
115
|
+
else: # full
|
|
116
|
+
return _build_full_prompt(server_infos, config.include_example, custom_tool_infos)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _get_server_tools(
|
|
120
|
+
mcp_client: BaseMCPClient,
|
|
121
|
+
server_name: str,
|
|
122
|
+
) -> list[dict[str, Any]]:
|
|
123
|
+
"""Get tool definitions for a server.
|
|
124
|
+
|
|
125
|
+
When tools are not loaded but allowed_tools exists, returns stub tool entries.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
mcp_client: MCP client instance.
|
|
129
|
+
server_name: Name of the server.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
List of tool definitions with name, description, and input_schema.
|
|
133
|
+
Stubs have empty description and minimal schema when tools not loaded.
|
|
134
|
+
"""
|
|
135
|
+
tools: list[dict[str, Any]] = []
|
|
136
|
+
allowed_tools: list[str] | None = None
|
|
137
|
+
raw_tools: list[Any] = []
|
|
138
|
+
try:
|
|
139
|
+
# Try to get cached tools from session pool
|
|
140
|
+
session = mcp_client.session_pool.get_session(server_name)
|
|
141
|
+
allowed_tools = session.allowed_tools if session.allowed_tools else None
|
|
142
|
+
|
|
143
|
+
# Get tools from session (public attribute on PersistentMCPSession)
|
|
144
|
+
raw_tools = list(getattr(session, "tools", []))
|
|
145
|
+
except (KeyError, AttributeError) as e:
|
|
146
|
+
logger.debug(f"Could not get tools for server '{server_name}': {e}")
|
|
147
|
+
|
|
148
|
+
if allowed_tools is None:
|
|
149
|
+
allowed_tools = _get_allowed_tools_from_config(mcp_client, server_name)
|
|
150
|
+
|
|
151
|
+
if not raw_tools and allowed_tools:
|
|
152
|
+
# Tools not loaded but allowlist exists - return stub entries
|
|
153
|
+
for tool_name in sorted(allowed_tools):
|
|
154
|
+
tools.append(
|
|
155
|
+
{
|
|
156
|
+
"name": tool_name,
|
|
157
|
+
"description": "",
|
|
158
|
+
"input_schema": {"type": "object", "properties": {}},
|
|
159
|
+
"stub": True,
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
elif raw_tools:
|
|
163
|
+
# Tools loaded - return actual tool definitions
|
|
164
|
+
for tool in raw_tools:
|
|
165
|
+
if allowed_tools and tool.name not in allowed_tools:
|
|
166
|
+
continue
|
|
167
|
+
tools.append(
|
|
168
|
+
{
|
|
169
|
+
"name": tool.name,
|
|
170
|
+
"description": tool.description or "",
|
|
171
|
+
"input_schema": tool.inputSchema,
|
|
172
|
+
"stub": False,
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
return tools
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _get_allowed_tools_from_config(mcp_client: BaseMCPClient, server_name: str) -> list[str] | None:
|
|
179
|
+
"""Extract allowed_tools from MCP client server config.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
mcp_client: MCP client instance.
|
|
183
|
+
server_name: Server name to look up.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
List of allowed tools or None.
|
|
187
|
+
"""
|
|
188
|
+
if not mcp_client or not mcp_client.servers:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
config = mcp_client.servers.get(server_name)
|
|
192
|
+
if not config:
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
raw_allowed = config.get("allowed_tools") if isinstance(config, dict) else getattr(config, "allowed_tools", None)
|
|
196
|
+
if raw_allowed and isinstance(raw_allowed, list):
|
|
197
|
+
return list(raw_allowed)
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _get_custom_tool_infos(custom_tools_config: PTCCustomToolConfig) -> list[dict[str, Any]]:
|
|
202
|
+
"""Get tool info dicts from custom tools configuration.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
custom_tools_config: Custom tools configuration.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
List of tool info dicts with name, description, and input_schema.
|
|
209
|
+
"""
|
|
210
|
+
tools: list[dict[str, Any]] = []
|
|
211
|
+
for tool_def in custom_tools_config.tools:
|
|
212
|
+
name = tool_def.get("name", "")
|
|
213
|
+
description = tool_def.get("description", "")
|
|
214
|
+
input_schema = tool_def.get("input_schema", {"type": "object", "properties": {}})
|
|
215
|
+
|
|
216
|
+
tools.append(
|
|
217
|
+
{
|
|
218
|
+
"name": name,
|
|
219
|
+
"description": description,
|
|
220
|
+
"input_schema": input_schema,
|
|
221
|
+
"stub": not description and not input_schema.get("properties"),
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
return tools
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _count_total_tools(
|
|
228
|
+
server_infos: list[dict[str, Any]],
|
|
229
|
+
custom_tool_infos: list[dict[str, Any]] | None = None,
|
|
230
|
+
) -> int:
|
|
231
|
+
"""Count total tools across all servers and custom tools.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
server_infos: List of server info dicts with name and tools.
|
|
235
|
+
custom_tool_infos: List of custom tool info dicts.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Total tool count.
|
|
239
|
+
"""
|
|
240
|
+
mcp_count = sum(len(info.get("tools", [])) for info in server_infos)
|
|
241
|
+
custom_count = len(custom_tool_infos) if custom_tool_infos else 0
|
|
242
|
+
return mcp_count + custom_count
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _resolve_mode(
|
|
246
|
+
config: PromptConfig,
|
|
247
|
+
server_infos: list[dict[str, Any]],
|
|
248
|
+
custom_tool_infos: list[dict[str, Any]] | None = None,
|
|
249
|
+
) -> PromptMode:
|
|
250
|
+
"""Resolve auto mode to concrete mode based on tool count.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
config: Prompt configuration.
|
|
254
|
+
server_infos: List of server info dicts.
|
|
255
|
+
custom_tool_infos: List of custom tool info dicts.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Resolved mode (minimal, index, or full).
|
|
259
|
+
"""
|
|
260
|
+
if config.mode != "auto":
|
|
261
|
+
return config.mode
|
|
262
|
+
|
|
263
|
+
total_tools = _count_total_tools(server_infos, custom_tool_infos)
|
|
264
|
+
if total_tools == 0 or total_tools > config.auto_threshold:
|
|
265
|
+
return "minimal"
|
|
266
|
+
return "full"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _build_discovery_example() -> str:
|
|
270
|
+
"""Build discovery example using ptc_helper module.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Discovery example code string.
|
|
274
|
+
"""
|
|
275
|
+
return """from tools.ptc_helper import list_tools, describe_tool
|
|
276
|
+
|
|
277
|
+
# List available tools in a package
|
|
278
|
+
tools = list_tools("package_name")
|
|
279
|
+
print([tool["name"] for tool in tools])
|
|
280
|
+
|
|
281
|
+
# Get details for a specific tool
|
|
282
|
+
doc = describe_tool("package_name", tools[0]["name"])
|
|
283
|
+
print(doc["doc"])"""
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _build_minimal_prompt(
|
|
287
|
+
server_infos: list[dict[str, Any]],
|
|
288
|
+
include_example: bool,
|
|
289
|
+
custom_tool_infos: list[dict[str, Any]] | None = None,
|
|
290
|
+
) -> str:
|
|
291
|
+
"""Build minimal prompt with rules and package list only.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
server_infos: List of server info dicts with name and tools.
|
|
295
|
+
include_example: Whether to include discovery example.
|
|
296
|
+
custom_tool_infos: List of custom tool info dicts.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Minimal PTC usage prompt.
|
|
300
|
+
"""
|
|
301
|
+
lines = [
|
|
302
|
+
PTC_USAGE_RULES.rstrip(),
|
|
303
|
+
"",
|
|
304
|
+
"### Available Packages",
|
|
305
|
+
"",
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
# List MCP packages (sorted reserved-safe sanitized names)
|
|
309
|
+
package_names = sorted(sanitize_module_name_with_reserved(info["name"]) for info in server_infos)
|
|
310
|
+
for pkg in package_names:
|
|
311
|
+
lines.append(f"- `tools.{pkg}`")
|
|
312
|
+
|
|
313
|
+
# Add custom tools package if present
|
|
314
|
+
if custom_tool_infos:
|
|
315
|
+
lines.append("- `tools.custom`")
|
|
316
|
+
|
|
317
|
+
lines.append("")
|
|
318
|
+
lines.append("Use `tools.ptc_helper` to discover available tools and their signatures.")
|
|
319
|
+
|
|
320
|
+
if include_example:
|
|
321
|
+
lines.extend(
|
|
322
|
+
[
|
|
323
|
+
"",
|
|
324
|
+
"### Discovery Example",
|
|
325
|
+
"",
|
|
326
|
+
PYTHON_BLOCK_START,
|
|
327
|
+
_build_discovery_example(),
|
|
328
|
+
"```",
|
|
329
|
+
]
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return "\n".join(lines)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _build_index_prompt(
|
|
336
|
+
server_infos: list[dict[str, Any]],
|
|
337
|
+
include_example: bool,
|
|
338
|
+
custom_tool_infos: list[dict[str, Any]] | None = None,
|
|
339
|
+
) -> str:
|
|
340
|
+
"""Build index prompt with rules, package list, and tool names.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
server_infos: List of server info dicts with name and tools.
|
|
344
|
+
include_example: Whether to include discovery example.
|
|
345
|
+
custom_tool_infos: List of custom tool info dicts.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Index PTC usage prompt.
|
|
349
|
+
"""
|
|
350
|
+
lines = [
|
|
351
|
+
PTC_USAGE_RULES.rstrip(),
|
|
352
|
+
"",
|
|
353
|
+
"### Available Tools",
|
|
354
|
+
"",
|
|
355
|
+
]
|
|
356
|
+
|
|
357
|
+
# Sort server infos by reserved-safe sanitized name for deterministic output
|
|
358
|
+
sorted_infos = sorted(server_infos, key=lambda x: sanitize_module_name_with_reserved(x["name"]))
|
|
359
|
+
|
|
360
|
+
for server_info in sorted_infos:
|
|
361
|
+
safe_server = sanitize_module_name_with_reserved(server_info["name"])
|
|
362
|
+
lines.append(f"**`tools.{safe_server}`**")
|
|
363
|
+
|
|
364
|
+
# Sort tools by sanitized name
|
|
365
|
+
sorted_tools = sorted(server_info["tools"], key=lambda t: sanitize_function_name(t["name"]))
|
|
366
|
+
tool_names = [sanitize_function_name(t["name"]) for t in sorted_tools]
|
|
367
|
+
lines.append(f" Tools: {', '.join(tool_names)}")
|
|
368
|
+
lines.append("")
|
|
369
|
+
|
|
370
|
+
# Add custom tools section if present
|
|
371
|
+
if custom_tool_infos:
|
|
372
|
+
lines.append("**`tools.custom`**")
|
|
373
|
+
sorted_custom = sorted(custom_tool_infos, key=lambda t: sanitize_function_name(t["name"]))
|
|
374
|
+
tool_names = [sanitize_function_name(t["name"]) for t in sorted_custom]
|
|
375
|
+
lines.append(f" Tools: {', '.join(tool_names)}")
|
|
376
|
+
lines.append("")
|
|
377
|
+
|
|
378
|
+
lines.append("Use `tools.ptc_helper` to get tool signatures and descriptions.")
|
|
379
|
+
|
|
380
|
+
if include_example:
|
|
381
|
+
lines.extend(
|
|
382
|
+
[
|
|
383
|
+
"",
|
|
384
|
+
"### Discovery Example",
|
|
385
|
+
"",
|
|
386
|
+
PYTHON_BLOCK_START,
|
|
387
|
+
_build_discovery_example(),
|
|
388
|
+
"```",
|
|
389
|
+
]
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return "\n".join(lines)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _build_full_prompt(
|
|
396
|
+
server_infos: list[dict[str, Any]],
|
|
397
|
+
include_example: bool,
|
|
398
|
+
custom_tool_infos: list[dict[str, Any]] | None = None,
|
|
399
|
+
) -> str:
|
|
400
|
+
"""Build full prompt with rules, signatures, and descriptions.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
server_infos: List of server info dicts with name and tools.
|
|
404
|
+
include_example: Whether to include real tool example.
|
|
405
|
+
custom_tool_infos: List of custom tool info dicts.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Full PTC usage prompt.
|
|
409
|
+
"""
|
|
410
|
+
lines = [
|
|
411
|
+
PTC_USAGE_RULES.rstrip(),
|
|
412
|
+
"",
|
|
413
|
+
"### Available Tools",
|
|
414
|
+
"",
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
# Sort server infos by reserved-safe sanitized name for deterministic output
|
|
418
|
+
sorted_infos = sorted(server_infos, key=lambda x: sanitize_module_name_with_reserved(x["name"]))
|
|
419
|
+
|
|
420
|
+
for server_info in sorted_infos:
|
|
421
|
+
safe_server = sanitize_module_name_with_reserved(server_info["name"])
|
|
422
|
+
lines.append(f"**Server: `{safe_server}`** (from `tools.{safe_server}`)")
|
|
423
|
+
lines.append("")
|
|
424
|
+
|
|
425
|
+
# Sort tools by sanitized name
|
|
426
|
+
sorted_tools = sorted(server_info["tools"], key=lambda t: sanitize_function_name(t["name"]))
|
|
427
|
+
|
|
428
|
+
for tool in sorted_tools:
|
|
429
|
+
func_name = sanitize_function_name(tool["name"])
|
|
430
|
+
schema = tool.get("input_schema", {})
|
|
431
|
+
params = schema_to_params(schema)
|
|
432
|
+
raw_desc = tool.get("description", "")
|
|
433
|
+
desc = raw_desc[:120]
|
|
434
|
+
if raw_desc and len(raw_desc) > 120:
|
|
435
|
+
desc += "..."
|
|
436
|
+
|
|
437
|
+
lines.append(f"- `{func_name}({params})`: {desc}")
|
|
438
|
+
|
|
439
|
+
lines.append("")
|
|
440
|
+
|
|
441
|
+
# Add custom tools section if present
|
|
442
|
+
if custom_tool_infos:
|
|
443
|
+
lines.append("**Custom Tools** (from `tools.custom`)")
|
|
444
|
+
lines.append("")
|
|
445
|
+
|
|
446
|
+
sorted_custom = sorted(custom_tool_infos, key=lambda t: sanitize_function_name(t["name"]))
|
|
447
|
+
|
|
448
|
+
for tool in sorted_custom:
|
|
449
|
+
func_name = sanitize_function_name(tool["name"])
|
|
450
|
+
schema = tool.get("input_schema", {})
|
|
451
|
+
params = schema_to_params(schema)
|
|
452
|
+
raw_desc = tool.get("description", "")
|
|
453
|
+
desc = raw_desc[:120]
|
|
454
|
+
if raw_desc and len(raw_desc) > 120:
|
|
455
|
+
desc += "..."
|
|
456
|
+
|
|
457
|
+
lines.append(f"- `{func_name}({params})`: {desc}")
|
|
458
|
+
|
|
459
|
+
lines.append("")
|
|
460
|
+
|
|
461
|
+
if include_example:
|
|
462
|
+
example = _build_example(server_infos, custom_tool_infos)
|
|
463
|
+
lines.extend(
|
|
464
|
+
[
|
|
465
|
+
"### Example",
|
|
466
|
+
"",
|
|
467
|
+
PYTHON_BLOCK_START,
|
|
468
|
+
example,
|
|
469
|
+
"```",
|
|
470
|
+
]
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
return "\n".join(lines)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _build_prompt_from_servers(server_infos: list[dict[str, Any]]) -> str:
|
|
477
|
+
"""Build prompt from collected server information (legacy, uses full mode).
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
server_infos: List of server info dicts with name and tools.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Formatted PTC usage prompt.
|
|
484
|
+
"""
|
|
485
|
+
return _build_full_prompt(server_infos, include_example=True)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _build_example(
|
|
489
|
+
server_infos: list[dict[str, Any]],
|
|
490
|
+
custom_tool_infos: list[dict[str, Any]] | None = None,
|
|
491
|
+
) -> str:
|
|
492
|
+
"""Build an example code snippet using the first available tool.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
server_infos: List of server info dicts.
|
|
496
|
+
custom_tool_infos: List of custom tool info dicts.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Example code string.
|
|
500
|
+
"""
|
|
501
|
+
# Try MCP tools first
|
|
502
|
+
if server_infos:
|
|
503
|
+
sorted_servers = sorted(server_infos, key=lambda info: sanitize_module_name_with_reserved(info["name"]))
|
|
504
|
+
for server in sorted_servers:
|
|
505
|
+
tools = server.get("tools", [])
|
|
506
|
+
if tools:
|
|
507
|
+
sorted_tools = sorted(tools, key=lambda t: sanitize_function_name(t["name"]))
|
|
508
|
+
tool = sorted_tools[0]
|
|
509
|
+
safe_server = sanitize_module_name_with_reserved(server["name"])
|
|
510
|
+
func_name = sanitize_function_name(tool["name"])
|
|
511
|
+
args_str = _build_example_args_from_schema(tool.get("input_schema", {}))
|
|
512
|
+
return f"""from tools.{safe_server} import {func_name}
|
|
513
|
+
|
|
514
|
+
result = {func_name}({args_str})
|
|
515
|
+
print(result)"""
|
|
516
|
+
|
|
517
|
+
# Fall back to custom tools
|
|
518
|
+
if custom_tool_infos:
|
|
519
|
+
sorted_custom = sorted(custom_tool_infos, key=lambda t: sanitize_function_name(t["name"]))
|
|
520
|
+
tool = sorted_custom[0]
|
|
521
|
+
func_name = sanitize_function_name(tool["name"])
|
|
522
|
+
args_str = _build_example_args_from_schema(tool.get("input_schema", {}))
|
|
523
|
+
return f"""from tools.custom import {func_name}
|
|
524
|
+
|
|
525
|
+
result = {func_name}({args_str})
|
|
526
|
+
print(result)"""
|
|
527
|
+
|
|
528
|
+
return _build_generic_example()
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _build_example_args_from_schema(schema: dict[str, Any]) -> str:
|
|
532
|
+
"""Build example arguments string from a JSON schema.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
schema: JSON schema for tool input.
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
Example arguments string.
|
|
539
|
+
"""
|
|
540
|
+
properties = schema.get("properties", {})
|
|
541
|
+
required = set(schema.get("required", []))
|
|
542
|
+
|
|
543
|
+
args: list[str] = []
|
|
544
|
+
for prop_name in sorted(required):
|
|
545
|
+
if prop_name not in properties:
|
|
546
|
+
continue
|
|
547
|
+
safe_name = sanitize_param_name(prop_name)
|
|
548
|
+
prop_schema = properties[prop_name]
|
|
549
|
+
example_value = _get_example_value(prop_schema, prop_name)
|
|
550
|
+
args.append(f"{safe_name}={example_value}")
|
|
551
|
+
|
|
552
|
+
for prop_name in sorted(properties.keys()):
|
|
553
|
+
if prop_name in required:
|
|
554
|
+
continue
|
|
555
|
+
if len(args) >= 2:
|
|
556
|
+
break
|
|
557
|
+
safe_name = sanitize_param_name(prop_name)
|
|
558
|
+
prop_schema = properties[prop_name]
|
|
559
|
+
example_value = _get_example_value(prop_schema, prop_name)
|
|
560
|
+
args.append(f"{safe_name}={example_value}")
|
|
561
|
+
|
|
562
|
+
return ", ".join(args) if args else ""
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _get_example_value(prop_schema: dict[str, Any], prop_name: str) -> str:
|
|
566
|
+
"""Generate an example value for a parameter.
|
|
567
|
+
|
|
568
|
+
Prefers schema-provided examples, defaults, or enums.
|
|
569
|
+
Falls back to type-based placeholders.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
prop_schema: Property schema from JSON schema.
|
|
573
|
+
prop_name: Original property name.
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
Example value as a Python literal string.
|
|
577
|
+
"""
|
|
578
|
+
return example_value_from_schema(prop_schema)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _build_generic_example() -> str:
|
|
582
|
+
"""Build a generic example when no tools are available.
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
Generic example code string.
|
|
586
|
+
"""
|
|
587
|
+
return """from tools.server_name import tool_name
|
|
588
|
+
|
|
589
|
+
result = tool_name(param="value")
|
|
590
|
+
print(result)"""
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _build_placeholder_prompt() -> str:
|
|
594
|
+
"""Build a placeholder prompt when no MCP servers are configured.
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
Placeholder PTC usage prompt.
|
|
598
|
+
"""
|
|
599
|
+
return PTC_USAGE_RULES + "\n*No MCP servers configured yet. Tools will be available after MCP setup.*\n"
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _build_server_hash_part(mcp_client: BaseMCPClient, server_name: str) -> str:
|
|
603
|
+
"""Build hash part for a single MCP server.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
mcp_client: MCP client instance.
|
|
607
|
+
server_name: Name of the server.
|
|
608
|
+
|
|
609
|
+
Returns:
|
|
610
|
+
Hash part string for the server.
|
|
611
|
+
"""
|
|
612
|
+
try:
|
|
613
|
+
session = mcp_client.session_pool.get_session(server_name)
|
|
614
|
+
tools = list(getattr(session, "tools", []))
|
|
615
|
+
tool_names = sorted(t.name for t in tools)
|
|
616
|
+
|
|
617
|
+
allowed = session.allowed_tools if hasattr(session, "allowed_tools") else None
|
|
618
|
+
if not allowed:
|
|
619
|
+
allowed = _get_allowed_tools_from_config(mcp_client, server_name)
|
|
620
|
+
allowed_str = ",".join(sorted(allowed)) if allowed else "*"
|
|
621
|
+
|
|
622
|
+
return f"{server_name}:{','.join(tool_names)}|allowed={allowed_str}"
|
|
623
|
+
except (KeyError, AttributeError):
|
|
624
|
+
allowed = _get_allowed_tools_from_config(mcp_client, server_name)
|
|
625
|
+
allowed_str = ",".join(sorted(allowed)) if allowed else "*"
|
|
626
|
+
return f"{server_name}:|allowed={allowed_str}"
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def compute_ptc_prompt_hash(
|
|
630
|
+
mcp_client: BaseMCPClient | None = None,
|
|
631
|
+
config: PromptConfig | None = None,
|
|
632
|
+
custom_tools_config: PTCCustomToolConfig | None = None,
|
|
633
|
+
) -> str:
|
|
634
|
+
"""Compute a hash of the MCP and custom tool configuration for change detection.
|
|
635
|
+
|
|
636
|
+
Includes PromptConfig fields, allowed_tools, and custom tools in hash computation
|
|
637
|
+
so prompt updates re-sync correctly when configuration changes.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
mcp_client: MCP client instance. Can be None if only custom tools.
|
|
641
|
+
config: Prompt configuration. If None, uses default PromptConfig.
|
|
642
|
+
custom_tools_config: Optional custom LangChain tools configuration.
|
|
643
|
+
|
|
644
|
+
Returns:
|
|
645
|
+
Hash string representing current configuration.
|
|
646
|
+
"""
|
|
647
|
+
import hashlib
|
|
648
|
+
|
|
649
|
+
if config is None:
|
|
650
|
+
config = PromptConfig()
|
|
651
|
+
|
|
652
|
+
# Include config fields in hash
|
|
653
|
+
config_part = f"mode={config.mode}|threshold={config.auto_threshold}|example={config.include_example}"
|
|
654
|
+
|
|
655
|
+
# Create hash from server names, tool names, and allowed_tools
|
|
656
|
+
parts: list[str] = [config_part]
|
|
657
|
+
|
|
658
|
+
# Add MCP server parts
|
|
659
|
+
if mcp_client and mcp_client.servers:
|
|
660
|
+
for server_name in sorted(mcp_client.servers.keys()):
|
|
661
|
+
parts.append(_build_server_hash_part(mcp_client, server_name))
|
|
662
|
+
|
|
663
|
+
# Add custom tools parts
|
|
664
|
+
if custom_tools_config and custom_tools_config.enabled and custom_tools_config.tools:
|
|
665
|
+
custom_tool_names = sorted(sanitize_function_name(t.get("name", "")) for t in custom_tools_config.tools)
|
|
666
|
+
parts.append(f"custom:{','.join(custom_tool_names)}")
|
|
667
|
+
|
|
668
|
+
# Return empty hash if no tools configured
|
|
669
|
+
if len(parts) == 1:
|
|
670
|
+
return ""
|
|
671
|
+
|
|
672
|
+
content = "|".join(parts)
|
|
673
|
+
return hashlib.sha256(content.encode()).hexdigest()[:16]
|