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.
- aip_agents/__init__.py +65 -0
- aip_agents/a2a/__init__.py +19 -0
- aip_agents/a2a/server/__init__.py +10 -0
- aip_agents/a2a/server/base_executor.py +1086 -0
- aip_agents/a2a/server/google_adk_executor.py +198 -0
- aip_agents/a2a/server/langflow_executor.py +180 -0
- aip_agents/a2a/server/langgraph_executor.py +270 -0
- aip_agents/a2a/types.py +232 -0
- aip_agents/agent/__init__.py +27 -0
- aip_agents/agent/base_agent.py +970 -0
- aip_agents/agent/base_langgraph_agent.py +2942 -0
- aip_agents/agent/google_adk_agent.py +926 -0
- aip_agents/agent/google_adk_constants.py +6 -0
- aip_agents/agent/hitl/__init__.py +24 -0
- aip_agents/agent/hitl/config.py +28 -0
- aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
- aip_agents/agent/hitl/manager.py +532 -0
- aip_agents/agent/hitl/models.py +18 -0
- aip_agents/agent/hitl/prompt/__init__.py +9 -0
- aip_agents/agent/hitl/prompt/base.py +42 -0
- aip_agents/agent/hitl/prompt/deferred.py +73 -0
- aip_agents/agent/hitl/registry.py +149 -0
- aip_agents/agent/interface.py +138 -0
- aip_agents/agent/interfaces.py +65 -0
- aip_agents/agent/langflow_agent.py +464 -0
- aip_agents/agent/langgraph_memory_enhancer_agent.py +433 -0
- aip_agents/agent/langgraph_react_agent.py +2514 -0
- aip_agents/agent/system_instruction_context.py +34 -0
- aip_agents/clients/__init__.py +10 -0
- aip_agents/clients/langflow/__init__.py +10 -0
- aip_agents/clients/langflow/client.py +477 -0
- aip_agents/clients/langflow/types.py +18 -0
- aip_agents/constants.py +23 -0
- aip_agents/credentials/manager.py +132 -0
- aip_agents/examples/__init__.py +5 -0
- aip_agents/examples/compare_streaming_client.py +783 -0
- aip_agents/examples/compare_streaming_server.py +142 -0
- aip_agents/examples/demo_memory_recall.py +401 -0
- aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_google_adk_server.py +79 -0
- aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_server.py +105 -0
- aip_agents/examples/hello_world_a2a_langchain_server.py +79 -0
- aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +78 -0
- aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
- aip_agents/examples/hello_world_a2a_langflow_server.py +82 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +92 -0
- aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
- aip_agents/examples/hello_world_a2a_langgraph_server.py +84 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +79 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +132 -0
- aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +251 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +80 -0
- aip_agents/examples/hello_world_google_adk.py +41 -0
- aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
- aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_stream.py +44 -0
- aip_agents/examples/hello_world_langchain.py +28 -0
- aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
- aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
- aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
- aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
- aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
- aip_agents/examples/hello_world_langchain_stream.py +36 -0
- aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
- aip_agents/examples/hello_world_langflow_agent.py +163 -0
- aip_agents/examples/hello_world_langgraph.py +39 -0
- aip_agents/examples/hello_world_langgraph_bosa_twitter.py +41 -0
- aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
- aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_stream.py +43 -0
- aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_model_switch_cli.py +210 -0
- aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
- aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
- aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
- aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
- aip_agents/examples/hello_world_pii_logger.py +21 -0
- aip_agents/examples/hello_world_sentry.py +133 -0
- aip_agents/examples/hello_world_step_limits.py +273 -0
- aip_agents/examples/hello_world_stock_a2a_server.py +103 -0
- aip_agents/examples/hello_world_tool_output_client.py +46 -0
- aip_agents/examples/hello_world_tool_output_server.py +114 -0
- aip_agents/examples/hitl_demo.py +724 -0
- aip_agents/examples/mcp_configs/configs.py +63 -0
- aip_agents/examples/mcp_servers/common.py +76 -0
- aip_agents/examples/mcp_servers/mcp_name.py +29 -0
- aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
- aip_agents/examples/mcp_servers/mcp_time.py +10 -0
- aip_agents/examples/pii_demo_langgraph_client.py +69 -0
- aip_agents/examples/pii_demo_langgraph_server.py +126 -0
- aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
- aip_agents/examples/pii_demo_multi_agent_server.py +247 -0
- aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
- aip_agents/examples/todolist_planning_a2a_langgraph_server.py +88 -0
- aip_agents/examples/tools/__init__.py +27 -0
- aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
- aip_agents/examples/tools/adk_weather_tool.py +60 -0
- aip_agents/examples/tools/data_generator_tool.py +103 -0
- aip_agents/examples/tools/data_visualization_tool.py +312 -0
- aip_agents/examples/tools/image_artifact_tool.py +136 -0
- aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
- aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
- aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
- aip_agents/examples/tools/langchain_weather_tool.py +48 -0
- aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
- aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
- aip_agents/examples/tools/pii_demo_tools.py +189 -0
- aip_agents/examples/tools/random_chart_tool.py +142 -0
- aip_agents/examples/tools/serper_tool.py +202 -0
- aip_agents/examples/tools/stock_tools.py +82 -0
- aip_agents/examples/tools/table_generator_tool.py +167 -0
- aip_agents/examples/tools/time_tool.py +82 -0
- aip_agents/examples/tools/weather_forecast_tool.py +38 -0
- aip_agents/executor/agent_executor.py +473 -0
- aip_agents/executor/base.py +48 -0
- aip_agents/mcp/__init__.py +1 -0
- aip_agents/mcp/client/__init__.py +14 -0
- aip_agents/mcp/client/base_mcp_client.py +369 -0
- aip_agents/mcp/client/connection_manager.py +193 -0
- aip_agents/mcp/client/google_adk/__init__.py +11 -0
- aip_agents/mcp/client/google_adk/client.py +381 -0
- aip_agents/mcp/client/langchain/__init__.py +11 -0
- aip_agents/mcp/client/langchain/client.py +265 -0
- aip_agents/mcp/client/persistent_session.py +359 -0
- aip_agents/mcp/client/session_pool.py +351 -0
- aip_agents/mcp/client/transports.py +215 -0
- aip_agents/mcp/utils/__init__.py +7 -0
- aip_agents/mcp/utils/config_validator.py +139 -0
- aip_agents/memory/__init__.py +14 -0
- aip_agents/memory/adapters/__init__.py +10 -0
- aip_agents/memory/adapters/base_adapter.py +717 -0
- aip_agents/memory/adapters/mem0.py +84 -0
- aip_agents/memory/base.py +84 -0
- aip_agents/memory/constants.py +49 -0
- aip_agents/memory/factory.py +86 -0
- aip_agents/memory/guidance.py +20 -0
- aip_agents/memory/simple_memory.py +47 -0
- aip_agents/middleware/__init__.py +17 -0
- aip_agents/middleware/base.py +88 -0
- aip_agents/middleware/manager.py +128 -0
- aip_agents/middleware/todolist.py +274 -0
- aip_agents/schema/__init__.py +69 -0
- aip_agents/schema/a2a.py +56 -0
- aip_agents/schema/agent.py +111 -0
- aip_agents/schema/hitl.py +157 -0
- aip_agents/schema/langgraph.py +37 -0
- aip_agents/schema/model_id.py +97 -0
- aip_agents/schema/step_limit.py +108 -0
- aip_agents/schema/storage.py +40 -0
- aip_agents/sentry/__init__.py +11 -0
- aip_agents/sentry/sentry.py +151 -0
- aip_agents/storage/__init__.py +41 -0
- aip_agents/storage/base.py +85 -0
- aip_agents/storage/clients/__init__.py +12 -0
- aip_agents/storage/clients/minio_client.py +318 -0
- aip_agents/storage/config.py +62 -0
- aip_agents/storage/providers/__init__.py +15 -0
- aip_agents/storage/providers/base.py +106 -0
- aip_agents/storage/providers/memory.py +114 -0
- aip_agents/storage/providers/object_storage.py +214 -0
- aip_agents/tools/__init__.py +33 -0
- aip_agents/tools/bosa_tools.py +105 -0
- aip_agents/tools/browser_use/__init__.py +82 -0
- aip_agents/tools/browser_use/action_parser.py +103 -0
- aip_agents/tools/browser_use/browser_use_tool.py +1112 -0
- aip_agents/tools/browser_use/llm_config.py +120 -0
- aip_agents/tools/browser_use/minio_storage.py +198 -0
- aip_agents/tools/browser_use/schemas.py +119 -0
- aip_agents/tools/browser_use/session.py +76 -0
- aip_agents/tools/browser_use/session_errors.py +132 -0
- aip_agents/tools/browser_use/steel_session_recording.py +317 -0
- aip_agents/tools/browser_use/streaming.py +813 -0
- aip_agents/tools/browser_use/structured_data_parser.py +257 -0
- aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
- aip_agents/tools/browser_use/types.py +78 -0
- aip_agents/tools/code_sandbox/__init__.py +26 -0
- aip_agents/tools/code_sandbox/constant.py +13 -0
- aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +257 -0
- aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +411 -0
- aip_agents/tools/constants.py +165 -0
- aip_agents/tools/document_loader/__init__.py +44 -0
- aip_agents/tools/document_loader/base_reader.py +302 -0
- aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
- aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
- aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
- aip_agents/tools/document_loader/pdf_splitter.py +169 -0
- aip_agents/tools/gl_connector/__init__.py +5 -0
- aip_agents/tools/gl_connector/tool.py +351 -0
- aip_agents/tools/memory_search/__init__.py +22 -0
- aip_agents/tools/memory_search/base.py +200 -0
- aip_agents/tools/memory_search/mem0.py +258 -0
- aip_agents/tools/memory_search/schema.py +48 -0
- aip_agents/tools/memory_search_tool.py +26 -0
- aip_agents/tools/time_tool.py +117 -0
- aip_agents/tools/tool_config_injector.py +300 -0
- aip_agents/tools/web_search/__init__.py +15 -0
- aip_agents/tools/web_search/serper_tool.py +187 -0
- aip_agents/types/__init__.py +70 -0
- aip_agents/types/a2a_events.py +13 -0
- aip_agents/utils/__init__.py +79 -0
- aip_agents/utils/a2a_connector.py +1757 -0
- aip_agents/utils/artifact_helpers.py +502 -0
- aip_agents/utils/constants.py +22 -0
- aip_agents/utils/datetime/__init__.py +34 -0
- aip_agents/utils/datetime/normalization.py +231 -0
- aip_agents/utils/datetime/timezone.py +206 -0
- aip_agents/utils/env_loader.py +27 -0
- aip_agents/utils/event_handler_registry.py +58 -0
- aip_agents/utils/file_prompt_utils.py +176 -0
- aip_agents/utils/final_response_builder.py +211 -0
- aip_agents/utils/formatter_llm_client.py +231 -0
- aip_agents/utils/langgraph/__init__.py +19 -0
- aip_agents/utils/langgraph/converter.py +128 -0
- aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
- aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
- aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
- aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1071 -0
- aip_agents/utils/langgraph/tool_output_management.py +967 -0
- aip_agents/utils/logger.py +195 -0
- aip_agents/utils/metadata/__init__.py +27 -0
- aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
- aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
- aip_agents/utils/metadata/activity_narrative/builder.py +817 -0
- aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
- aip_agents/utils/metadata/activity_narrative/context.py +49 -0
- aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
- aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
- aip_agents/utils/metadata/schemas/__init__.py +16 -0
- aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
- aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
- aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
- aip_agents/utils/metadata_helper.py +358 -0
- aip_agents/utils/name_preprocessor/__init__.py +17 -0
- aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
- aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
- aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
- aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
- aip_agents/utils/pii/__init__.py +25 -0
- aip_agents/utils/pii/pii_handler.py +397 -0
- aip_agents/utils/pii/pii_helper.py +207 -0
- aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
- aip_agents/utils/reference_helper.py +273 -0
- aip_agents/utils/sse_chunk_transformer.py +831 -0
- aip_agents/utils/step_limit_manager.py +265 -0
- aip_agents/utils/token_usage_helper.py +156 -0
- aip_agents_binary-0.5.20.dist-info/METADATA +681 -0
- aip_agents_binary-0.5.20.dist-info/RECORD +280 -0
- aip_agents_binary-0.5.20.dist-info/WHEEL +5 -0
- 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): \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
|