aip-agents-binary 0.5.21__py3-none-macosx_13_0_arm64.whl → 0.6.8__py3-none-macosx_13_0_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. aip_agents/agent/__init__.py +44 -4
  2. aip_agents/agent/base_langgraph_agent.py +169 -74
  3. aip_agents/agent/base_langgraph_agent.pyi +3 -2
  4. aip_agents/agent/langgraph_memory_enhancer_agent.py +368 -34
  5. aip_agents/agent/langgraph_memory_enhancer_agent.pyi +3 -2
  6. aip_agents/agent/langgraph_react_agent.py +424 -35
  7. aip_agents/agent/langgraph_react_agent.pyi +46 -2
  8. aip_agents/examples/{hello_world_langgraph_bosa_twitter.py → hello_world_langgraph_gl_connector_twitter.py} +10 -7
  9. aip_agents/examples/hello_world_langgraph_gl_connector_twitter.pyi +5 -0
  10. aip_agents/examples/hello_world_ptc.py +49 -0
  11. aip_agents/examples/hello_world_ptc.pyi +5 -0
  12. aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
  13. aip_agents/examples/hello_world_ptc_custom_tools.pyi +7 -0
  14. aip_agents/examples/hello_world_sentry.py +2 -2
  15. aip_agents/examples/hello_world_tool_output_client.py +9 -0
  16. aip_agents/examples/tools/multiply_tool.py +43 -0
  17. aip_agents/examples/tools/multiply_tool.pyi +18 -0
  18. aip_agents/guardrails/__init__.py +83 -0
  19. aip_agents/guardrails/__init__.pyi +6 -0
  20. aip_agents/guardrails/engines/__init__.py +69 -0
  21. aip_agents/guardrails/engines/__init__.pyi +4 -0
  22. aip_agents/guardrails/engines/base.py +90 -0
  23. aip_agents/guardrails/engines/base.pyi +61 -0
  24. aip_agents/guardrails/engines/nemo.py +101 -0
  25. aip_agents/guardrails/engines/nemo.pyi +46 -0
  26. aip_agents/guardrails/engines/phrase_matcher.py +113 -0
  27. aip_agents/guardrails/engines/phrase_matcher.pyi +48 -0
  28. aip_agents/guardrails/exceptions.py +39 -0
  29. aip_agents/guardrails/exceptions.pyi +23 -0
  30. aip_agents/guardrails/manager.py +163 -0
  31. aip_agents/guardrails/manager.pyi +42 -0
  32. aip_agents/guardrails/middleware.py +199 -0
  33. aip_agents/guardrails/middleware.pyi +87 -0
  34. aip_agents/guardrails/schemas.py +63 -0
  35. aip_agents/guardrails/schemas.pyi +43 -0
  36. aip_agents/guardrails/utils.py +45 -0
  37. aip_agents/guardrails/utils.pyi +19 -0
  38. aip_agents/mcp/client/__init__.py +38 -2
  39. aip_agents/mcp/client/connection_manager.py +36 -1
  40. aip_agents/mcp/client/connection_manager.pyi +3 -0
  41. aip_agents/mcp/client/persistent_session.py +318 -65
  42. aip_agents/mcp/client/persistent_session.pyi +9 -0
  43. aip_agents/mcp/client/transports.py +52 -4
  44. aip_agents/mcp/client/transports.pyi +9 -0
  45. aip_agents/memory/adapters/base_adapter.py +98 -0
  46. aip_agents/memory/adapters/base_adapter.pyi +25 -0
  47. aip_agents/middleware/base.py +8 -0
  48. aip_agents/middleware/base.pyi +4 -0
  49. aip_agents/middleware/manager.py +22 -0
  50. aip_agents/middleware/manager.pyi +4 -0
  51. aip_agents/ptc/__init__.py +87 -0
  52. aip_agents/ptc/__init__.pyi +14 -0
  53. aip_agents/ptc/custom_tools.py +473 -0
  54. aip_agents/ptc/custom_tools.pyi +184 -0
  55. aip_agents/ptc/custom_tools_payload.py +400 -0
  56. aip_agents/ptc/custom_tools_payload.pyi +31 -0
  57. aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
  58. aip_agents/ptc/custom_tools_templates/__init__.pyi +0 -0
  59. aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +23 -0
  60. aip_agents/ptc/custom_tools_templates/custom_init.py.template +15 -0
  61. aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +60 -0
  62. aip_agents/ptc/custom_tools_templates/custom_registry.py.template +87 -0
  63. aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
  64. aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +19 -0
  65. aip_agents/ptc/doc_gen.py +122 -0
  66. aip_agents/ptc/doc_gen.pyi +40 -0
  67. aip_agents/ptc/exceptions.py +57 -0
  68. aip_agents/ptc/exceptions.pyi +37 -0
  69. aip_agents/ptc/executor.py +261 -0
  70. aip_agents/ptc/executor.pyi +99 -0
  71. aip_agents/ptc/mcp/__init__.py +45 -0
  72. aip_agents/ptc/mcp/__init__.pyi +7 -0
  73. aip_agents/ptc/mcp/sandbox_bridge.py +668 -0
  74. aip_agents/ptc/mcp/sandbox_bridge.pyi +47 -0
  75. aip_agents/ptc/mcp/templates/__init__.py +1 -0
  76. aip_agents/ptc/mcp/templates/__init__.pyi +0 -0
  77. aip_agents/ptc/mcp/templates/mcp_client.py.template +239 -0
  78. aip_agents/ptc/naming.py +196 -0
  79. aip_agents/ptc/naming.pyi +85 -0
  80. aip_agents/ptc/payload.py +26 -0
  81. aip_agents/ptc/payload.pyi +15 -0
  82. aip_agents/ptc/prompt_builder.py +673 -0
  83. aip_agents/ptc/prompt_builder.pyi +59 -0
  84. aip_agents/ptc/ptc_helper.py +16 -0
  85. aip_agents/ptc/ptc_helper.pyi +1 -0
  86. aip_agents/ptc/sandbox_bridge.py +256 -0
  87. aip_agents/ptc/sandbox_bridge.pyi +38 -0
  88. aip_agents/ptc/template_utils.py +33 -0
  89. aip_agents/ptc/template_utils.pyi +13 -0
  90. aip_agents/ptc/templates/__init__.py +1 -0
  91. aip_agents/ptc/templates/__init__.pyi +0 -0
  92. aip_agents/ptc/templates/ptc_helper.py.template +134 -0
  93. aip_agents/ptc/tool_def_helpers.py +101 -0
  94. aip_agents/ptc/tool_def_helpers.pyi +38 -0
  95. aip_agents/ptc/tool_enrichment.py +163 -0
  96. aip_agents/ptc/tool_enrichment.pyi +60 -0
  97. aip_agents/sandbox/__init__.py +43 -0
  98. aip_agents/sandbox/__init__.pyi +5 -0
  99. aip_agents/sandbox/defaults.py +205 -0
  100. aip_agents/sandbox/defaults.pyi +30 -0
  101. aip_agents/sandbox/e2b_runtime.py +295 -0
  102. aip_agents/sandbox/e2b_runtime.pyi +57 -0
  103. aip_agents/sandbox/template_builder.py +131 -0
  104. aip_agents/sandbox/template_builder.pyi +36 -0
  105. aip_agents/sandbox/types.py +24 -0
  106. aip_agents/sandbox/types.pyi +14 -0
  107. aip_agents/sandbox/validation.py +50 -0
  108. aip_agents/sandbox/validation.pyi +20 -0
  109. aip_agents/sentry/__init__.py +1 -1
  110. aip_agents/sentry/sentry.py +33 -12
  111. aip_agents/sentry/sentry.pyi +5 -4
  112. aip_agents/tools/__init__.py +20 -3
  113. aip_agents/tools/__init__.pyi +4 -2
  114. aip_agents/tools/browser_use/browser_use_tool.py +8 -0
  115. aip_agents/tools/browser_use/streaming.py +2 -0
  116. aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +80 -31
  117. aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.pyi +25 -9
  118. aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +6 -6
  119. aip_agents/tools/constants.py +24 -12
  120. aip_agents/tools/constants.pyi +14 -11
  121. aip_agents/tools/date_range_tool.py +554 -0
  122. aip_agents/tools/date_range_tool.pyi +21 -0
  123. aip_agents/tools/execute_ptc_code.py +357 -0
  124. aip_agents/tools/execute_ptc_code.pyi +90 -0
  125. aip_agents/tools/gl_connector/__init__.py +1 -1
  126. aip_agents/tools/gl_connector/tool.py +62 -30
  127. aip_agents/tools/gl_connector/tool.pyi +3 -3
  128. aip_agents/tools/gl_connector_tools.py +119 -0
  129. aip_agents/tools/gl_connector_tools.pyi +39 -0
  130. aip_agents/tools/memory_search/__init__.py +8 -1
  131. aip_agents/tools/memory_search/__init__.pyi +3 -3
  132. aip_agents/tools/memory_search/mem0.py +114 -1
  133. aip_agents/tools/memory_search/mem0.pyi +11 -1
  134. aip_agents/tools/memory_search/schema.py +33 -0
  135. aip_agents/tools/memory_search/schema.pyi +10 -0
  136. aip_agents/tools/memory_search_tool.py +8 -0
  137. aip_agents/tools/memory_search_tool.pyi +2 -2
  138. aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +26 -1
  139. aip_agents/utils/langgraph/tool_output_management.py +80 -0
  140. aip_agents/utils/langgraph/tool_output_management.pyi +37 -0
  141. {aip_agents_binary-0.5.21.dist-info → aip_agents_binary-0.6.8.dist-info}/METADATA +14 -22
  142. {aip_agents_binary-0.5.21.dist-info → aip_agents_binary-0.6.8.dist-info}/RECORD +144 -58
  143. {aip_agents_binary-0.5.21.dist-info → aip_agents_binary-0.6.8.dist-info}/WHEEL +1 -1
  144. aip_agents/examples/demo_memory_recall.py +0 -401
  145. aip_agents/examples/demo_memory_recall.pyi +0 -58
  146. aip_agents/examples/hello_world_langgraph_bosa_twitter.pyi +0 -5
  147. aip_agents/tools/bosa_tools.py +0 -105
  148. aip_agents/tools/bosa_tools.pyi +0 -37
  149. {aip_agents_binary-0.5.21.dist-info → aip_agents_binary-0.6.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,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
+ '''
@@ -1,4 +1,4 @@
1
- """GL Connector tool wrapper exports."""
1
+ """GL Connectors tool wrapper exports."""
2
2
 
3
3
  from aip_agents.tools.gl_connector.tool import GLConnectorTool
4
4
 
@@ -1,7 +1,8 @@
1
- """GL Connector tool wrapper for BOSA connector tools.
1
+ """Wrapper for GL Connectors.
2
2
 
3
3
  Authors:
4
4
  Saul Sayers (saul.sayers@gdplabs.id)
5
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
5
6
 
6
7
  Reference:
7
8
  https://gl-docs.gitbook.io/bosa/gl-connector/gl-connector
@@ -21,11 +22,23 @@ from pydantic import ConfigDict, PrivateAttr
21
22
  from aip_agents.tools.constants import ToolType
22
23
 
23
24
  _REQUIRED_ENV_VARS: tuple[str, ...] = (
24
- "BOSA_BASE_URL",
25
- "BOSA_API_KEY",
26
- "BOSA_USERNAME",
27
- "BOSA_PASSWORD",
25
+ "GL_CONNECTORS_BASE_URL",
26
+ "GL_CONNECTORS_API_KEY",
27
+ "GL_CONNECTORS_USERNAME",
28
+ "GL_CONNECTORS_PASSWORD",
28
29
  )
30
+
31
+ _ENV_VAR_MAPPING: dict[str, tuple[str, ...]] = {
32
+ "GL_CONNECTORS_BASE_URL": (
33
+ "GL_CONNECTORS_BASE_URL",
34
+ "BOSA_BASE_URL",
35
+ "BOSA_API_BASE_URL",
36
+ ),
37
+ "GL_CONNECTORS_API_KEY": ("GL_CONNECTORS_API_KEY", "BOSA_API_KEY"),
38
+ "GL_CONNECTORS_USERNAME": ("GL_CONNECTORS_USERNAME", "BOSA_USERNAME"),
39
+ "GL_CONNECTORS_PASSWORD": ("GL_CONNECTORS_PASSWORD", "BOSA_PASSWORD"),
40
+ "GL_CONNECTORS_IDENTIFIER": ("GL_CONNECTORS_IDENTIFIER", "BOSA_IDENTIFIER"),
41
+ }
29
42
  _TOP_LEVEL_KEYS: tuple[str, ...] = (
30
43
  "token",
31
44
  "identifier",
@@ -145,12 +158,12 @@ def GLConnectorTool(
145
158
  api_key: str | None = None,
146
159
  identifier: str | None = None,
147
160
  ) -> BaseTool:
148
- """Create a single GL Connector tool by exact tool name.
161
+ """Create a single tool from GL Connectors by exact tool name.
149
162
 
150
163
  Args:
151
164
  tool_name: Exact tool name (not module name).
152
- api_key: Optional override for BOSA API key.
153
- identifier: Optional override for BOSA identifier.
165
+ api_key: Optional override for GL Connectors API key.
166
+ identifier: Optional override for GL Connectors identifier.
154
167
 
155
168
  Returns:
156
169
  A single LangChain BaseTool with token injection.
@@ -159,14 +172,17 @@ def GLConnectorTool(
159
172
  raise ValueError("tool_name must be a non-empty string")
160
173
 
161
174
  env_values = _load_env(api_key=api_key, identifier=identifier)
162
- connector = BosaConnector(api_base_url=env_values["BOSA_BASE_URL"], api_key=env_values["BOSA_API_KEY"])
175
+ connector = BosaConnector(
176
+ api_base_url=env_values["GL_CONNECTORS_BASE_URL"],
177
+ api_key=env_values["GL_CONNECTORS_API_KEY"],
178
+ )
163
179
 
164
180
  modules = _get_available_modules(connector)
165
181
  module_name = _resolve_module(tool_name, modules)
166
182
 
167
183
  generator = BOSAConnectorToolGenerator(
168
- api_base_url=env_values["BOSA_BASE_URL"],
169
- api_key=env_values["BOSA_API_KEY"],
184
+ api_base_url=env_values["GL_CONNECTORS_BASE_URL"],
185
+ api_key=env_values["GL_CONNECTORS_API_KEY"],
170
186
  app_name=module_name,
171
187
  )
172
188
  tools = generator.generate_tools(tool_type=ToolType.LANGCHAIN)
@@ -177,16 +193,20 @@ def GLConnectorTool(
177
193
  if len(matching) > 1:
178
194
  raise ValueError(f"Multiple tools named '{tool_name}' found in module '{module_name}'")
179
195
 
180
- token = _create_token(connector, env_values["BOSA_USERNAME"], env_values["BOSA_PASSWORD"])
181
- return _InjectedTool(matching[0], token, env_values.get("BOSA_IDENTIFIER"))
196
+ token = _create_token(
197
+ connector,
198
+ env_values["GL_CONNECTORS_USERNAME"],
199
+ env_values["GL_CONNECTORS_PASSWORD"],
200
+ )
201
+ return _InjectedTool(matching[0], token, env_values.get("GL_CONNECTORS_IDENTIFIER"))
182
202
 
183
203
 
184
204
  def _load_env(*, api_key: str | None, identifier: str | None) -> dict[str, str]:
185
205
  """Load and validate environment configuration for connector access.
186
206
 
187
207
  Args:
188
- api_key: Optional override for BOSA API key.
189
- identifier: Optional override for BOSA identifier.
208
+ api_key: Optional override for GL Connectors API key.
209
+ identifier: Optional override for GL Connectors identifier.
190
210
 
191
211
  Returns:
192
212
  Dictionary containing environment configuration values.
@@ -194,28 +214,40 @@ def _load_env(*, api_key: str | None, identifier: str | None) -> dict[str, str]:
194
214
  Raises:
195
215
  ValueError: If required environment variables are missing.
196
216
  """
197
- env = {key: os.getenv(key) for key in _REQUIRED_ENV_VARS}
217
+ env: dict[str, str | None] = {}
198
218
 
199
- resolved_api_key = api_key or env["BOSA_API_KEY"]
200
- env["BOSA_API_KEY"] = resolved_api_key
219
+ # Load from environment using mapping (prefers GL_CONNECTORS_* over BOSA_*)
220
+ for internal_key, env_vars in _ENV_VAR_MAPPING.items():
221
+ val = None
222
+ for var_name in env_vars:
223
+ val = os.getenv(var_name)
224
+ if val:
225
+ break
226
+ env[internal_key] = val
201
227
 
202
- optional_identifier = identifier or os.getenv("BOSA_IDENTIFIER")
228
+ if api_key:
229
+ env["GL_CONNECTORS_API_KEY"] = api_key
203
230
 
204
- if optional_identifier:
205
- env["BOSA_IDENTIFIER"] = optional_identifier
231
+ if identifier:
232
+ env["GL_CONNECTORS_IDENTIFIER"] = identifier
206
233
 
207
- missing = [key for key, value in env.items() if key in _REQUIRED_ENV_VARS and not value]
234
+ missing = [key for key in _REQUIRED_ENV_VARS if not env.get(key)]
208
235
  if missing:
209
- raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
236
+ # Map back to human-friendly names for the error message
237
+ friendly_missing = []
238
+ for m in missing:
239
+ preferred = _ENV_VAR_MAPPING[m][0]
240
+ friendly_missing.append(preferred)
241
+ raise ValueError(f"Missing required environment variables: {', '.join(friendly_missing)}")
210
242
 
211
- return {key: value for key, value in env.items() if value is not None}
243
+ return {k: v for k, v in env.items() if v is not None}
212
244
 
213
245
 
214
246
  def _get_available_modules(connector: BosaConnector) -> list[str]:
215
247
  """Return available connector modules or raise an actionable error.
216
248
 
217
249
  Args:
218
- connector: BOSA connector instance to query for modules.
250
+ connector: GL Connectors instance to query for modules.
219
251
 
220
252
  Returns:
221
253
  List of available module names.
@@ -260,9 +292,9 @@ def _create_token(connector: BosaConnector, username: str, password: str) -> str
260
292
  """Authenticate the connector user and return a user token.
261
293
 
262
294
  Args:
263
- connector: BOSA connector instance for authentication.
264
- username: BOSA username for authentication.
265
- password: BOSA password for authentication.
295
+ connector: GL Connectors instance for authentication.
296
+ username: GL Connectors username for authentication.
297
+ password: GL Connectors password for authentication.
266
298
 
267
299
  Returns:
268
300
  Authentication token string.
@@ -273,11 +305,11 @@ def _create_token(connector: BosaConnector, username: str, password: str) -> str
273
305
  try:
274
306
  user = connector.authenticate_bosa_user(username, password)
275
307
  except Exception as exc:
276
- raise ValueError("Failed to authenticate BOSA user") from exc
308
+ raise ValueError("Failed to authenticate GL Connectors user") from exc
277
309
 
278
310
  token = getattr(user, "token", None)
279
311
  if not token:
280
- raise ValueError("BOSA user token missing after authentication")
312
+ raise ValueError("GL Connectors user token missing after authentication")
281
313
  return token
282
314
 
283
315
 
@@ -62,12 +62,12 @@ class _InjectedTool(BaseTool):
62
62
  """
63
63
 
64
64
  def GLConnectorTool(tool_name: str, *, api_key: str | None = None, identifier: str | None = None) -> BaseTool:
65
- """Create a single GL Connector tool by exact tool name.
65
+ """Create a single tool from GL Connectors by exact tool name.
66
66
 
67
67
  Args:
68
68
  tool_name: Exact tool name (not module name).
69
- api_key: Optional override for BOSA API key.
70
- identifier: Optional override for BOSA identifier.
69
+ api_key: Optional override for GL Connectors API key.
70
+ identifier: Optional override for GL Connectors identifier.
71
71
 
72
72
  Returns:
73
73
  A single LangChain BaseTool with token injection.