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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
28
tests/integration_test_composio.py
Normal file
28
tests/integration_test_composio.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user