Merge pull request #491 from letta-ai/matt/let-671-for-local-sandbox-using-local-env-variables-instead-of

fix: Add local composio env vars to the default org/user
This commit is contained in:
Matthew Zhou
2024-12-30 14:59:20 -10:00
committed by GitHub
5 changed files with 61 additions and 6 deletions

View File

@@ -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:

View File

@@ -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
)

View File

@@ -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()

View File

@@ -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

View File

@@ -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: