aip-agents-binary 0.5.20__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.
Files changed (280) hide show
  1. aip_agents/__init__.py +65 -0
  2. aip_agents/a2a/__init__.py +19 -0
  3. aip_agents/a2a/server/__init__.py +10 -0
  4. aip_agents/a2a/server/base_executor.py +1086 -0
  5. aip_agents/a2a/server/google_adk_executor.py +198 -0
  6. aip_agents/a2a/server/langflow_executor.py +180 -0
  7. aip_agents/a2a/server/langgraph_executor.py +270 -0
  8. aip_agents/a2a/types.py +232 -0
  9. aip_agents/agent/__init__.py +27 -0
  10. aip_agents/agent/base_agent.py +970 -0
  11. aip_agents/agent/base_langgraph_agent.py +2942 -0
  12. aip_agents/agent/google_adk_agent.py +926 -0
  13. aip_agents/agent/google_adk_constants.py +6 -0
  14. aip_agents/agent/hitl/__init__.py +24 -0
  15. aip_agents/agent/hitl/config.py +28 -0
  16. aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
  17. aip_agents/agent/hitl/manager.py +532 -0
  18. aip_agents/agent/hitl/models.py +18 -0
  19. aip_agents/agent/hitl/prompt/__init__.py +9 -0
  20. aip_agents/agent/hitl/prompt/base.py +42 -0
  21. aip_agents/agent/hitl/prompt/deferred.py +73 -0
  22. aip_agents/agent/hitl/registry.py +149 -0
  23. aip_agents/agent/interface.py +138 -0
  24. aip_agents/agent/interfaces.py +65 -0
  25. aip_agents/agent/langflow_agent.py +464 -0
  26. aip_agents/agent/langgraph_memory_enhancer_agent.py +433 -0
  27. aip_agents/agent/langgraph_react_agent.py +2514 -0
  28. aip_agents/agent/system_instruction_context.py +34 -0
  29. aip_agents/clients/__init__.py +10 -0
  30. aip_agents/clients/langflow/__init__.py +10 -0
  31. aip_agents/clients/langflow/client.py +477 -0
  32. aip_agents/clients/langflow/types.py +18 -0
  33. aip_agents/constants.py +23 -0
  34. aip_agents/credentials/manager.py +132 -0
  35. aip_agents/examples/__init__.py +5 -0
  36. aip_agents/examples/compare_streaming_client.py +783 -0
  37. aip_agents/examples/compare_streaming_server.py +142 -0
  38. aip_agents/examples/demo_memory_recall.py +401 -0
  39. aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
  40. aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
  41. aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
  42. aip_agents/examples/hello_world_a2a_google_adk_server.py +79 -0
  43. aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
  44. aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
  45. aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
  46. aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
  47. aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
  48. aip_agents/examples/hello_world_a2a_langchain_reference_server.py +105 -0
  49. aip_agents/examples/hello_world_a2a_langchain_server.py +79 -0
  50. aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +78 -0
  51. aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
  52. aip_agents/examples/hello_world_a2a_langflow_server.py +82 -0
  53. aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
  54. aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
  55. aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +92 -0
  56. aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
  57. aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
  58. aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
  59. aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
  60. aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
  61. aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
  62. aip_agents/examples/hello_world_a2a_langgraph_server.py +84 -0
  63. aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +79 -0
  64. aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +132 -0
  65. aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
  66. aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
  67. aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +251 -0
  68. aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
  69. aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +80 -0
  70. aip_agents/examples/hello_world_google_adk.py +41 -0
  71. aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
  72. aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
  73. aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
  74. aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
  75. aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
  76. aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
  77. aip_agents/examples/hello_world_google_adk_stream.py +44 -0
  78. aip_agents/examples/hello_world_langchain.py +28 -0
  79. aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
  80. aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
  81. aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
  82. aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
  83. aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
  84. aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
  85. aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
  86. aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
  87. aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
  88. aip_agents/examples/hello_world_langchain_stream.py +36 -0
  89. aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
  90. aip_agents/examples/hello_world_langflow_agent.py +163 -0
  91. aip_agents/examples/hello_world_langgraph.py +39 -0
  92. aip_agents/examples/hello_world_langgraph_bosa_twitter.py +41 -0
  93. aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
  94. aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
  95. aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
  96. aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
  97. aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
  98. aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
  99. aip_agents/examples/hello_world_langgraph_stream.py +43 -0
  100. aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
  101. aip_agents/examples/hello_world_model_switch_cli.py +210 -0
  102. aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
  103. aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
  104. aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
  105. aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
  106. aip_agents/examples/hello_world_pii_logger.py +21 -0
  107. aip_agents/examples/hello_world_sentry.py +133 -0
  108. aip_agents/examples/hello_world_step_limits.py +273 -0
  109. aip_agents/examples/hello_world_stock_a2a_server.py +103 -0
  110. aip_agents/examples/hello_world_tool_output_client.py +46 -0
  111. aip_agents/examples/hello_world_tool_output_server.py +114 -0
  112. aip_agents/examples/hitl_demo.py +724 -0
  113. aip_agents/examples/mcp_configs/configs.py +63 -0
  114. aip_agents/examples/mcp_servers/common.py +76 -0
  115. aip_agents/examples/mcp_servers/mcp_name.py +29 -0
  116. aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
  117. aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
  118. aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
  119. aip_agents/examples/mcp_servers/mcp_time.py +10 -0
  120. aip_agents/examples/pii_demo_langgraph_client.py +69 -0
  121. aip_agents/examples/pii_demo_langgraph_server.py +126 -0
  122. aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
  123. aip_agents/examples/pii_demo_multi_agent_server.py +247 -0
  124. aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
  125. aip_agents/examples/todolist_planning_a2a_langgraph_server.py +88 -0
  126. aip_agents/examples/tools/__init__.py +27 -0
  127. aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
  128. aip_agents/examples/tools/adk_weather_tool.py +60 -0
  129. aip_agents/examples/tools/data_generator_tool.py +103 -0
  130. aip_agents/examples/tools/data_visualization_tool.py +312 -0
  131. aip_agents/examples/tools/image_artifact_tool.py +136 -0
  132. aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
  133. aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
  134. aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
  135. aip_agents/examples/tools/langchain_weather_tool.py +48 -0
  136. aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
  137. aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
  138. aip_agents/examples/tools/pii_demo_tools.py +189 -0
  139. aip_agents/examples/tools/random_chart_tool.py +142 -0
  140. aip_agents/examples/tools/serper_tool.py +202 -0
  141. aip_agents/examples/tools/stock_tools.py +82 -0
  142. aip_agents/examples/tools/table_generator_tool.py +167 -0
  143. aip_agents/examples/tools/time_tool.py +82 -0
  144. aip_agents/examples/tools/weather_forecast_tool.py +38 -0
  145. aip_agents/executor/agent_executor.py +473 -0
  146. aip_agents/executor/base.py +48 -0
  147. aip_agents/mcp/__init__.py +1 -0
  148. aip_agents/mcp/client/__init__.py +14 -0
  149. aip_agents/mcp/client/base_mcp_client.py +369 -0
  150. aip_agents/mcp/client/connection_manager.py +193 -0
  151. aip_agents/mcp/client/google_adk/__init__.py +11 -0
  152. aip_agents/mcp/client/google_adk/client.py +381 -0
  153. aip_agents/mcp/client/langchain/__init__.py +11 -0
  154. aip_agents/mcp/client/langchain/client.py +265 -0
  155. aip_agents/mcp/client/persistent_session.py +359 -0
  156. aip_agents/mcp/client/session_pool.py +351 -0
  157. aip_agents/mcp/client/transports.py +215 -0
  158. aip_agents/mcp/utils/__init__.py +7 -0
  159. aip_agents/mcp/utils/config_validator.py +139 -0
  160. aip_agents/memory/__init__.py +14 -0
  161. aip_agents/memory/adapters/__init__.py +10 -0
  162. aip_agents/memory/adapters/base_adapter.py +717 -0
  163. aip_agents/memory/adapters/mem0.py +84 -0
  164. aip_agents/memory/base.py +84 -0
  165. aip_agents/memory/constants.py +49 -0
  166. aip_agents/memory/factory.py +86 -0
  167. aip_agents/memory/guidance.py +20 -0
  168. aip_agents/memory/simple_memory.py +47 -0
  169. aip_agents/middleware/__init__.py +17 -0
  170. aip_agents/middleware/base.py +88 -0
  171. aip_agents/middleware/manager.py +128 -0
  172. aip_agents/middleware/todolist.py +274 -0
  173. aip_agents/schema/__init__.py +69 -0
  174. aip_agents/schema/a2a.py +56 -0
  175. aip_agents/schema/agent.py +111 -0
  176. aip_agents/schema/hitl.py +157 -0
  177. aip_agents/schema/langgraph.py +37 -0
  178. aip_agents/schema/model_id.py +97 -0
  179. aip_agents/schema/step_limit.py +108 -0
  180. aip_agents/schema/storage.py +40 -0
  181. aip_agents/sentry/__init__.py +11 -0
  182. aip_agents/sentry/sentry.py +151 -0
  183. aip_agents/storage/__init__.py +41 -0
  184. aip_agents/storage/base.py +85 -0
  185. aip_agents/storage/clients/__init__.py +12 -0
  186. aip_agents/storage/clients/minio_client.py +318 -0
  187. aip_agents/storage/config.py +62 -0
  188. aip_agents/storage/providers/__init__.py +15 -0
  189. aip_agents/storage/providers/base.py +106 -0
  190. aip_agents/storage/providers/memory.py +114 -0
  191. aip_agents/storage/providers/object_storage.py +214 -0
  192. aip_agents/tools/__init__.py +33 -0
  193. aip_agents/tools/bosa_tools.py +105 -0
  194. aip_agents/tools/browser_use/__init__.py +82 -0
  195. aip_agents/tools/browser_use/action_parser.py +103 -0
  196. aip_agents/tools/browser_use/browser_use_tool.py +1112 -0
  197. aip_agents/tools/browser_use/llm_config.py +120 -0
  198. aip_agents/tools/browser_use/minio_storage.py +198 -0
  199. aip_agents/tools/browser_use/schemas.py +119 -0
  200. aip_agents/tools/browser_use/session.py +76 -0
  201. aip_agents/tools/browser_use/session_errors.py +132 -0
  202. aip_agents/tools/browser_use/steel_session_recording.py +317 -0
  203. aip_agents/tools/browser_use/streaming.py +813 -0
  204. aip_agents/tools/browser_use/structured_data_parser.py +257 -0
  205. aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
  206. aip_agents/tools/browser_use/types.py +78 -0
  207. aip_agents/tools/code_sandbox/__init__.py +26 -0
  208. aip_agents/tools/code_sandbox/constant.py +13 -0
  209. aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +257 -0
  210. aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +411 -0
  211. aip_agents/tools/constants.py +165 -0
  212. aip_agents/tools/document_loader/__init__.py +44 -0
  213. aip_agents/tools/document_loader/base_reader.py +302 -0
  214. aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
  215. aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
  216. aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
  217. aip_agents/tools/document_loader/pdf_splitter.py +169 -0
  218. aip_agents/tools/gl_connector/__init__.py +5 -0
  219. aip_agents/tools/gl_connector/tool.py +351 -0
  220. aip_agents/tools/memory_search/__init__.py +22 -0
  221. aip_agents/tools/memory_search/base.py +200 -0
  222. aip_agents/tools/memory_search/mem0.py +258 -0
  223. aip_agents/tools/memory_search/schema.py +48 -0
  224. aip_agents/tools/memory_search_tool.py +26 -0
  225. aip_agents/tools/time_tool.py +117 -0
  226. aip_agents/tools/tool_config_injector.py +300 -0
  227. aip_agents/tools/web_search/__init__.py +15 -0
  228. aip_agents/tools/web_search/serper_tool.py +187 -0
  229. aip_agents/types/__init__.py +70 -0
  230. aip_agents/types/a2a_events.py +13 -0
  231. aip_agents/utils/__init__.py +79 -0
  232. aip_agents/utils/a2a_connector.py +1757 -0
  233. aip_agents/utils/artifact_helpers.py +502 -0
  234. aip_agents/utils/constants.py +22 -0
  235. aip_agents/utils/datetime/__init__.py +34 -0
  236. aip_agents/utils/datetime/normalization.py +231 -0
  237. aip_agents/utils/datetime/timezone.py +206 -0
  238. aip_agents/utils/env_loader.py +27 -0
  239. aip_agents/utils/event_handler_registry.py +58 -0
  240. aip_agents/utils/file_prompt_utils.py +176 -0
  241. aip_agents/utils/final_response_builder.py +211 -0
  242. aip_agents/utils/formatter_llm_client.py +231 -0
  243. aip_agents/utils/langgraph/__init__.py +19 -0
  244. aip_agents/utils/langgraph/converter.py +128 -0
  245. aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
  246. aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
  247. aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
  248. aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1071 -0
  249. aip_agents/utils/langgraph/tool_output_management.py +967 -0
  250. aip_agents/utils/logger.py +195 -0
  251. aip_agents/utils/metadata/__init__.py +27 -0
  252. aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
  253. aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
  254. aip_agents/utils/metadata/activity_narrative/builder.py +817 -0
  255. aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
  256. aip_agents/utils/metadata/activity_narrative/context.py +49 -0
  257. aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
  258. aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
  259. aip_agents/utils/metadata/schemas/__init__.py +16 -0
  260. aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
  261. aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
  262. aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
  263. aip_agents/utils/metadata_helper.py +358 -0
  264. aip_agents/utils/name_preprocessor/__init__.py +17 -0
  265. aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
  266. aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
  267. aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
  268. aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
  269. aip_agents/utils/pii/__init__.py +25 -0
  270. aip_agents/utils/pii/pii_handler.py +397 -0
  271. aip_agents/utils/pii/pii_helper.py +207 -0
  272. aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
  273. aip_agents/utils/reference_helper.py +273 -0
  274. aip_agents/utils/sse_chunk_transformer.py +831 -0
  275. aip_agents/utils/step_limit_manager.py +265 -0
  276. aip_agents/utils/token_usage_helper.py +156 -0
  277. aip_agents_binary-0.5.20.dist-info/METADATA +681 -0
  278. aip_agents_binary-0.5.20.dist-info/RECORD +280 -0
  279. aip_agents_binary-0.5.20.dist-info/WHEEL +5 -0
  280. aip_agents_binary-0.5.20.dist-info/top_level.txt +1 -0
@@ -0,0 +1,257 @@
1
+ """Tool for E2B Cloud Sandbox code execution.
2
+
3
+ Authors:
4
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
5
+ Komang Elang Surya Prawira (komang.e.s.prawira@gdplabs.id)
6
+ """
7
+
8
+ import asyncio
9
+ import time
10
+ from http import HTTPStatus
11
+ from typing import Any
12
+
13
+ import requests
14
+ from gllm_inference.schema import Attachment
15
+ from gllm_tools.code_interpreter.code_sandbox.e2b_cloud_sandbox import E2BCloudSandbox
16
+ from gllm_tools.code_interpreter.code_sandbox.models import (
17
+ ExecutionResult,
18
+ ExecutionStatus,
19
+ )
20
+ from gllm_tools.code_interpreter.code_sandbox.utils import calculate_duration_ms
21
+
22
+ from aip_agents.tools.code_sandbox.constant import DATA_FILE_PATH
23
+ from aip_agents.utils.logger import get_logger
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ class SandboxFileWatcher:
29
+ """File watcher for monitoring file creation in sandbox environments."""
30
+
31
+ def __init__(self, sandbox: Any):
32
+ """Initialize the file watcher with a sandbox instance.
33
+
34
+ Args:
35
+ sandbox (Any): The sandbox instance to monitor.
36
+ """
37
+ self.sandbox = sandbox
38
+ self._created_files: list[str] = []
39
+ self._watchers_with_dirs: list[tuple[Any, str]] = []
40
+
41
+ def setup_monitoring(self) -> None:
42
+ """Set up filesystem watchers for monitoring file creation.
43
+
44
+ Note: /tmp/output is a sandbox-isolated directory, not a shared system /tmp.
45
+ This directory is scoped to the E2B sandbox instance and is safe for use.
46
+ """
47
+ output_dirs = [
48
+ "/tmp/output", # NOSONAR: python:S5443 - Sandbox-isolated directory, safe for temp outputs
49
+ ]
50
+
51
+ self._watchers_with_dirs = []
52
+
53
+ for output_dir in output_dirs:
54
+ try:
55
+ # Create the directory if it doesn't exist
56
+ # NOSONAR: python:S5443 - Sandbox-isolated directory, safe for use
57
+ self.sandbox.files.make_dir(output_dir)
58
+
59
+ # Watch the directory for new files
60
+ watcher = self.sandbox.files.watch_dir(output_dir, recursive=True)
61
+ self._watchers_with_dirs.append((watcher, output_dir))
62
+
63
+ logger.debug(f"Set up file watcher for directory: {output_dir}")
64
+
65
+ except Exception as e:
66
+ logger.debug(f"Could not set up watcher for {output_dir}: {str(e)}")
67
+ continue
68
+
69
+ def _process_single_event(self, event: Any, output_dir: str) -> None:
70
+ """Process a single filesystem event and add created files to the list.
71
+
72
+ Args:
73
+ event: The filesystem event to process.
74
+ output_dir: The directory being watched.
75
+ """
76
+ if not (hasattr(event, "name") and hasattr(event, "type")):
77
+ return
78
+
79
+ if str(event.type) != "FilesystemEventType.CREATE":
80
+ logger.debug(f"Ignored filesystem event: {event.type} - {event.name}")
81
+ return
82
+
83
+ # Construct full path by combining output_dir with filename
84
+ full_path = f"{output_dir}/{event.name}".replace("//", "/")
85
+ logger.info(f"New file created: {full_path}")
86
+ if full_path not in self._created_files:
87
+ self._created_files.append(full_path)
88
+
89
+ def _process_watcher_events(self, watcher: Any, output_dir: str) -> None:
90
+ """Process all events from a single watcher.
91
+
92
+ Args:
93
+ watcher: The filesystem watcher instance.
94
+ output_dir: The directory being watched.
95
+ """
96
+ try:
97
+ events = watcher.get_new_events()
98
+ for event in events:
99
+ logger.debug(f"Event: {event}")
100
+ self._process_single_event(event, output_dir)
101
+ watcher.stop()
102
+ except Exception as e:
103
+ logger.debug(f"Error processing watcher events: {str(e)}")
104
+
105
+ async def process_events(self) -> None:
106
+ """Process filesystem events from watchers and update created files list."""
107
+ # Poll for file system events (allow time for events to be generated)
108
+ await asyncio.sleep(0.5)
109
+
110
+ for watcher, output_dir in self._watchers_with_dirs:
111
+ self._process_watcher_events(watcher, output_dir)
112
+
113
+ def reset_created_files(self) -> None:
114
+ """Reset the list of created files."""
115
+ self._created_files = []
116
+
117
+ def get_created_files(self) -> list[str]:
118
+ """Get the list of files created during monitoring.
119
+
120
+ Returns:
121
+ list[str]: List of file paths that were created.
122
+ """
123
+ return self._created_files.copy()
124
+
125
+
126
+ class MyE2BCloudSandbox(E2BCloudSandbox):
127
+ """Extended E2B Cloud Sandbox with filesystem monitoring capabilities."""
128
+
129
+ def __init__(self, *args, **kwargs):
130
+ """Initialize the sandbox with monitoring capabilities.
131
+
132
+ Args:
133
+ *args: Positional arguments forwarded to ``E2BCloudSandbox``.
134
+ **kwargs: Keyword arguments forwarded to ``E2BCloudSandbox``.
135
+ """
136
+ super().__init__(*args, **kwargs)
137
+ self.file_watcher: SandboxFileWatcher | None = None
138
+
139
+ async def execute_code(
140
+ self,
141
+ code: str,
142
+ timeout: int = 30,
143
+ files: list[Attachment] | None = None,
144
+ **kwargs: Any,
145
+ ) -> ExecutionResult:
146
+ """Execute code in the E2B Cloud sandbox with filesystem monitoring.
147
+
148
+ This override fixes the Pydantic validation error by ensuring execution.error
149
+ is converted to string. Always enables filesystem monitoring to track
150
+ created files.
151
+
152
+ Args:
153
+ code (str): The code to execute.
154
+ timeout (int, optional): Maximum execution time in seconds. Defaults to 30.
155
+ files (list[Attachment] | None, optional): List of Attachment objects with file details. Defaults to None.
156
+ **kwargs (Any): Additional execution parameters.
157
+
158
+ Returns:
159
+ ExecutionResult: Structured result of the execution.
160
+
161
+ Raises:
162
+ RuntimeError: If sandbox is not initialized.
163
+ """
164
+ if not self.sandbox:
165
+ raise RuntimeError("Sandbox is not initialized")
166
+
167
+ start_time = time.time()
168
+
169
+ try:
170
+ # Initialize filesystem monitoring
171
+ self.file_watcher = SandboxFileWatcher(self.sandbox)
172
+ self.file_watcher.reset_created_files()
173
+ self.file_watcher.setup_monitoring()
174
+
175
+ self._upload_files(files)
176
+ # Pre-populate the variable `df` for direct use in the code
177
+ if files:
178
+ logger.info("Pre-populating the variable `df` with the data from the file.")
179
+ self.sandbox.run_code(f"import pandas as pd; df = pd.read_csv('{DATA_FILE_PATH}')", timeout=timeout)
180
+ execution = self.sandbox.run_code(code, timeout=timeout)
181
+ duration_ms = calculate_duration_ms(start_time)
182
+ status = ExecutionStatus.ERROR if execution.error else ExecutionStatus.SUCCESS
183
+
184
+ # Process filesystem events
185
+ if self.file_watcher:
186
+ await self.file_watcher.process_events()
187
+ created_files_count = len(self.file_watcher.get_created_files())
188
+ logger.info(f"File monitoring detected {created_files_count} newly created files")
189
+
190
+ # Fix: Convert execution.error to string
191
+ return ExecutionResult.create(
192
+ status=status,
193
+ code=code,
194
+ stdout=(execution.logs.stdout[0] if execution.logs and execution.logs.stdout else ""),
195
+ stderr=(execution.logs.stderr[0] if execution.logs and execution.logs.stderr else ""),
196
+ error=(str(execution.error) if execution.error else ""), # Convert to string here
197
+ duration_ms=duration_ms,
198
+ )
199
+ except Exception as e:
200
+ logger.warning(f"Error executing code in {self.language} sandbox: {str(e)}")
201
+ return ExecutionResult.create(
202
+ status=ExecutionStatus.ERROR,
203
+ code=code,
204
+ error=str(e),
205
+ duration_ms=calculate_duration_ms(start_time),
206
+ )
207
+
208
+ def get_created_files(self) -> list[str]:
209
+ """Get the list of files created during the last monitored execution.
210
+
211
+ Returns:
212
+ list[str]: List of file paths that were created.
213
+ """
214
+ if self.file_watcher:
215
+ return self.file_watcher.get_created_files()
216
+ return []
217
+
218
+ def download_file(self, file_path: str) -> bytes | None:
219
+ """Download file content from the sandbox.
220
+
221
+ Uses download_url method to get a direct URL and downloads via HTTP,
222
+ which avoids the binary corruption issue with files.read().
223
+
224
+ Args:
225
+ file_path (str): Path to the file in the sandbox.
226
+
227
+ Returns:
228
+ bytes | None: File content as bytes, or None if download fails.
229
+
230
+ Raises:
231
+ RuntimeError: If sandbox is not initialized.
232
+ """
233
+ if not self.sandbox:
234
+ raise RuntimeError("Sandbox is not initialized")
235
+
236
+ try:
237
+ if hasattr(self.sandbox, "download_url"):
238
+ logger.info(f"Downloading {file_path} via download_url method")
239
+
240
+ # Get the download URL
241
+ url = self.sandbox.download_url(file_path)
242
+ logger.debug(f"Got download URL: {url}")
243
+
244
+ response = requests.get(url, timeout=30)
245
+
246
+ if response.status_code == HTTPStatus.OK:
247
+ content = response.content
248
+ logger.info(f"Successfully downloaded {len(content)} bytes via URL")
249
+ return content
250
+ else:
251
+ logger.warning(f"URL download failed with status {response.status_code}")
252
+
253
+ return None
254
+
255
+ except Exception as e:
256
+ logger.warning(f"Failed to download file {file_path}: {str(e)}")
257
+ return None
@@ -0,0 +1,411 @@
1
+ """Tool for E2B Cloud Sandbox code execution.
2
+
3
+ Authors:
4
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
5
+ Komang Elang Surya Prawira (komang.e.s.prawira@gdplabs.id)
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import os
11
+ from typing import Any
12
+
13
+ import pandas as pd
14
+ from gllm_inference.schema import Attachment
15
+ from gllm_tools.code_interpreter.code_sandbox.e2b_cloud_sandbox import E2BCloudSandbox
16
+ from langchain_core.tools import BaseTool
17
+ from langgraph.types import Command
18
+ from pydantic import BaseModel, Field
19
+
20
+ from aip_agents.a2a.types import get_mime_type_from_filename
21
+ from aip_agents.tools.code_sandbox.constant import DATA_FILE_NAME
22
+ from aip_agents.tools.code_sandbox.e2b_cloud_sandbox_extended import MyE2BCloudSandbox
23
+ from aip_agents.utils.artifact_helpers import (
24
+ ArtifactHandler,
25
+ create_multiple_artifacts_response,
26
+ )
27
+ from aip_agents.utils.logger import get_logger
28
+
29
+ logger = get_logger(__name__)
30
+
31
+
32
+ class E2BCodeSandboxInput(BaseModel):
33
+ """Input schema for the E2BCodeSandboxTool."""
34
+
35
+ code: str = Field(
36
+ ...,
37
+ description=(
38
+ "Python code to execute in the sandbox. "
39
+ "If `data_source` is provided, the data will be automatically pre-loaded into "
40
+ "the sandbox as a Pandas DataFrame. "
41
+ "You can access the data directly via a variable named `df`. "
42
+ "The final result must be printed to stdout."
43
+ ),
44
+ min_length=1,
45
+ )
46
+ data_source: str | list[dict[str, Any]] | None = Field(
47
+ default=None,
48
+ description=(
49
+ "The data source used during code execution. "
50
+ "It can be a tool output reference (using $tool_output) or raw data. "
51
+ "If the code requires a data source, this field should not be left empty. "
52
+ "When an output reference is available, prioritize the output reference over raw data."
53
+ ),
54
+ )
55
+ timeout: int = Field(default=30, description="Maximum execution time in seconds", ge=1, le=300)
56
+ language: str = Field(default="python", description="Programming language for the sandbox")
57
+ additional_packages: list[str] | None = Field(
58
+ default=None,
59
+ description="Additional Python packages to install before execution",
60
+ )
61
+
62
+
63
+ class E2BCodeSandboxTool(BaseTool):
64
+ """Tool to execute Python code in E2B Cloud Sandbox."""
65
+
66
+ name: str = "e2b_sandbox_tool"
67
+ description: str = (
68
+ "Useful for executing Python code in a secure cloud sandbox environment. "
69
+ "Input should include the Python code to execute and optional data source, timeout, and packages. "
70
+ "Returns execution results including stdout, stderr, and any errors. "
71
+ "Automatically downloads all files created during execution as artifacts."
72
+ )
73
+ save_output_history: bool = Field(default=True)
74
+ args_schema: type[BaseModel] = E2BCodeSandboxInput
75
+ api_key: str = Field(
76
+ default_factory=lambda: os.getenv("E2B_API_KEY", ""),
77
+ description="E2B API key for cloud sandbox access",
78
+ )
79
+ default_additional_packages: list[str] = Field(
80
+ default_factory=lambda: [
81
+ pkg.strip() for pkg in os.getenv("E2B_SANDBOX_TOOL_ADDITIONAL_PACKAGES", "").split(",") if pkg.strip()
82
+ ],
83
+ description="Default additional packages from environment variable",
84
+ )
85
+ store_final_output: bool = False
86
+
87
+ def _run(
88
+ self,
89
+ code: str,
90
+ data_source: str | list[dict[str, Any]] | None = None,
91
+ timeout: int = 30,
92
+ language: str = "python",
93
+ additional_packages: list[str] | None = None,
94
+ ) -> str | dict[str, Any]:
95
+ """Execute code in E2B Cloud Sandbox and return the result with artifacts.
96
+
97
+ This method calls the async _arun method to avoid code duplication.
98
+
99
+ Args:
100
+ code (str): The Python code to execute.
101
+ data_source (str | list[dict[str, Any]] | None, optional): Data source to be used during code execution.
102
+ Defaults to None.
103
+ timeout (int, optional): Maximum execution time in seconds. Defaults to 30.
104
+ language (str, optional): Programming language for the sandbox. Defaults to "python".
105
+ additional_packages (list[str] | None, optional): Additional packages to install. Defaults to None.
106
+
107
+ Returns:
108
+ str | dict[str, Any]: The execution result as JSON string, or dict with artifacts if files downloaded.
109
+ """
110
+ try:
111
+ loop = asyncio.new_event_loop()
112
+ asyncio.set_event_loop(loop)
113
+ try:
114
+ return loop.run_until_complete(self._arun(code, data_source, timeout, language, additional_packages))
115
+ finally:
116
+ loop.close()
117
+ except Exception as e:
118
+ logger.warning(f"Error in synchronous execution wrapper: {str(e)}")
119
+ return self._create_error_result(code, str(e))
120
+
121
+ def _create_error_result(self, code: str, error_message: str, duration_ms: int = 0) -> str:
122
+ """Create a standardized error result.
123
+
124
+ Args:
125
+ code (str): The code that was being executed.
126
+ error_message (str): The error message.
127
+ duration_ms (int, optional): Duration in milliseconds. Defaults to 0.
128
+
129
+ Returns:
130
+ str: JSON string of error result.
131
+ """
132
+ error_result = {
133
+ "status": "error",
134
+ "code": code,
135
+ "stdout": "",
136
+ "stderr": "",
137
+ "error": error_message,
138
+ "duration_ms": duration_ms,
139
+ }
140
+ return json.dumps(error_result, indent=2)
141
+
142
+ def _prepare_packages(self, additional_packages: list[str] | None) -> list[str]:
143
+ """Prepare the final list of packages by combining defaults with additional packages.
144
+
145
+ Args:
146
+ additional_packages (list[str] | None): Additional packages to install.
147
+
148
+ Returns:
149
+ list[str]: Deduplicated list of packages.
150
+ """
151
+ all_packages = list(self.default_additional_packages) + (additional_packages or [])
152
+ return list(dict.fromkeys(all_packages))
153
+
154
+ def _convert_execution_result_to_dict(self, result) -> dict[str, Any]:
155
+ """Convert execution result to dictionary for JSON serialization.
156
+
157
+ Args:
158
+ result: The execution result from sandbox.
159
+
160
+ Returns:
161
+ dict[str, Any]: Dictionary representation of the result.
162
+ """
163
+ return {
164
+ "status": (result.status.value if hasattr(result.status, "value") else str(result.status)),
165
+ "code": result.code,
166
+ "stdout": result.stdout,
167
+ "stderr": result.stderr,
168
+ "error": str(result.error) if result.error else "",
169
+ "duration_ms": result.duration_ms,
170
+ }
171
+
172
+ async def _arun(
173
+ self,
174
+ code: str,
175
+ data_source: str | list[dict[str, Any]] | None = None,
176
+ timeout: int = 30,
177
+ language: str = "python",
178
+ additional_packages: list[str] | None = None,
179
+ ) -> str | dict[str, Any]:
180
+ """Async version of code execution in E2B Cloud Sandbox.
181
+
182
+ Args:
183
+ code (str): The Python code to execute.
184
+ data_source (str | list[dict[str, Any]] | None, optional): Data source to be used during code execution.
185
+ Defaults to None.
186
+ timeout (int, optional): Maximum execution time in seconds. Defaults to 30.
187
+ language (str, optional): Programming language for the sandbox. Defaults to "python".
188
+ additional_packages (list[str] | None, optional): Additional packages to install. Defaults to None.
189
+
190
+ Returns:
191
+ str | dict[str, Any]: The execution result as JSON string, or dict with artifacts if files downloaded.
192
+ """
193
+ sandbox = None
194
+ try:
195
+ if isinstance(data_source, str) and data_source.startswith("$tool_output"):
196
+ logger.warning(f"Reference {data_source!r} not resolved! Skipping the code execution.")
197
+ return self._create_error_result(code, f"Reference {data_source!r} not resolved!")
198
+
199
+ files = await self._create_files(data_source)
200
+ sandbox = await self._create_sandbox(language, additional_packages)
201
+ result = await sandbox.execute_code(code, timeout=timeout, files=files)
202
+ return await self._process_execution_result(sandbox, result)
203
+
204
+ except Exception as e:
205
+ logger.error(f"Error executing code in E2B sandbox: {str(e)}")
206
+ return self._create_error_result(code, str(e))
207
+
208
+ finally:
209
+ self._cleanup_sandbox(sandbox)
210
+
211
+ async def _create_files(self, data_source: Command | list[dict[str, Any]] | None) -> list[Attachment] | None:
212
+ """Create files from data source.
213
+
214
+ This method will convert the data source to a CSV file and return it as an Attachment object,
215
+ as required by the E2B Cloud Sandbox library.
216
+
217
+ Args:
218
+ data_source (Command | list[dict[str, Any]] | None): Data source to be used during code execution.
219
+
220
+ Returns:
221
+ list[Attachment] | None: List of Attachment objects.
222
+
223
+ Note:
224
+ 1. Command is the expected data type when this method receives data from the tool output sharing.
225
+ 2. The known use cases so far only require a single file, hence the current implementation only accepts
226
+ a single list of dictionaries and creates one file, even though the Sandbox supports multiple files.
227
+ """
228
+ if isinstance(data_source, Command):
229
+ data_source: list[dict[str, Any]] = data_source.update["result"]
230
+
231
+ if not (isinstance(data_source, list) and data_source and all(isinstance(row, dict) for row in data_source)):
232
+ if not data_source:
233
+ logger.info("No data source provided. Ignoring the data source.")
234
+ else:
235
+ logger.warning(
236
+ "Unsupported `data_source` type. Expected a non-empty list of dictionaries. "
237
+ "Ignoring the data source.",
238
+ )
239
+ return None
240
+
241
+ try:
242
+ logger.info("Creating files from data source to upload to the sandbox...")
243
+ df = pd.DataFrame(data_source)
244
+ csv_string = df.to_csv(index=False)
245
+ csv_bytes = csv_string.encode("utf-8")
246
+ return [Attachment.from_bytes(csv_bytes, filename=DATA_FILE_NAME)]
247
+ except Exception as e:
248
+ logger.warning(f"Error creating files from data source: {str(e)}. Ignoring the data source.")
249
+ return None
250
+
251
+ async def _create_sandbox(self, language: str, additional_packages: list[str] | None) -> MyE2BCloudSandbox:
252
+ """Create and initialize the E2B Cloud Sandbox.
253
+
254
+ Args:
255
+ language (str): Programming language for the sandbox.
256
+ additional_packages (list[str] | None): Additional packages to install.
257
+
258
+ Returns:
259
+ MyE2BCloudSandbox: Initialized sandbox instance.
260
+ """
261
+ unique_packages = self._prepare_packages(additional_packages)
262
+
263
+ return MyE2BCloudSandbox(
264
+ api_key=self.api_key,
265
+ language=language,
266
+ additional_packages=unique_packages,
267
+ )
268
+
269
+ async def _process_execution_result(self, sandbox: MyE2BCloudSandbox, result: Any) -> str | dict[str, Any]:
270
+ """Process the execution result and handle file artifacts.
271
+
272
+ Args:
273
+ sandbox (MyE2BCloudSandbox): The sandbox instance.
274
+ result (Any): The execution result from sandbox.
275
+
276
+ Returns:
277
+ str | dict[str, Any]: Processed result as JSON or dict with artifacts.
278
+ """
279
+ result_dict = self._convert_execution_result_to_dict(result)
280
+ created_files = sandbox.get_created_files()
281
+
282
+ if not created_files:
283
+ result_dict["message"] = "No new files were created during execution"
284
+ return json.dumps(result_dict, indent=2)
285
+
286
+ downloaded_artifacts = self._create_artifacts_from_files(sandbox, created_files)
287
+
288
+ if downloaded_artifacts:
289
+ execution_summary = self._create_execution_summary(result_dict, created_files)
290
+ return create_multiple_artifacts_response(result=execution_summary, artifacts=downloaded_artifacts)
291
+
292
+ logger.warning("Files were detected but could not be downloaded")
293
+ return json.dumps(result_dict, indent=2)
294
+
295
+ def _cleanup_sandbox(self, sandbox: MyE2BCloudSandbox | None) -> None:
296
+ """Clean up sandbox resources.
297
+
298
+ Args:
299
+ sandbox (MyE2BCloudSandbox | None): The sandbox instance to cleanup.
300
+ """
301
+ if sandbox:
302
+ try:
303
+ sandbox.terminate()
304
+ except Exception as e:
305
+ logger.warning(f"Error terminating sandbox: {str(e)}")
306
+
307
+ def _create_artifacts_from_files(self, sandbox: E2BCloudSandbox, file_paths: list[str]) -> list[dict[str, Any]]:
308
+ """Create artifacts from a list of file paths using ArtifactHandler.
309
+
310
+ Args:
311
+ sandbox (E2BCloudSandbox): The active sandbox instance.
312
+ file_paths (list[str]): List of file paths to download.
313
+
314
+ Returns:
315
+ list[dict[str, Any]]: List of artifact dictionaries.
316
+ """
317
+ artifacts = []
318
+
319
+ try:
320
+ for file_path in file_paths:
321
+ logger.debug(f"Processing newly created file: {file_path}")
322
+ artifact_dict = self._download_and_create_artifact(sandbox, file_path)
323
+ if artifact_dict:
324
+ artifacts.append(artifact_dict)
325
+
326
+ except Exception as e:
327
+ logger.error(f"Error creating artifacts from files: {str(e)}")
328
+
329
+ return artifacts
330
+
331
+ def _create_execution_summary(self, result_dict: dict[str, Any], created_files: list[str]) -> str:
332
+ """Create execution summary with file information.
333
+
334
+ Args:
335
+ result_dict (dict[str, Any]): The execution result dictionary.
336
+ created_files (list[str]): List of created file paths.
337
+
338
+ Returns:
339
+ str: Formatted execution summary.
340
+ """
341
+ execution_summary = f"Code executed successfully (status: {result_dict['status']}).\n"
342
+
343
+ if result_dict["stdout"]:
344
+ execution_summary += f"Output: {result_dict['stdout']}\n"
345
+
346
+ execution_summary += f"Downloaded {len(created_files)} file(s) created during execution:\n\n"
347
+
348
+ # Add markdown format for each file, especially images
349
+ for file_path in created_files:
350
+ filename = self._extract_filename(file_path)
351
+ mime_type = get_mime_type_from_filename(filename)
352
+
353
+ if mime_type.startswith("image/"):
354
+ # Use just the filename for both alt text and display name
355
+ execution_summary += f"- **{filename}** (Image): ![{filename}]({filename})\n"
356
+ else:
357
+ execution_summary += f"- **{filename}** ({mime_type}): `{file_path}`\n"
358
+
359
+ return execution_summary
360
+
361
+ def _download_and_create_artifact(self, sandbox: E2BCloudSandbox, file_path: str) -> dict[str, Any] | None:
362
+ """Download a single file and create an artifact using ArtifactHandler.
363
+
364
+ Args:
365
+ sandbox (E2BCloudSandbox): The active sandbox instance.
366
+ file_path (str): Path to the file in the sandbox.
367
+
368
+ Returns:
369
+ dict[str, Any] | None: Artifact dictionary or None if download failed.
370
+ """
371
+ try:
372
+ # Download file content
373
+ file_content = sandbox.download_file(file_path)
374
+ if file_content is None:
375
+ logger.warning(f"Failed to download file: {file_path}")
376
+ return None
377
+
378
+ # Extract filename from path
379
+ filename = self._extract_filename(file_path)
380
+
381
+ # Create artifact using ArtifactHandler
382
+ artifact_response = ArtifactHandler().create_file_artifact(
383
+ result=f"File created during execution: {file_path}",
384
+ artifact_data=file_content,
385
+ artifact_name=filename,
386
+ artifact_description=f"File created during execution: {file_path}",
387
+ enable_deduplication=False,
388
+ )
389
+
390
+ logger.info(f"Successfully downloaded newly created file: {file_path} ({len(file_content)} bytes)")
391
+
392
+ # Return just the artifact part (not the response wrapper)
393
+ return artifact_response["artifact"]
394
+
395
+ except Exception as e:
396
+ logger.warning(f"Failed to download file {file_path}: {str(e)}")
397
+ return None
398
+
399
+ def _extract_filename(self, file_path: str) -> str:
400
+ """Extract filename from file path with fallback.
401
+
402
+ Args:
403
+ file_path (str): The file path.
404
+
405
+ Returns:
406
+ str: The extracted filename.
407
+ """
408
+ filename = os.path.basename(file_path)
409
+ if not filename:
410
+ filename = f"sandbox_file_{hash(file_path) % 10000}"
411
+ return filename