aip-agents-binary 0.5.25b9__py3-none-any.whl → 0.6.1__py3-none-any.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.

Potentially problematic release.


This version of aip-agents-binary might be problematic. Click here for more details.

Files changed (73) hide show
  1. aip_agents/agent/base_langgraph_agent.py +137 -68
  2. aip_agents/agent/base_langgraph_agent.pyi +3 -2
  3. aip_agents/agent/langgraph_react_agent.py +252 -16
  4. aip_agents/agent/langgraph_react_agent.pyi +40 -1
  5. aip_agents/examples/compare_streaming_client.py +2 -2
  6. aip_agents/examples/compare_streaming_server.py +1 -1
  7. aip_agents/examples/hello_world_ptc.py +51 -0
  8. aip_agents/examples/hello_world_ptc.pyi +5 -0
  9. aip_agents/examples/hello_world_tool_output_client.py +9 -0
  10. aip_agents/examples/todolist_planning_a2a_langchain_client.py +2 -2
  11. aip_agents/examples/todolist_planning_a2a_langgraph_server.py +1 -1
  12. aip_agents/guardrails/engines/base.py +6 -6
  13. aip_agents/mcp/client/connection_manager.py +36 -1
  14. aip_agents/mcp/client/connection_manager.pyi +3 -0
  15. aip_agents/mcp/client/persistent_session.py +318 -68
  16. aip_agents/mcp/client/persistent_session.pyi +9 -0
  17. aip_agents/mcp/client/transports.py +33 -2
  18. aip_agents/mcp/client/transports.pyi +9 -0
  19. aip_agents/ptc/__init__.py +48 -0
  20. aip_agents/ptc/__init__.pyi +10 -0
  21. aip_agents/ptc/doc_gen.py +122 -0
  22. aip_agents/ptc/doc_gen.pyi +40 -0
  23. aip_agents/ptc/exceptions.py +39 -0
  24. aip_agents/ptc/exceptions.pyi +22 -0
  25. aip_agents/ptc/executor.py +143 -0
  26. aip_agents/ptc/executor.pyi +73 -0
  27. aip_agents/ptc/mcp/__init__.py +45 -0
  28. aip_agents/ptc/mcp/__init__.pyi +7 -0
  29. aip_agents/ptc/mcp/sandbox_bridge.py +668 -0
  30. aip_agents/ptc/mcp/sandbox_bridge.pyi +47 -0
  31. aip_agents/ptc/mcp/templates/__init__.py +1 -0
  32. aip_agents/ptc/mcp/templates/__init__.pyi +0 -0
  33. aip_agents/ptc/mcp/templates/mcp_client.py.template +239 -0
  34. aip_agents/ptc/naming.py +184 -0
  35. aip_agents/ptc/naming.pyi +76 -0
  36. aip_agents/ptc/payload.py +26 -0
  37. aip_agents/ptc/payload.pyi +15 -0
  38. aip_agents/ptc/prompt_builder.py +571 -0
  39. aip_agents/ptc/prompt_builder.pyi +55 -0
  40. aip_agents/ptc/ptc_helper.py +16 -0
  41. aip_agents/ptc/ptc_helper.pyi +1 -0
  42. aip_agents/ptc/sandbox_bridge.py +58 -0
  43. aip_agents/ptc/sandbox_bridge.pyi +25 -0
  44. aip_agents/ptc/template_utils.py +33 -0
  45. aip_agents/ptc/template_utils.pyi +13 -0
  46. aip_agents/ptc/templates/__init__.py +1 -0
  47. aip_agents/ptc/templates/__init__.pyi +0 -0
  48. aip_agents/ptc/templates/ptc_helper.py.template +134 -0
  49. aip_agents/sandbox/__init__.py +43 -0
  50. aip_agents/sandbox/__init__.pyi +5 -0
  51. aip_agents/sandbox/defaults.py +9 -0
  52. aip_agents/sandbox/defaults.pyi +2 -0
  53. aip_agents/sandbox/e2b_runtime.py +267 -0
  54. aip_agents/sandbox/e2b_runtime.pyi +51 -0
  55. aip_agents/sandbox/template_builder.py +131 -0
  56. aip_agents/sandbox/template_builder.pyi +36 -0
  57. aip_agents/sandbox/types.py +24 -0
  58. aip_agents/sandbox/types.pyi +14 -0
  59. aip_agents/sandbox/validation.py +50 -0
  60. aip_agents/sandbox/validation.pyi +20 -0
  61. aip_agents/tools/__init__.py +2 -0
  62. aip_agents/tools/__init__.pyi +2 -1
  63. aip_agents/tools/browser_use/browser_use_tool.py +8 -0
  64. aip_agents/tools/browser_use/streaming.py +2 -0
  65. aip_agents/tools/execute_ptc_code.py +305 -0
  66. aip_agents/tools/execute_ptc_code.pyi +87 -0
  67. aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +26 -1
  68. aip_agents/utils/langgraph/tool_output_management.py +80 -0
  69. aip_agents/utils/langgraph/tool_output_management.pyi +37 -0
  70. {aip_agents_binary-0.5.25b9.dist-info → aip_agents_binary-0.6.1.dist-info}/METADATA +51 -48
  71. {aip_agents_binary-0.5.25b9.dist-info → aip_agents_binary-0.6.1.dist-info}/RECORD +73 -27
  72. {aip_agents_binary-0.5.25b9.dist-info → aip_agents_binary-0.6.1.dist-info}/WHEEL +0 -0
  73. {aip_agents_binary-0.5.25b9.dist-info → aip_agents_binary-0.6.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1 @@
1
+ """Templates package for PTC sandbox code generation."""
File without changes
@@ -0,0 +1,239 @@
1
+ """MCP Client for sandbox PTC execution.
2
+
3
+ This module uses the official MCP Python SDK to call MCP tools from within
4
+ the E2B sandbox. It provides sync wrappers around the async SDK.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import os
10
+ from typing import Any
11
+
12
+ import httpx
13
+ from mcp import ClientSession
14
+ from mcp.client.streamable_http import streamable_http_client
15
+ from mcp.client.sse import sse_client
16
+
17
+
18
+ def load_config() -> dict[str, Any]:
19
+ """Load PTC config from ptc_config.json."""
20
+ config_path = os.path.join(os.path.dirname(__file__), "..", "ptc_config.json")
21
+ with open(config_path) as f:
22
+ return json.load(f)
23
+
24
+
25
+ _CONFIG: dict[str, Any] | None = None
26
+
27
+
28
+ def get_config() -> dict[str, Any]:
29
+ """Get cached config."""
30
+ global _CONFIG
31
+ if _CONFIG is None:
32
+ _CONFIG = load_config()
33
+ return _CONFIG
34
+
35
+
36
+ def _is_transient_error(exc: BaseException) -> bool:
37
+ """Return True for retryable network errors."""
38
+ if isinstance(exc, asyncio.CancelledError):
39
+ return False
40
+ # Network-related errors that are retryable
41
+ if isinstance(exc, (httpx.HTTPError, OSError, ConnectionError, TimeoutError)):
42
+ return True
43
+ # MCP SDK wraps network errors in ExceptionGroup from anyio TaskGroups
44
+ if isinstance(exc, BaseExceptionGroup):
45
+ return any(_is_transient_error(sub) for sub in exc.exceptions)
46
+ return False
47
+
48
+
49
+ async def _execute_tool_call(
50
+ url: str,
51
+ transport: str,
52
+ headers: dict[str, str] | None,
53
+ timeout: float,
54
+ tool_name: str,
55
+ arguments: dict[str, Any],
56
+ ) -> Any:
57
+ """Execute a single MCP tool call (no retry)."""
58
+ if transport == "sse":
59
+ async with sse_client(
60
+ url=url,
61
+ timeout=timeout,
62
+ sse_read_timeout=timeout * 2,
63
+ headers=headers,
64
+ ) as (read_stream, write_stream):
65
+ async with ClientSession(read_stream, write_stream) as session:
66
+ await session.initialize()
67
+ result = await session.call_tool(tool_name, arguments)
68
+ return _normalize_result(result)
69
+
70
+ # Default: streamable-http
71
+ async with httpx.AsyncClient(
72
+ timeout=httpx.Timeout(timeout),
73
+ headers=headers,
74
+ ) as http_client:
75
+ async with streamable_http_client(
76
+ url=url,
77
+ http_client=http_client,
78
+ ) as (read_stream, write_stream, _):
79
+ async with ClientSession(read_stream, write_stream) as session:
80
+ await session.initialize()
81
+ result = await session.call_tool(tool_name, arguments)
82
+ return _normalize_result(result)
83
+
84
+
85
+ async def _call_tool_async(
86
+ server_name: str,
87
+ tool_name: str,
88
+ arguments: dict[str, Any],
89
+ ) -> Any:
90
+ """Call an MCP tool asynchronously with retry.
91
+
92
+ Args:
93
+ server_name: Name of the MCP server.
94
+ tool_name: Name of the tool to call.
95
+ arguments: Arguments to pass to the tool.
96
+
97
+ Returns:
98
+ Normalized tool result.
99
+
100
+ Raises:
101
+ RuntimeError: If all retry attempts fail.
102
+ """
103
+ config = get_config()
104
+ server_config = config["servers"].get(server_name)
105
+ if not server_config:
106
+ raise RuntimeError(f"Server '{server_name}' not found in config")
107
+
108
+ allowed = server_config.get("allowed_tools")
109
+ if allowed and tool_name not in allowed:
110
+ raise PermissionError(f"Tool '{tool_name}' not allowed on server '{server_name}'")
111
+
112
+ url = server_config["url"]
113
+ headers = server_config.get("headers") or None
114
+ timeout = float(server_config.get("timeout", 60.0))
115
+ transport = server_config.get("transport", "streamable-http").lower().replace("_", "-")
116
+ max_retries = 3
117
+
118
+ last_error: Exception | None = None
119
+ backoff = 0.5
120
+
121
+ for attempt in range(max_retries):
122
+ try:
123
+ return await _execute_tool_call(url, transport, headers, timeout, tool_name, arguments)
124
+ except Exception as e:
125
+ last_error = e
126
+ if attempt < max_retries - 1 and _is_transient_error(e):
127
+ await asyncio.sleep(backoff)
128
+ backoff *= 2
129
+ continue
130
+ break
131
+
132
+ raise RuntimeError(f"MCP tool call failed: {last_error}") from last_error
133
+
134
+
135
+ def _run_async(coro):
136
+ """Run an async coroutine, handling existing event loops (e.g., in Jupyter/E2B).
137
+
138
+ Args:
139
+ coro: The coroutine to run.
140
+
141
+ Returns:
142
+ The result of the coroutine.
143
+ """
144
+ try:
145
+ loop = asyncio.get_running_loop()
146
+ except RuntimeError:
147
+ # No running loop, use asyncio.run()
148
+ return asyncio.run(coro)
149
+
150
+ # There's a running loop - we need to run in a separate thread
151
+ import concurrent.futures
152
+
153
+ def run_in_new_loop():
154
+ """Run coroutine in a new event loop in a thread."""
155
+ new_loop = asyncio.new_event_loop()
156
+ asyncio.set_event_loop(new_loop)
157
+ try:
158
+ result = new_loop.run_until_complete(coro)
159
+ new_loop.run_until_complete(new_loop.shutdown_asyncgens())
160
+ if hasattr(new_loop, "shutdown_default_executor"):
161
+ new_loop.run_until_complete(new_loop.shutdown_default_executor())
162
+ return result
163
+ finally:
164
+ new_loop.close()
165
+
166
+ with concurrent.futures.ThreadPoolExecutor() as executor:
167
+ future = executor.submit(run_in_new_loop)
168
+ return future.result()
169
+
170
+
171
+ def call_tool(server_name: str, tool_name: str, arguments: dict[str, Any]) -> Any:
172
+ """Call an MCP tool (sync wrapper).
173
+
174
+ This is a synchronous wrapper around the async MCP SDK for easier use
175
+ in LLM-generated code. Handles both regular scripts and Jupyter/E2B environments.
176
+
177
+ Args:
178
+ server_name: Name of the MCP server.
179
+ tool_name: Name of the tool to call.
180
+ arguments: Arguments to pass to the tool.
181
+
182
+ Returns:
183
+ Normalized tool result.
184
+
185
+ Raises:
186
+ RuntimeError: If the tool call fails.
187
+ """
188
+ return _run_async(_call_tool_async(server_name, tool_name, arguments))
189
+
190
+
191
+ def _normalize_result(result: Any) -> Any:
192
+ """Normalize MCP tool result.
193
+
194
+ Normalization rules:
195
+ - If result has content array with single text item: return text (JSON-parsed if possible)
196
+ - Otherwise return structured object with text and non_text lists
197
+
198
+ Args:
199
+ result: MCP CallToolResult object.
200
+
201
+ Returns:
202
+ Normalized result value.
203
+ """
204
+ # Handle MCP SDK result object
205
+ content = getattr(result, "content", [])
206
+ if not content:
207
+ return result
208
+
209
+ text_contents: list[str] = []
210
+ non_text_contents: list[Any] = []
211
+
212
+ for item in content:
213
+ item_type = getattr(item, "type", None)
214
+ if item_type == "text":
215
+ text_contents.append(getattr(item, "text", ""))
216
+ else:
217
+ # Convert non-text content to dict for serialization
218
+ non_text_contents.append({
219
+ "type": item_type,
220
+ **{k: v for k, v in vars(item).items() if k != "type"}
221
+ })
222
+
223
+ # Single text content, no non-text content
224
+ if len(text_contents) == 1 and not non_text_contents:
225
+ text = text_contents[0]
226
+ # Try to parse as JSON
227
+ stripped = text.strip()
228
+ if stripped.startswith(("{", "[")):
229
+ try:
230
+ return json.loads(text)
231
+ except json.JSONDecodeError:
232
+ pass
233
+ return text
234
+
235
+ # Return structured object for complex results
236
+ return {
237
+ "text": text_contents,
238
+ "non_text": non_text_contents,
239
+ }
@@ -0,0 +1,184 @@
1
+ """Naming utilities for PTC module.
2
+
3
+ Shared naming helpers for sanitizing module, function, and parameter names
4
+ used in sandbox code generation and prompt building.
5
+
6
+ Authors:
7
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
8
+ """
9
+
10
+ import keyword
11
+ import re
12
+ from typing import Any
13
+
14
+ # Reserved module names that cannot be used for server packages.
15
+ RESERVED_MODULE_NAMES = frozenset({"ptc_helper", "mcp_client"})
16
+
17
+ # Default placeholder for example values.
18
+ DEFAULT_EXAMPLE_PLACEHOLDER = '"example"'
19
+
20
+
21
+ def sanitize_module_name(name: str) -> str:
22
+ """Sanitize server name for use as Python module name.
23
+
24
+ Args:
25
+ name: Original server name.
26
+
27
+ Returns:
28
+ Valid Python module name (lowercase, underscored).
29
+ """
30
+ # Replace non-alphanumeric chars with underscore
31
+ sanitized = re.sub(r"[^a-zA-Z0-9]", "_", name)
32
+ # Remove leading digits
33
+ sanitized = re.sub(r"^\d+", "", sanitized)
34
+ # Remove consecutive underscores
35
+ sanitized = re.sub(r"_+", "_", sanitized)
36
+ # Strip leading/trailing underscores
37
+ sanitized = sanitized.strip("_")
38
+ # Ensure not empty
39
+ if not sanitized:
40
+ sanitized = "server"
41
+ sanitized = sanitized.lower()
42
+ # Avoid Python keywords
43
+ if keyword.iskeyword(sanitized):
44
+ sanitized = f"{sanitized}_"
45
+ return sanitized
46
+
47
+
48
+ def sanitize_module_name_with_reserved(name: str) -> str:
49
+ """Sanitize server name, avoiding reserved module names.
50
+
51
+ If the sanitized name collides with a reserved name (e.g., ptc_helper),
52
+ appends '_mcp' suffix to avoid collision.
53
+
54
+ Args:
55
+ name: Original server name.
56
+
57
+ Returns:
58
+ Valid Python module name that does not collide with reserved names.
59
+ """
60
+ sanitized = sanitize_module_name(name)
61
+ if sanitized in RESERVED_MODULE_NAMES:
62
+ return f"{sanitized}_mcp"
63
+ return sanitized
64
+
65
+
66
+ def sanitize_function_name(name: str) -> str:
67
+ """Sanitize tool name for use as Python function name.
68
+
69
+ Args:
70
+ name: Original tool name.
71
+
72
+ Returns:
73
+ Valid Python function name (lowercase, underscored).
74
+ """
75
+ return sanitize_module_name(name)
76
+
77
+
78
+ def sanitize_param_name(name: str) -> str:
79
+ """Sanitize parameter name for use as Python parameter.
80
+
81
+ Args:
82
+ name: Original parameter name (e.g., userId, user-id).
83
+
84
+ Returns:
85
+ Valid Python parameter name (lowercase, underscored).
86
+ """
87
+ return sanitize_module_name(name)
88
+
89
+
90
+ def json_type_to_python(json_type: str | list) -> str:
91
+ """Convert JSON schema type to Python type hint.
92
+
93
+ Args:
94
+ json_type: JSON schema type.
95
+
96
+ Returns:
97
+ Python type hint string.
98
+ """
99
+ if isinstance(json_type, list):
100
+ return "Any"
101
+
102
+ type_map = {
103
+ "string": "str",
104
+ "integer": "int",
105
+ "number": "float",
106
+ "boolean": "bool",
107
+ "array": "list",
108
+ "object": "dict",
109
+ "null": "None",
110
+ }
111
+ return type_map.get(json_type, "Any")
112
+
113
+
114
+ def schema_to_params(schema: dict[str, Any]) -> str:
115
+ """Convert JSON schema to Python function parameters.
116
+
117
+ Args:
118
+ schema: JSON schema for tool input.
119
+
120
+ Returns:
121
+ Function parameter string.
122
+ """
123
+ properties = schema.get("properties", {})
124
+ required = set(schema.get("required", []))
125
+
126
+ if not properties:
127
+ return "**kwargs: Any"
128
+
129
+ params: list[str] = []
130
+ optional_params: list[str] = []
131
+
132
+ for prop_name, prop_schema in properties.items():
133
+ safe_name = sanitize_param_name(prop_name)
134
+ type_hint = json_type_to_python(prop_schema.get("type", "any"))
135
+
136
+ if prop_name in required:
137
+ params.append(f"{safe_name}: {type_hint}")
138
+ else:
139
+ optional_params.append(f"{safe_name}: {type_hint} | None = None")
140
+
141
+ # Required params first, then optional
142
+ all_params = params + optional_params
143
+ return ", ".join(all_params) if all_params else "**kwargs: Any"
144
+
145
+
146
+ def example_value_from_schema(
147
+ prop_schema: dict[str, Any],
148
+ default_placeholder: str = DEFAULT_EXAMPLE_PLACEHOLDER,
149
+ ) -> str:
150
+ """Return an example value for a schema property.
151
+
152
+ Prefers schema-provided examples, defaults, or enums, and falls back
153
+ to type-based placeholders.
154
+
155
+ Args:
156
+ prop_schema: Property schema from JSON schema.
157
+ default_placeholder: Placeholder value to use for unknown types.
158
+
159
+ Returns:
160
+ Example value as a Python literal string.
161
+ """
162
+ if prop_schema.get("examples"):
163
+ return repr(prop_schema["examples"][0])
164
+ if "default" in prop_schema:
165
+ return repr(prop_schema["default"])
166
+ if prop_schema.get("enum"):
167
+ return repr(prop_schema["enum"][0])
168
+
169
+ prop_type = prop_schema.get("type", "string")
170
+ type_placeholders = {
171
+ "integer": "1",
172
+ "number": "1.0",
173
+ "boolean": "True",
174
+ "array": "[]",
175
+ "object": "{}",
176
+ }
177
+
178
+ if prop_type in type_placeholders:
179
+ return type_placeholders[prop_type]
180
+
181
+ if prop_type == "string":
182
+ return default_placeholder
183
+
184
+ return default_placeholder
@@ -0,0 +1,76 @@
1
+ from _typeshed import Incomplete
2
+ from typing import Any
3
+
4
+ RESERVED_MODULE_NAMES: Incomplete
5
+ DEFAULT_EXAMPLE_PLACEHOLDER: str
6
+
7
+ def sanitize_module_name(name: str) -> str:
8
+ """Sanitize server name for use as Python module name.
9
+
10
+ Args:
11
+ name: Original server name.
12
+
13
+ Returns:
14
+ Valid Python module name (lowercase, underscored).
15
+ """
16
+ def sanitize_module_name_with_reserved(name: str) -> str:
17
+ """Sanitize server name, avoiding reserved module names.
18
+
19
+ If the sanitized name collides with a reserved name (e.g., ptc_helper),
20
+ appends '_mcp' suffix to avoid collision.
21
+
22
+ Args:
23
+ name: Original server name.
24
+
25
+ Returns:
26
+ Valid Python module name that does not collide with reserved names.
27
+ """
28
+ def sanitize_function_name(name: str) -> str:
29
+ """Sanitize tool name for use as Python function name.
30
+
31
+ Args:
32
+ name: Original tool name.
33
+
34
+ Returns:
35
+ Valid Python function name (lowercase, underscored).
36
+ """
37
+ def sanitize_param_name(name: str) -> str:
38
+ """Sanitize parameter name for use as Python parameter.
39
+
40
+ Args:
41
+ name: Original parameter name (e.g., userId, user-id).
42
+
43
+ Returns:
44
+ Valid Python parameter name (lowercase, underscored).
45
+ """
46
+ def json_type_to_python(json_type: str | list) -> str:
47
+ """Convert JSON schema type to Python type hint.
48
+
49
+ Args:
50
+ json_type: JSON schema type.
51
+
52
+ Returns:
53
+ Python type hint string.
54
+ """
55
+ def schema_to_params(schema: dict[str, Any]) -> str:
56
+ """Convert JSON schema to Python function parameters.
57
+
58
+ Args:
59
+ schema: JSON schema for tool input.
60
+
61
+ Returns:
62
+ Function parameter string.
63
+ """
64
+ def example_value_from_schema(prop_schema: dict[str, Any], default_placeholder: str = ...) -> str:
65
+ """Return an example value for a schema property.
66
+
67
+ Prefers schema-provided examples, defaults, or enums, and falls back
68
+ to type-based placeholders.
69
+
70
+ Args:
71
+ prop_schema: Property schema from JSON schema.
72
+ default_placeholder: Placeholder value to use for unknown types.
73
+
74
+ Returns:
75
+ Example value as a Python literal string.
76
+ """
@@ -0,0 +1,26 @@
1
+ """Shared payload types for PTC.
2
+
3
+ This module defines the payload structure used across PTC implementations
4
+ (MCP, custom tools, etc.).
5
+
6
+ Authors:
7
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
8
+ """
9
+
10
+ from dataclasses import dataclass, field
11
+
12
+
13
+ @dataclass
14
+ class SandboxPayload:
15
+ """Payload to upload to the sandbox for PTC execution.
16
+
17
+ Attributes:
18
+ files: Mapping of file path -> content to upload (static bundle).
19
+ per_run_files: Mapping of file path -> content to upload on every run.
20
+ These files are always re-uploaded, even when the static bundle is cached.
21
+ env: Environment variables to set in the sandbox.
22
+ """
23
+
24
+ files: dict[str, str] = field(default_factory=dict)
25
+ per_run_files: dict[str, str] = field(default_factory=dict)
26
+ env: dict[str, str] = field(default_factory=dict)
@@ -0,0 +1,15 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ @dataclass
4
+ class SandboxPayload:
5
+ """Payload to upload to the sandbox for PTC execution.
6
+
7
+ Attributes:
8
+ files: Mapping of file path -> content to upload (static bundle).
9
+ per_run_files: Mapping of file path -> content to upload on every run.
10
+ These files are always re-uploaded, even when the static bundle is cached.
11
+ env: Environment variables to set in the sandbox.
12
+ """
13
+ files: dict[str, str] = field(default_factory=dict)
14
+ per_run_files: dict[str, str] = field(default_factory=dict)
15
+ env: dict[str, str] = field(default_factory=dict)