diff --git a/letta/server/server.py b/letta/server/server.py index 03604a89..a619463a 100644 --- a/letta/server/server.py +++ b/letta/server/server.py @@ -53,6 +53,7 @@ from letta.schemas.memory import ArchivalMemorySummary, ContextWindowOverview, M from letta.schemas.message import Message, MessageCreate, MessageRole, MessageUpdate from letta.schemas.organization import Organization from letta.schemas.passage import Passage +from letta.schemas.sandbox_config import SandboxEnvironmentVariableCreate, SandboxType from letta.schemas.source import Source from letta.schemas.tool import Tool from letta.schemas.usage import LettaUsageStatistics @@ -298,6 +299,17 @@ class SyncServer(Server): self.block_manager.add_default_blocks(actor=self.default_user) self.tool_manager.upsert_base_tools(actor=self.default_user) + # Add composio keys to the tool sandbox env vars of the org + if tool_settings.composio_api_key: + manager = SandboxConfigManager(tool_settings) + sandbox_config = manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.LOCAL, actor=self.default_user) + + manager.create_sandbox_env_var( + SandboxEnvironmentVariableCreate(key="COMPOSIO_API_KEY", value=tool_settings.composio_api_key), + sandbox_config_id=sandbox_config.id, + actor=self.default_user, + ) + # collect providers (always has Letta as a default) self._enabled_providers: List[Provider] = [LettaProvider()] if model_settings.openai_api_key: diff --git a/letta/services/tool_execution_sandbox.py b/letta/services/tool_execution_sandbox.py index fc6e1bdd..1060af43 100644 --- a/letta/services/tool_execution_sandbox.py +++ b/letta/services/tool_execution_sandbox.py @@ -127,7 +127,7 @@ class ToolExecutionSandbox: if local_configs.use_venv: return self.run_local_dir_sandbox_venv(sbx_config, env, temp_file_path) else: - return self.run_local_dir_sandbox_runpy(sbx_config, env_vars, temp_file_path) + return self.run_local_dir_sandbox_runpy(sbx_config, env, temp_file_path) except Exception as e: logger.error(f"Executing tool {self.tool_name} has an unexpected error: {e}") logger.error(f"Logging out tool {self.tool_name} auto-generated code for debugging: \n\n{code}") @@ -200,7 +200,7 @@ class ToolExecutionSandbox: logger.error(f"Executing tool {self.tool_name} has an unexpected error: {e}") raise e - def run_local_dir_sandbox_runpy(self, sbx_config: SandboxConfig, env_vars: Dict[str, str], temp_file_path: str) -> SandboxRunResult: + def run_local_dir_sandbox_runpy(self, sbx_config: SandboxConfig, env: Dict[str, str], temp_file_path: str) -> SandboxRunResult: status = "success" agent_state, stderr = None, None @@ -213,8 +213,8 @@ class ToolExecutionSandbox: try: # Execute the temp file - with self.temporary_env_vars(env_vars): - result = runpy.run_path(temp_file_path, init_globals=env_vars) + with self.temporary_env_vars(env): + result = runpy.run_path(temp_file_path, init_globals=env) # Fetch the result func_result = result.get(self.LOCAL_SANDBOX_RESULT_VAR_NAME) @@ -277,6 +277,10 @@ class ToolExecutionSandbox: sbx_config = self.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.E2B, actor=self.user) sbx = self.get_running_e2b_sandbox_with_same_state(sbx_config) if not sbx or self.force_recreate: + if not sbx: + logger.info(f"No running e2b sandbox found with the same state: {sbx_config}") + else: + logger.info(f"Force recreated e2b sandbox with state: {sbx_config}") sbx = self.create_e2b_sandbox_with_metadata_hash(sandbox_config=sbx_config) # Since this sandbox was used, we extend its lifecycle by the timeout @@ -292,6 +296,8 @@ class ToolExecutionSandbox: func_return, agent_state = self.parse_best_effort(execution.results[0].text) elif execution.error: logger.error(f"Executing tool {self.tool_name} failed with {execution.error}") + logger.error(f"E2B Sandbox configurations: {sbx_config}") + logger.error(f"E2B Sandbox ID: {sbx.sandbox_id}") func_return = get_friendly_error_msg( function_name=self.tool_name, exception_name=execution.error.name, exception_message=execution.error.value ) diff --git a/tests/integration_test_composio.py b/tests/integration_test_composio.py new file mode 100644 index 00000000..1b2c2e3f --- /dev/null +++ b/tests/integration_test_composio.py @@ -0,0 +1,28 @@ +import pytest +from fastapi.testclient import TestClient + +from letta.server.rest_api.app import app + + +@pytest.fixture +def client(): + return TestClient(app) + + +def test_list_composio_apps(client): + response = client.get("/v1/tools/composio/apps") + assert response.status_code == 200 + assert isinstance(response.json(), list) + + +def test_list_composio_actions_by_app(client): + response = client.get("/v1/tools/composio/apps/github/actions") + assert response.status_code == 200 + assert isinstance(response.json(), list) + + +def test_add_composio_tool(client): + response = client.post("/v1/tools/composio/GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER") + assert response.status_code == 200 + assert "id" in response.json() + assert "name" in response.json() diff --git a/tests/integration_test_tool_execution_sandbox.py b/tests/integration_test_tool_execution_sandbox.py index 97346021..3f64b287 100644 --- a/tests/integration_test_tool_execution_sandbox.py +++ b/tests/integration_test_tool_execution_sandbox.py @@ -351,6 +351,14 @@ def test_local_sandbox_e2e_composio_star_github(mock_e2b_api_key_none, check_com assert result.func_return["details"] == "Action executed successfully" +@pytest.mark.local_sandbox +def test_local_sandbox_e2e_composio_star_github_without_setting_db_env_vars( + mock_e2b_api_key_none, check_composio_key_set, composio_github_star_tool, test_user +): + result = ToolExecutionSandbox(composio_github_star_tool.name, {"owner": "letta-ai", "repo": "letta"}, user=test_user).run() + assert result.func_return["details"] == "Action executed successfully" + + @pytest.mark.local_sandbox def test_local_sandbox_external_codebase(mock_e2b_api_key_none, custom_test_sandbox_config, external_codebase_tool, test_user): # Set the args @@ -456,7 +464,7 @@ def test_e2b_sandbox_inject_env_var_existing_sandbox(check_e2b_key_is_set, get_e config = manager.create_or_update_sandbox_config(config_create, test_user) # Run the custom sandbox once, assert nothing returns because missing env variable - sandbox = ToolExecutionSandbox(get_env_tool.name, {}, user=test_user, force_recreate=True) + sandbox = ToolExecutionSandbox(get_env_tool.name, {}, user=test_user) result = sandbox.run() # response should be None assert result.func_return is None diff --git a/tests/test_client.py b/tests/test_client.py index cc391235..5db67157 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -43,7 +43,7 @@ def run_server(): @pytest.fixture( params=[{"server": False}, {"server": True}], # whether to use REST API server - # params=[{"server": True}], # whether to use REST API server + # params=[{"server": False}], # whether to use REST API server scope="module", ) def client(request): @@ -408,6 +408,7 @@ def test_function_always_error(client: Union[LocalClient, RESTClient]): assert response_message, "ToolReturnMessage message not found in response" assert response_message.status == "error" + if isinstance(client, RESTClient): assert response_message.tool_return == "Error executing function always_error: ZeroDivisionError: division by zero" else: