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,357 @@
|
|
|
1
|
+
"""Execute PTC Code Tool.
|
|
2
|
+
|
|
3
|
+
This module provides a LangChain tool for executing Python code with MCP tool access
|
|
4
|
+
inside an E2B sandbox. The tool is designed for LLM-generated code that needs to call
|
|
5
|
+
multiple MCP tools programmatically.
|
|
6
|
+
|
|
7
|
+
Authors:
|
|
8
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import concurrent.futures
|
|
13
|
+
import json
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
from langchain_core.callbacks import (
|
|
17
|
+
AsyncCallbackManagerForToolRun,
|
|
18
|
+
CallbackManagerForToolRun,
|
|
19
|
+
)
|
|
20
|
+
from langchain_core.tools import BaseTool
|
|
21
|
+
from pydantic import BaseModel, Field
|
|
22
|
+
|
|
23
|
+
from aip_agents.ptc.naming import sanitize_function_name
|
|
24
|
+
from aip_agents.tools.tool_config_injector import TOOL_CONFIGS_KEY
|
|
25
|
+
from aip_agents.utils.logger import get_logger
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from aip_agents.mcp.client.base_mcp_client import BaseMCPClient
|
|
29
|
+
from aip_agents.ptc.executor import PTCSandboxConfig, PTCSandboxExecutor
|
|
30
|
+
from aip_agents.sandbox.e2b_runtime import E2BSandboxRuntime
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PTCCodeInput(BaseModel):
|
|
36
|
+
"""Input schema for PTCCodeTool."""
|
|
37
|
+
|
|
38
|
+
code: str = Field(
|
|
39
|
+
...,
|
|
40
|
+
description=(
|
|
41
|
+
"Python code to execute. Import MCP tools from the generated `tools` package, "
|
|
42
|
+
"for example: `from tools.yfinance import get_stock_history`, and custom tools "
|
|
43
|
+
"from `tools.custom`. The code runs in a sandboxed environment with access "
|
|
44
|
+
"to all configured MCP and custom tools. "
|
|
45
|
+
"Use print() to output results. The tool returns JSON with keys: "
|
|
46
|
+
"ok, stdout, stderr, exit_code."
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _merge_config_layer(
|
|
52
|
+
merged: dict[str, dict[str, Any]],
|
|
53
|
+
source: dict[str, Any],
|
|
54
|
+
skip_tool_configs_key: bool = True,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Merge a single layer of tool configs into the merged dict.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
merged: Target dict to merge into (modified in place).
|
|
60
|
+
source: Source dict containing tool configs.
|
|
61
|
+
skip_tool_configs_key: Whether to skip the TOOL_CONFIGS_KEY entry.
|
|
62
|
+
"""
|
|
63
|
+
for name, config in source.items():
|
|
64
|
+
if skip_tool_configs_key and name == TOOL_CONFIGS_KEY:
|
|
65
|
+
continue
|
|
66
|
+
if not isinstance(config, dict):
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
sanitized = sanitize_function_name(name)
|
|
70
|
+
if sanitized in merged:
|
|
71
|
+
merged[sanitized].update(config)
|
|
72
|
+
else:
|
|
73
|
+
merged[sanitized] = dict(config)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def merge_tool_configs(
|
|
77
|
+
agent_configs: dict[str, Any] | None,
|
|
78
|
+
runtime_configs: dict[str, Any] | None,
|
|
79
|
+
) -> dict[str, dict[str, Any]]:
|
|
80
|
+
"""Merge agent-level and runtime tool configs with sanitized keys.
|
|
81
|
+
|
|
82
|
+
Merges tool configurations from two sources:
|
|
83
|
+
1. Agent-level defaults (from agent.tool_configs)
|
|
84
|
+
2. Runtime overrides (from RunnableConfig.metadata["tool_configs"])
|
|
85
|
+
|
|
86
|
+
Both sources support two formats (matching LangGraphReactAgent behavior):
|
|
87
|
+
- Direct per-tool keys: {"time_tool": {"timezone": "UTC"}}
|
|
88
|
+
- Nested structure: {"tool_configs": {"time_tool": {"timezone": "UTC"}}}
|
|
89
|
+
|
|
90
|
+
The nested "tool_configs" key has higher precedence than direct keys.
|
|
91
|
+
Tool names are sanitized to match sandbox expectations (e.g., "Time Tool" -> "time_tool").
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
agent_configs: Agent-level tool configs (may be None or contain nested dicts)
|
|
95
|
+
runtime_configs: Runtime overrides from metadata (may be None)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Merged dict with sanitized tool names as keys and config dicts as values.
|
|
99
|
+
Only includes entries that are dicts (non-dict values are agent-wide defaults).
|
|
100
|
+
"""
|
|
101
|
+
merged: dict[str, dict[str, Any]] = {}
|
|
102
|
+
|
|
103
|
+
# Layer 1: Agent-level per-tool configs (direct keys)
|
|
104
|
+
if agent_configs:
|
|
105
|
+
_merge_config_layer(merged, agent_configs, skip_tool_configs_key=True)
|
|
106
|
+
|
|
107
|
+
# Layer 2: Agent-level per-tool configs (nested tool_configs key)
|
|
108
|
+
if agent_configs:
|
|
109
|
+
nested_agent = agent_configs.get(TOOL_CONFIGS_KEY)
|
|
110
|
+
if isinstance(nested_agent, dict):
|
|
111
|
+
_merge_config_layer(merged, nested_agent, skip_tool_configs_key=False)
|
|
112
|
+
|
|
113
|
+
# Layer 3: Runtime per-tool configs (direct keys, override agent defaults)
|
|
114
|
+
if runtime_configs:
|
|
115
|
+
_merge_config_layer(merged, runtime_configs, skip_tool_configs_key=True)
|
|
116
|
+
|
|
117
|
+
# Layer 4: Runtime per-tool configs (nested tool_configs key, highest precedence)
|
|
118
|
+
if runtime_configs:
|
|
119
|
+
nested_runtime = runtime_configs.get(TOOL_CONFIGS_KEY)
|
|
120
|
+
if isinstance(nested_runtime, dict):
|
|
121
|
+
_merge_config_layer(merged, nested_runtime, skip_tool_configs_key=False)
|
|
122
|
+
|
|
123
|
+
return merged
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class PTCCodeTool(BaseTool):
|
|
127
|
+
"""Tool for executing Python code with MCP tool access in a sandbox.
|
|
128
|
+
|
|
129
|
+
This tool uses BaseTool to properly access runtime config via run_manager.metadata.
|
|
130
|
+
The config parameter is NOT exposed to the LLM schema - it's extracted from
|
|
131
|
+
the callback manager during execution.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
name: str = "execute_ptc_code"
|
|
135
|
+
description: str = (
|
|
136
|
+
"Execute Python code that can call MCP tools programmatically. "
|
|
137
|
+
"Import tools from the generated `tools` package (e.g., `from tools.yfinance import get_stock`) "
|
|
138
|
+
"and custom tools from `tools.custom` when enabled. "
|
|
139
|
+
"Run normal Python code. Use print() to output results. "
|
|
140
|
+
"Returns JSON with ok, stdout, stderr, and exit_code keys. "
|
|
141
|
+
"This tool is useful for chaining multiple MCP tool calls with local data processing."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Input schema for LangChain tool invocation
|
|
145
|
+
args_schema: type[BaseModel] = PTCCodeInput
|
|
146
|
+
|
|
147
|
+
# Internal attributes (not exposed to LLM)
|
|
148
|
+
_ptc_executor: "PTCSandboxExecutor" = None # type: ignore[assignment]
|
|
149
|
+
_ptc_runtime: "E2BSandboxRuntime" = None # type: ignore[assignment]
|
|
150
|
+
_agent_tool_configs: dict[str, Any] | None = None
|
|
151
|
+
|
|
152
|
+
def __init__(
|
|
153
|
+
self,
|
|
154
|
+
executor: "PTCSandboxExecutor",
|
|
155
|
+
runtime: "E2BSandboxRuntime",
|
|
156
|
+
agent_tool_configs: dict[str, Any] | None = None,
|
|
157
|
+
**kwargs: Any,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Initialize the PTC code tool.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
executor: The PTC sandbox executor.
|
|
163
|
+
runtime: The E2B sandbox runtime.
|
|
164
|
+
agent_tool_configs: Optional agent-level tool configs.
|
|
165
|
+
**kwargs: Additional keyword arguments passed to BaseTool.
|
|
166
|
+
"""
|
|
167
|
+
super().__init__(**kwargs)
|
|
168
|
+
# Store as private attributes to avoid Pydantic field issues
|
|
169
|
+
object.__setattr__(self, "_ptc_executor", executor)
|
|
170
|
+
object.__setattr__(self, "_ptc_runtime", runtime)
|
|
171
|
+
object.__setattr__(self, "_agent_tool_configs", agent_tool_configs)
|
|
172
|
+
|
|
173
|
+
def _run(
|
|
174
|
+
self,
|
|
175
|
+
code: str,
|
|
176
|
+
run_manager: CallbackManagerForToolRun | None = None,
|
|
177
|
+
) -> str:
|
|
178
|
+
"""Execute code synchronously (wraps async version)."""
|
|
179
|
+
# Extract runtime metadata from run_manager
|
|
180
|
+
runtime_metadata = None
|
|
181
|
+
if run_manager and hasattr(run_manager, "metadata"):
|
|
182
|
+
runtime_metadata = run_manager.metadata
|
|
183
|
+
|
|
184
|
+
# Run async version in sync context
|
|
185
|
+
try:
|
|
186
|
+
asyncio.get_running_loop()
|
|
187
|
+
except RuntimeError:
|
|
188
|
+
return asyncio.run(self._execute(code, runtime_metadata))
|
|
189
|
+
|
|
190
|
+
# Already in async context - run in thread
|
|
191
|
+
def run_in_new_loop() -> str:
|
|
192
|
+
new_loop = asyncio.new_event_loop()
|
|
193
|
+
asyncio.set_event_loop(new_loop)
|
|
194
|
+
try:
|
|
195
|
+
return new_loop.run_until_complete(self._execute(code, runtime_metadata))
|
|
196
|
+
finally:
|
|
197
|
+
new_loop.close()
|
|
198
|
+
|
|
199
|
+
with concurrent.futures.ThreadPoolExecutor() as executor_service:
|
|
200
|
+
future = executor_service.submit(run_in_new_loop)
|
|
201
|
+
return future.result()
|
|
202
|
+
|
|
203
|
+
async def _arun(
|
|
204
|
+
self,
|
|
205
|
+
code: str,
|
|
206
|
+
run_manager: AsyncCallbackManagerForToolRun | None = None,
|
|
207
|
+
) -> str:
|
|
208
|
+
"""Execute code asynchronously."""
|
|
209
|
+
# Extract runtime metadata from run_manager
|
|
210
|
+
runtime_metadata = None
|
|
211
|
+
if run_manager and hasattr(run_manager, "metadata"):
|
|
212
|
+
runtime_metadata = run_manager.metadata
|
|
213
|
+
|
|
214
|
+
return await self._execute(code, runtime_metadata)
|
|
215
|
+
|
|
216
|
+
async def _execute(
|
|
217
|
+
self,
|
|
218
|
+
code: str,
|
|
219
|
+
runtime_metadata: dict[str, Any] | None,
|
|
220
|
+
) -> str:
|
|
221
|
+
"""Internal execution logic."""
|
|
222
|
+
try:
|
|
223
|
+
# Merge agent defaults with runtime overrides
|
|
224
|
+
merged_configs = merge_tool_configs(
|
|
225
|
+
self._agent_tool_configs,
|
|
226
|
+
runtime_metadata,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
logger.info("Executing PTC code in sandbox")
|
|
230
|
+
result = await self._ptc_executor.execute_code(
|
|
231
|
+
code,
|
|
232
|
+
tool_configs=merged_configs or None,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if result.exit_code == 0:
|
|
236
|
+
logger.info("PTC code execution completed successfully")
|
|
237
|
+
payload = {
|
|
238
|
+
"ok": True,
|
|
239
|
+
"stdout": result.stdout,
|
|
240
|
+
"stderr": "",
|
|
241
|
+
"exit_code": 0,
|
|
242
|
+
}
|
|
243
|
+
return json.dumps(payload)
|
|
244
|
+
|
|
245
|
+
logger.warning(f"PTC code execution failed with exit code {result.exit_code}")
|
|
246
|
+
payload = {
|
|
247
|
+
"ok": False,
|
|
248
|
+
"stdout": result.stdout,
|
|
249
|
+
"stderr": result.stderr,
|
|
250
|
+
"exit_code": result.exit_code,
|
|
251
|
+
}
|
|
252
|
+
return json.dumps(payload)
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.error(f"PTC code execution failed: {e}")
|
|
256
|
+
payload = {
|
|
257
|
+
"ok": False,
|
|
258
|
+
"stdout": "",
|
|
259
|
+
"stderr": f"Execution failed: {type(e).__name__}: {e}",
|
|
260
|
+
"exit_code": 1,
|
|
261
|
+
}
|
|
262
|
+
return json.dumps(payload)
|
|
263
|
+
|
|
264
|
+
async def cleanup(self) -> None:
|
|
265
|
+
"""Clean up the sandbox runtime."""
|
|
266
|
+
await self._ptc_runtime.cleanup()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _get_user_provided_packages(config: "PTCSandboxConfig | None") -> list[str] | None:
|
|
270
|
+
"""Determine if user explicitly provided ptc_packages.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
config: Optional sandbox executor configuration.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
None if packages not explicitly set (equals DEFAULT_PTC_PACKAGES or config is None).
|
|
277
|
+
List of packages if user explicitly modified ptc_packages.
|
|
278
|
+
"""
|
|
279
|
+
from aip_agents.sandbox.defaults import DEFAULT_PTC_PACKAGES
|
|
280
|
+
|
|
281
|
+
if config is None or config.ptc_packages is None:
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
# Check if it's the default value (not user-modified)
|
|
285
|
+
if list(config.ptc_packages) == list(DEFAULT_PTC_PACKAGES):
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
# User explicitly changed ptc_packages
|
|
289
|
+
return config.ptc_packages
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def create_execute_ptc_code_tool(
|
|
293
|
+
mcp_client: "BaseMCPClient | None",
|
|
294
|
+
config: "PTCSandboxConfig | None" = None, # noqa: F821
|
|
295
|
+
agent_tool_configs: dict[str, Any] | None = None,
|
|
296
|
+
) -> PTCCodeTool:
|
|
297
|
+
r"""Create a tool that executes Python code with MCP and/or custom tool access.
|
|
298
|
+
|
|
299
|
+
The code runs inside an E2B sandbox with access to generated MCP tool modules
|
|
300
|
+
and/or custom LangChain tools. This tool is designed for LLM-generated code
|
|
301
|
+
that needs to call multiple tools programmatically in a single execution.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
mcp_client: The MCP client with configured servers. Can be None for custom-only configs.
|
|
305
|
+
config: Optional sandbox executor configuration.
|
|
306
|
+
agent_tool_configs: Optional agent-level tool configs (from agent.tool_configs).
|
|
307
|
+
These are merged with runtime overrides from RunnableConfig.metadata.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
PTCCodeTool configured for PTC code execution.
|
|
311
|
+
|
|
312
|
+
Example:
|
|
313
|
+
```python
|
|
314
|
+
from aip_agents.mcp.client import LangchainMCPClient
|
|
315
|
+
from aip_agents.tools.execute_ptc_code import create_execute_ptc_code_tool
|
|
316
|
+
|
|
317
|
+
mcp_client = LangchainMCPClient()
|
|
318
|
+
await mcp_client.add_server("yfinance", {...})
|
|
319
|
+
|
|
320
|
+
tool = create_execute_ptc_code_tool(mcp_client)
|
|
321
|
+
result = await tool.ainvoke({"code": "from tools.yfinance import get_stock\\nprint(get_stock('AAPL'))"})
|
|
322
|
+
```
|
|
323
|
+
"""
|
|
324
|
+
# Import here to avoid circular dependencies and allow lazy loading
|
|
325
|
+
from aip_agents.ptc.executor import PTCSandboxConfig, PTCSandboxExecutor
|
|
326
|
+
from aip_agents.sandbox.defaults import select_sandbox_packages
|
|
327
|
+
from aip_agents.sandbox.e2b_runtime import E2BSandboxRuntime
|
|
328
|
+
|
|
329
|
+
# Use provided config or create default
|
|
330
|
+
sandbox_config = config or PTCSandboxConfig()
|
|
331
|
+
|
|
332
|
+
# Determine if user explicitly provided packages (None means use smart selection)
|
|
333
|
+
user_ptc_packages = _get_user_provided_packages(config)
|
|
334
|
+
|
|
335
|
+
# Create a package selector callback that defers package selection until after
|
|
336
|
+
# sandbox creation, when we know if the template actually succeeded.
|
|
337
|
+
# This ensures smart package selection works correctly even with template fallback.
|
|
338
|
+
def package_selector(actual_template: str | None) -> list[str] | None:
|
|
339
|
+
return select_sandbox_packages(
|
|
340
|
+
mcp_client=mcp_client,
|
|
341
|
+
custom_tools_config=sandbox_config.custom_tools,
|
|
342
|
+
template=actual_template,
|
|
343
|
+
user_ptc_packages=user_ptc_packages,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Create runtime and executor
|
|
347
|
+
runtime = E2BSandboxRuntime(
|
|
348
|
+
template=sandbox_config.sandbox_template,
|
|
349
|
+
package_selector=package_selector,
|
|
350
|
+
)
|
|
351
|
+
executor = PTCSandboxExecutor(mcp_client, runtime, sandbox_config)
|
|
352
|
+
|
|
353
|
+
return PTCCodeTool(
|
|
354
|
+
executor=executor,
|
|
355
|
+
runtime=runtime,
|
|
356
|
+
agent_tool_configs=agent_tool_configs,
|
|
357
|
+
)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from _typeshed import Incomplete
|
|
2
|
+
from aip_agents.mcp.client.base_mcp_client import BaseMCPClient as BaseMCPClient
|
|
3
|
+
from aip_agents.ptc.executor import PTCSandboxConfig as PTCSandboxConfig, PTCSandboxExecutor as PTCSandboxExecutor
|
|
4
|
+
from aip_agents.ptc.naming import sanitize_function_name as sanitize_function_name
|
|
5
|
+
from aip_agents.sandbox.e2b_runtime import E2BSandboxRuntime as E2BSandboxRuntime
|
|
6
|
+
from aip_agents.tools.tool_config_injector import TOOL_CONFIGS_KEY as TOOL_CONFIGS_KEY
|
|
7
|
+
from aip_agents.utils.logger import get_logger as get_logger
|
|
8
|
+
from langchain_core.tools import BaseTool
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
logger: Incomplete
|
|
13
|
+
|
|
14
|
+
class PTCCodeInput(BaseModel):
|
|
15
|
+
"""Input schema for PTCCodeTool."""
|
|
16
|
+
code: str
|
|
17
|
+
|
|
18
|
+
def merge_tool_configs(agent_configs: dict[str, Any] | None, runtime_configs: dict[str, Any] | None) -> dict[str, dict[str, Any]]:
|
|
19
|
+
'''Merge agent-level and runtime tool configs with sanitized keys.
|
|
20
|
+
|
|
21
|
+
Merges tool configurations from two sources:
|
|
22
|
+
1. Agent-level defaults (from agent.tool_configs)
|
|
23
|
+
2. Runtime overrides (from RunnableConfig.metadata["tool_configs"])
|
|
24
|
+
|
|
25
|
+
Both sources support two formats (matching LangGraphReactAgent behavior):
|
|
26
|
+
- Direct per-tool keys: {"time_tool": {"timezone": "UTC"}}
|
|
27
|
+
- Nested structure: {"tool_configs": {"time_tool": {"timezone": "UTC"}}}
|
|
28
|
+
|
|
29
|
+
The nested "tool_configs" key has higher precedence than direct keys.
|
|
30
|
+
Tool names are sanitized to match sandbox expectations (e.g., "Time Tool" -> "time_tool").
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
agent_configs: Agent-level tool configs (may be None or contain nested dicts)
|
|
34
|
+
runtime_configs: Runtime overrides from metadata (may be None)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Merged dict with sanitized tool names as keys and config dicts as values.
|
|
38
|
+
Only includes entries that are dicts (non-dict values are agent-wide defaults).
|
|
39
|
+
'''
|
|
40
|
+
|
|
41
|
+
class PTCCodeTool(BaseTool):
|
|
42
|
+
"""Tool for executing Python code with MCP tool access in a sandbox.
|
|
43
|
+
|
|
44
|
+
This tool uses BaseTool to properly access runtime config via run_manager.metadata.
|
|
45
|
+
The config parameter is NOT exposed to the LLM schema - it's extracted from
|
|
46
|
+
the callback manager during execution.
|
|
47
|
+
"""
|
|
48
|
+
name: str
|
|
49
|
+
description: str
|
|
50
|
+
args_schema: type[BaseModel]
|
|
51
|
+
def __init__(self, executor: PTCSandboxExecutor, runtime: E2BSandboxRuntime, agent_tool_configs: dict[str, Any] | None = None, **kwargs: Any) -> None:
|
|
52
|
+
"""Initialize the PTC code tool.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
executor: The PTC sandbox executor.
|
|
56
|
+
runtime: The E2B sandbox runtime.
|
|
57
|
+
agent_tool_configs: Optional agent-level tool configs.
|
|
58
|
+
**kwargs: Additional keyword arguments passed to BaseTool.
|
|
59
|
+
"""
|
|
60
|
+
async def cleanup(self) -> None:
|
|
61
|
+
"""Clean up the sandbox runtime."""
|
|
62
|
+
|
|
63
|
+
def create_execute_ptc_code_tool(mcp_client: BaseMCPClient | None, config: PTCSandboxConfig | None = None, agent_tool_configs: dict[str, Any] | None = None) -> PTCCodeTool:
|
|
64
|
+
'''Create a tool that executes Python code with MCP and/or custom tool access.
|
|
65
|
+
|
|
66
|
+
The code runs inside an E2B sandbox with access to generated MCP tool modules
|
|
67
|
+
and/or custom LangChain tools. This tool is designed for LLM-generated code
|
|
68
|
+
that needs to call multiple tools programmatically in a single execution.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
mcp_client: The MCP client with configured servers. Can be None for custom-only configs.
|
|
72
|
+
config: Optional sandbox executor configuration.
|
|
73
|
+
agent_tool_configs: Optional agent-level tool configs (from agent.tool_configs).
|
|
74
|
+
These are merged with runtime overrides from RunnableConfig.metadata.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
PTCCodeTool configured for PTC code execution.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
```python
|
|
81
|
+
from aip_agents.mcp.client import LangchainMCPClient
|
|
82
|
+
from aip_agents.tools.execute_ptc_code import create_execute_ptc_code_tool
|
|
83
|
+
|
|
84
|
+
mcp_client = LangchainMCPClient()
|
|
85
|
+
await mcp_client.add_server("yfinance", {...})
|
|
86
|
+
|
|
87
|
+
tool = create_execute_ptc_code_tool(mcp_client)
|
|
88
|
+
result = await tool.ainvoke({"code": "from tools.yfinance import get_stock\\\\nprint(get_stock(\'AAPL\'))"})
|
|
89
|
+
```
|
|
90
|
+
'''
|
|
@@ -6,17 +6,24 @@ Authors:
|
|
|
6
6
|
|
|
7
7
|
from aip_agents.tools.memory_search.base import LongTermMemorySearchTool
|
|
8
8
|
from aip_agents.tools.memory_search.mem0 import (
|
|
9
|
+
MEMORY_DELETE_TOOL_NAME,
|
|
9
10
|
MEMORY_SEARCH_TOOL_NAME,
|
|
11
|
+
Mem0DeleteInput,
|
|
12
|
+
Mem0DeleteTool,
|
|
10
13
|
Mem0SearchInput,
|
|
11
14
|
Mem0SearchTool,
|
|
12
15
|
)
|
|
13
|
-
from aip_agents.tools.memory_search.schema import LongTermMemorySearchInput, MemoryConfig
|
|
16
|
+
from aip_agents.tools.memory_search.schema import LongTermMemoryDeleteInput, LongTermMemorySearchInput, MemoryConfig
|
|
14
17
|
|
|
15
18
|
__all__ = [
|
|
16
19
|
"MemoryConfig",
|
|
20
|
+
"LongTermMemoryDeleteInput",
|
|
17
21
|
"LongTermMemorySearchInput",
|
|
18
22
|
"LongTermMemorySearchTool",
|
|
23
|
+
"Mem0DeleteInput",
|
|
24
|
+
"Mem0DeleteTool",
|
|
19
25
|
"Mem0SearchInput",
|
|
20
26
|
"Mem0SearchTool",
|
|
27
|
+
"MEMORY_DELETE_TOOL_NAME",
|
|
21
28
|
"MEMORY_SEARCH_TOOL_NAME",
|
|
22
29
|
]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from aip_agents.tools.memory_search.base import LongTermMemorySearchTool as LongTermMemorySearchTool
|
|
2
|
-
from aip_agents.tools.memory_search.mem0 import MEMORY_SEARCH_TOOL_NAME as MEMORY_SEARCH_TOOL_NAME, Mem0SearchInput as Mem0SearchInput, Mem0SearchTool as Mem0SearchTool
|
|
3
|
-
from aip_agents.tools.memory_search.schema import LongTermMemorySearchInput as LongTermMemorySearchInput, MemoryConfig as MemoryConfig
|
|
2
|
+
from aip_agents.tools.memory_search.mem0 import MEMORY_DELETE_TOOL_NAME as MEMORY_DELETE_TOOL_NAME, MEMORY_SEARCH_TOOL_NAME as MEMORY_SEARCH_TOOL_NAME, Mem0DeleteInput as Mem0DeleteInput, Mem0DeleteTool as Mem0DeleteTool, Mem0SearchInput as Mem0SearchInput, Mem0SearchTool as Mem0SearchTool
|
|
3
|
+
from aip_agents.tools.memory_search.schema import LongTermMemoryDeleteInput as LongTermMemoryDeleteInput, LongTermMemorySearchInput as LongTermMemorySearchInput, MemoryConfig as MemoryConfig
|
|
4
4
|
|
|
5
|
-
__all__ = ['MemoryConfig', 'LongTermMemorySearchInput', 'LongTermMemorySearchTool', 'Mem0SearchInput', 'Mem0SearchTool', 'MEMORY_SEARCH_TOOL_NAME']
|
|
5
|
+
__all__ = ['MemoryConfig', 'LongTermMemoryDeleteInput', 'LongTermMemorySearchInput', 'LongTermMemorySearchTool', 'Mem0DeleteInput', 'Mem0DeleteTool', 'Mem0SearchInput', 'Mem0SearchTool', 'MEMORY_DELETE_TOOL_NAME', 'MEMORY_SEARCH_TOOL_NAME']
|
|
@@ -13,13 +13,14 @@ from langchain_core.runnables import RunnableConfig
|
|
|
13
13
|
|
|
14
14
|
from aip_agents.memory.constants import MemoryDefaults
|
|
15
15
|
from aip_agents.tools.memory_search.base import LongTermMemorySearchTool
|
|
16
|
-
from aip_agents.tools.memory_search.schema import LongTermMemorySearchInput
|
|
16
|
+
from aip_agents.tools.memory_search.schema import LongTermMemoryDeleteInput, LongTermMemorySearchInput
|
|
17
17
|
from aip_agents.utils.datetime import is_valid_date_string, next_day_iso
|
|
18
18
|
from aip_agents.utils.logger import get_logger
|
|
19
19
|
|
|
20
20
|
logger = get_logger(__name__)
|
|
21
21
|
|
|
22
22
|
MEMORY_SEARCH_TOOL_NAME = "built_in_mem0_search"
|
|
23
|
+
MEMORY_DELETE_TOOL_NAME = "built_in_mem0_delete"
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class Mem0SearchTool(LongTermMemorySearchTool):
|
|
@@ -256,3 +257,115 @@ class Mem0SearchTool(LongTermMemorySearchTool):
|
|
|
256
257
|
|
|
257
258
|
|
|
258
259
|
Mem0SearchInput = LongTermMemorySearchInput
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class Mem0DeleteTool(LongTermMemorySearchTool):
|
|
263
|
+
"""Mem0-specific implementation of the long-term memory delete tool."""
|
|
264
|
+
|
|
265
|
+
name: str = MEMORY_DELETE_TOOL_NAME
|
|
266
|
+
description: str = (
|
|
267
|
+
"Delete memories from long-term mem0 storage. Supports three modes:\n"
|
|
268
|
+
"1. DELETE BY IDS: Provide 'memory_ids'\n"
|
|
269
|
+
"2. DELETE BY QUERY: Provide 'query'\n"
|
|
270
|
+
"3. DELETE ALL: Provide 'delete_all=true' with no query/IDs\n"
|
|
271
|
+
)
|
|
272
|
+
args_schema: type[LongTermMemoryDeleteInput] = LongTermMemoryDeleteInput
|
|
273
|
+
LOG_PREFIX: ClassVar[str] = "Mem0DeleteTool"
|
|
274
|
+
METADATA_FILTER_BLOCKLIST: ClassVar[set[str]] = {"user_id", "memory_user_id"}
|
|
275
|
+
|
|
276
|
+
async def _arun(
|
|
277
|
+
self,
|
|
278
|
+
query: str | None = None,
|
|
279
|
+
config: RunnableConfig | None = None,
|
|
280
|
+
run_manager: Any | None = None,
|
|
281
|
+
**kwargs: Any,
|
|
282
|
+
) -> str:
|
|
283
|
+
"""Execute the memory delete asynchronously for LangChain.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
query: Semantic delete query when provided.
|
|
287
|
+
config: Runnable configuration containing LangChain metadata.
|
|
288
|
+
run_manager: LangChain callbacks (unused).
|
|
289
|
+
**kwargs: Additional arguments such as ``memory_ids``, ``delete_all``, ``metadata``.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
str: JSON-encoded delete result or an error message.
|
|
293
|
+
"""
|
|
294
|
+
logger.info("%s: Received config: %s", self.LOG_PREFIX, config)
|
|
295
|
+
|
|
296
|
+
memory_ids: list[str] | None = kwargs.get("memory_ids")
|
|
297
|
+
delete_all: bool | None = kwargs.get("delete_all")
|
|
298
|
+
threshold: float | None = kwargs.get("threshold")
|
|
299
|
+
top_k: int | None = kwargs.get("top_k")
|
|
300
|
+
categories: list[str] | None = kwargs.get("categories")
|
|
301
|
+
metadata: dict[str, Any] | None = kwargs.get("metadata")
|
|
302
|
+
|
|
303
|
+
user_id = self._resolve_user_id(metadata=metadata, config=config)
|
|
304
|
+
|
|
305
|
+
metadata_filter = None
|
|
306
|
+
if isinstance(metadata, dict):
|
|
307
|
+
metadata_filter = {k: v for k, v in metadata.items() if k not in self.METADATA_FILTER_BLOCKLIST} or None
|
|
308
|
+
|
|
309
|
+
if memory_ids:
|
|
310
|
+
if not hasattr(self.memory, "delete"):
|
|
311
|
+
return f"Error executing memory tool '{self.name}': backend does not support delete()"
|
|
312
|
+
mode = "ids"
|
|
313
|
+
result = self.memory.delete( # type: ignore[attr-defined]
|
|
314
|
+
memory_ids=memory_ids,
|
|
315
|
+
user_id=user_id,
|
|
316
|
+
metadata=metadata_filter,
|
|
317
|
+
categories=categories,
|
|
318
|
+
)
|
|
319
|
+
elif query:
|
|
320
|
+
if not hasattr(self.memory, "delete_by_query"):
|
|
321
|
+
return f"Error executing memory tool '{self.name}': backend does not support delete_by_query()"
|
|
322
|
+
mode = "query"
|
|
323
|
+
filters: dict[str, Any] | None = None
|
|
324
|
+
if metadata_filter or categories:
|
|
325
|
+
filters = {}
|
|
326
|
+
if metadata_filter:
|
|
327
|
+
filters["metadata"] = metadata_filter
|
|
328
|
+
if categories:
|
|
329
|
+
filters["categories"] = categories
|
|
330
|
+
result = self.memory.delete_by_query( # type: ignore[attr-defined]
|
|
331
|
+
query=query,
|
|
332
|
+
user_id=user_id,
|
|
333
|
+
threshold=threshold,
|
|
334
|
+
top_k=top_k,
|
|
335
|
+
filters=filters,
|
|
336
|
+
)
|
|
337
|
+
elif delete_all:
|
|
338
|
+
if not hasattr(self.memory, "delete"):
|
|
339
|
+
return f"Error executing memory tool '{self.name}': backend does not support delete()"
|
|
340
|
+
mode = "all"
|
|
341
|
+
result = self.memory.delete( # type: ignore[attr-defined]
|
|
342
|
+
memory_ids=None,
|
|
343
|
+
user_id=user_id,
|
|
344
|
+
metadata=metadata_filter,
|
|
345
|
+
categories=categories,
|
|
346
|
+
)
|
|
347
|
+
else:
|
|
348
|
+
return f"Error executing memory tool '{self.name}': provide memory_ids, query, or delete_all=true."
|
|
349
|
+
|
|
350
|
+
count = None
|
|
351
|
+
if isinstance(result, dict):
|
|
352
|
+
count = result.get("count") or result.get("deleted") or result.get("total")
|
|
353
|
+
|
|
354
|
+
logger.info(
|
|
355
|
+
"%s: delete mode=%s user_id='%s' count=%s",
|
|
356
|
+
self.LOG_PREFIX,
|
|
357
|
+
mode,
|
|
358
|
+
user_id,
|
|
359
|
+
count if count is not None else "unknown",
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
payload = {"status": "success", "mode": mode}
|
|
363
|
+
try:
|
|
364
|
+
json.dumps(result)
|
|
365
|
+
payload["result"] = result
|
|
366
|
+
except TypeError:
|
|
367
|
+
payload["result"] = str(result)
|
|
368
|
+
return json.dumps(payload)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
Mem0DeleteInput = LongTermMemoryDeleteInput
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from _typeshed import Incomplete
|
|
2
2
|
from aip_agents.memory.constants import MemoryDefaults as MemoryDefaults
|
|
3
3
|
from aip_agents.tools.memory_search.base import LongTermMemorySearchTool as LongTermMemorySearchTool
|
|
4
|
-
from aip_agents.tools.memory_search.schema import LongTermMemorySearchInput as LongTermMemorySearchInput
|
|
4
|
+
from aip_agents.tools.memory_search.schema import LongTermMemoryDeleteInput as LongTermMemoryDeleteInput, LongTermMemorySearchInput as LongTermMemorySearchInput
|
|
5
5
|
from aip_agents.utils.datetime import is_valid_date_string as is_valid_date_string, next_day_iso as next_day_iso
|
|
6
6
|
from aip_agents.utils.logger import get_logger as get_logger
|
|
7
7
|
from typing import ClassVar
|
|
8
8
|
|
|
9
9
|
logger: Incomplete
|
|
10
10
|
MEMORY_SEARCH_TOOL_NAME: str
|
|
11
|
+
MEMORY_DELETE_TOOL_NAME: str
|
|
11
12
|
|
|
12
13
|
class Mem0SearchTool(LongTermMemorySearchTool):
|
|
13
14
|
"""Mem0-specific implementation of the long-term memory search tool."""
|
|
@@ -17,3 +18,12 @@ class Mem0SearchTool(LongTermMemorySearchTool):
|
|
|
17
18
|
LOG_PREFIX: ClassVar[str]
|
|
18
19
|
METADATA_FILTER_BLOCKLIST: ClassVar[set[str]]
|
|
19
20
|
Mem0SearchInput = LongTermMemorySearchInput
|
|
21
|
+
|
|
22
|
+
class Mem0DeleteTool(LongTermMemorySearchTool):
|
|
23
|
+
"""Mem0-specific implementation of the long-term memory delete tool."""
|
|
24
|
+
name: str
|
|
25
|
+
description: str
|
|
26
|
+
args_schema: type[LongTermMemoryDeleteInput]
|
|
27
|
+
LOG_PREFIX: ClassVar[str]
|
|
28
|
+
METADATA_FILTER_BLOCKLIST: ClassVar[set[str]]
|
|
29
|
+
Mem0DeleteInput = LongTermMemoryDeleteInput
|
|
@@ -46,3 +46,36 @@ class LongTermMemorySearchInput(BaseModel):
|
|
|
46
46
|
None,
|
|
47
47
|
description="Optional metadata dict to filter by (exact key-value match).",
|
|
48
48
|
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class LongTermMemoryDeleteInput(BaseModel):
|
|
52
|
+
"""Input schema for unified long-term memory deletion."""
|
|
53
|
+
|
|
54
|
+
query: str | None = Field(
|
|
55
|
+
None,
|
|
56
|
+
description="Semantic query describing memories to delete. If provided, delete_by_user_query is used.",
|
|
57
|
+
)
|
|
58
|
+
memory_ids: list[str] | None = Field(
|
|
59
|
+
None,
|
|
60
|
+
description="Optional list of memory IDs to delete directly.",
|
|
61
|
+
)
|
|
62
|
+
delete_all: bool | None = Field(
|
|
63
|
+
None,
|
|
64
|
+
description="When True and no query/IDs are provided, delete all memories for the user scope.",
|
|
65
|
+
)
|
|
66
|
+
top_k: int | None = Field(
|
|
67
|
+
None,
|
|
68
|
+
description="Optional maximum number of memories to delete by query.",
|
|
69
|
+
)
|
|
70
|
+
threshold: float | None = Field(
|
|
71
|
+
None,
|
|
72
|
+
description="Optional semantic threshold for delete_by_user_query.",
|
|
73
|
+
)
|
|
74
|
+
categories: list[str] | None = Field(
|
|
75
|
+
None,
|
|
76
|
+
description="Optional categories to filter by (uses 'in' operator).",
|
|
77
|
+
)
|
|
78
|
+
metadata: dict[str, Any] | None = Field(
|
|
79
|
+
None,
|
|
80
|
+
description="Optional metadata dict to filter by (exact key-value match).",
|
|
81
|
+
)
|