Skip to content

Commit

Permalink
Merge branch 'main' into allow-tool-exec-via-api
Browse files Browse the repository at this point in the history
  • Loading branch information
cpacker authored Dec 3, 2024
2 parents a794230 + aa9dda5 commit b2a583c
Show file tree
Hide file tree
Showing 12 changed files with 459 additions and 95 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
- "test_agent_tool_graph.py"
- "test_utils.py"
- "test_tool_schema_parsing.py"
- "test_v1_routes.py"
services:
qdrant:
image: qdrant/qdrant
Expand Down Expand Up @@ -132,4 +133,4 @@ jobs:
LETTA_SERVER_PASS: test_server_token
PYTHONPATH: ${{ github.workspace }}:${{ env.PYTHONPATH }}
run: |
poetry run pytest -s -vv -k "not test_model_letta_perfomance.py and not test_utils.py and not test_client.py and not integration_test_tool_execution_sandbox.py and not integration_test_summarizer.py and not test_agent_tool_graph.py and not test_tool_rule_solver.py and not test_local_client.py and not test_o1_agent.py and not test_cli.py and not test_concurrent_connections.py and not test_quickstart and not test_model_letta_performance and not test_storage and not test_server and not test_openai_client and not test_providers and not test_client_legacy.py" tests
poetry run pytest -s -vv -k "not test_v1_routes.py and not test_model_letta_perfomance.py and not test_utils.py and not test_client.py and not integration_test_tool_execution_sandbox.py and not integration_test_summarizer.py and not test_agent_tool_graph.py and not test_tool_rule_solver.py and not test_local_client.py and not test_o1_agent.py and not test_cli.py and not test_concurrent_connections.py and not test_quickstart and not test_model_letta_performance and not test_storage and not test_server and not test_openai_client and not test_providers and not test_client_legacy.py" tests
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,24 @@ The two main ways to install Letta are through **pypi** (`pip`) or via **Docker*

### Step 1 - Install Letta using `pip`
```sh
$ pip install -U letta
pip install -U letta
```

### Step 2 - Set your environment variables for your chosen LLM / embedding providers
```sh
$ export OPENAI_API_KEY=sk-...
export OPENAI_API_KEY=sk-...
```

For Ollama (see our full [documentation](https://docs.letta.com/install) for examples of how to set up various providers):
```sh
$ export OLLAMA_BASE_URL=http://localhost:11434
export OLLAMA_BASE_URL=http://localhost:11434
```

### Step 3 - Run the Letta CLI

You can create agents and chat with them via the Letta CLI tool (`letta run`):
```sh
$ letta run
letta run
```
```
🧬 Creating new agent...
Expand Down Expand Up @@ -94,7 +94,7 @@ Hit enter to begin (will request first Letta message)

You can start the Letta API server with `letta server` (see the full API reference [here](https://docs.letta.com/api-reference)):
```sh
$ letta server
letta server
```
```
Initializing database...
Expand Down
46 changes: 0 additions & 46 deletions letta/functions/functions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import importlib
import inspect
import os
from textwrap import dedent # remove indentation
from types import ModuleType
from typing import Dict, List, Optional

from letta.constants import CLI_WARNING_PREFIX
from letta.errors import LettaToolCreateError
from letta.functions.schema_generator import generate_schema

Expand Down Expand Up @@ -90,46 +87,3 @@ def load_function_set(module: ModuleType) -> dict:
if len(function_dict) == 0:
raise ValueError(f"No functions found in module {module}")
return function_dict


def validate_function(module_name, module_full_path):
try:
file = os.path.basename(module_full_path)
spec = importlib.util.spec_from_file_location(module_name, module_full_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
except ModuleNotFoundError as e:
# Handle missing module imports
missing_package = str(e).split("'")[1] # Extract the name of the missing package
print(f"{CLI_WARNING_PREFIX}skipped loading python file '{module_full_path}'!")
return (
False,
f"'{file}' imports '{missing_package}', but '{missing_package}' is not installed locally - install python package '{missing_package}' to link functions from '{file}' to Letta.",
)
except SyntaxError as e:
# Handle syntax errors in the module
return False, f"{CLI_WARNING_PREFIX}skipped loading python file '{file}' due to a syntax error: {e}"
except Exception as e:
# Handle other general exceptions
return False, f"{CLI_WARNING_PREFIX}skipped loading python file '{file}': {e}"

return True, None


def load_function_file(filepath: str) -> dict:
file = os.path.basename(filepath)
module_name = file[:-3] # Remove '.py' from filename
try:
spec = importlib.util.spec_from_file_location(module_name, filepath)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
except ModuleNotFoundError as e:
# Handle missing module imports
missing_package = str(e).split("'")[1] # Extract the name of the missing package
print(f"{CLI_WARNING_PREFIX}skipped loading python file '{filepath}'!")
print(
f"'{file}' imports '{missing_package}', but '{missing_package}' is not installed locally - install python package '{missing_package}' to link functions from '{file}' to Letta."
)
# load all functions in the module
function_dict = load_function_set(module)
return function_dict
19 changes: 19 additions & 0 deletions letta/server/rest_api/routers/v1/sandbox_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from letta.schemas.sandbox_config import (
SandboxEnvironmentVariableCreate,
SandboxEnvironmentVariableUpdate,
SandboxType,
)
from letta.server.rest_api.utils import get_letta_server, get_user_id
from letta.server.server import SyncServer
Expand All @@ -29,6 +30,24 @@ def create_sandbox_config(
return server.sandbox_config_manager.create_or_update_sandbox_config(config_create, actor)


@router.post("/e2b/default", response_model=PydanticSandboxConfig)
def create_default_e2b_sandbox_config(
server: SyncServer = Depends(get_letta_server),
user_id: str = Depends(get_user_id),
):
actor = server.get_user_or_default(user_id=user_id)
return server.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.E2B, actor=actor)


@router.post("/local/default", response_model=PydanticSandboxConfig)
def create_default_local_sandbox_config(
server: SyncServer = Depends(get_letta_server),
user_id: str = Depends(get_user_id),
):
actor = server.get_user_or_default(user_id=user_id)
return server.sandbox_config_manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.LOCAL, actor=actor)


@router.patch("/{sandbox_config_id}", response_model=PydanticSandboxConfig)
def update_sandbox_config(
sandbox_config_id: str,
Expand Down
37 changes: 37 additions & 0 deletions letta/server/rest_api/routers/v1/tools.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import List, Optional

from composio.client.collections import ActionModel, AppModel
from fastapi import APIRouter, Body, Depends, Header, HTTPException

from letta.errors import LettaToolCreateError
Expand Down Expand Up @@ -191,3 +192,39 @@ def run_tool_from_source(
tool_name=request.name,
user_id=actor.id,
)


# Specific routes for Composio


@router.get("/composio/apps", response_model=List[AppModel], operation_id="list_composio_apps")
def list_composio_apps(server: SyncServer = Depends(get_letta_server)):
"""
Get a list of all Composio apps
"""
return server.get_composio_apps()


@router.get("/composio/apps/{composio_app_name}/actions", response_model=List[ActionModel], operation_id="list_composio_actions_by_app")
def list_composio_actions_by_app(
composio_app_name: str,
server: SyncServer = Depends(get_letta_server),
):
"""
Get a list of all Composio actions for a specific app
"""
return server.get_composio_actions_from_app_name(composio_app_name=composio_app_name)


@router.post("/composio/{composio_action_name}", response_model=Tool, operation_id="add_composio_tool")
def add_composio_tool(
composio_action_name: str,
server: SyncServer = Depends(get_letta_server),
user_id: Optional[str] = Header(None, alias="user_id"),
):
"""
Add a new Composio tool by action name (Composio refers to each tool as an `Action`)
"""
actor = server.get_user_or_default(user_id=user_id)
tool_create = ToolCreate.from_composio(action=composio_action_name)
return server.tool_manager.create_or_update_tool(pydantic_tool=Tool(**tool_create.model_dump()), actor=actor)
24 changes: 23 additions & 1 deletion letta/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from datetime import datetime
from typing import Callable, Dict, List, Optional, Tuple, Union

from composio.client import Composio
from composio.client.collections import ActionModel, AppModel
from fastapi import HTTPException

import letta.constants as constants
Expand Down Expand Up @@ -229,6 +231,11 @@ def __init__(
# Locks
self.send_message_lock = Lock()

# Composio
self.composio_client = None
if tool_settings.composio_api_key:
self.composio_client = Composio(api_key=tool_settings.composio_api_key)

# Initialize the metadata store
config = LettaConfig.load()
if settings.letta_pg_uri_no_default:
Expand Down Expand Up @@ -1753,7 +1760,7 @@ def get_agent_block_by_label(self, user_id: str, agent_id: str, label: str) -> B
return block
return None

def run_tool(self, tool_id: str, tool_args: str, user_id: str) -> FunctionReturn:
def run_tool(self, tool_id: str, tool_args: str, user_id: str) -> FunctionReturn:
"""Run a tool using the sandbox and return the result"""

try:
Expand Down Expand Up @@ -1856,3 +1863,18 @@ def run_tool_from_source(
status="error",
function_return=error_msg,
)

# Composio wrappers
def get_composio_apps(self) -> List["AppModel"]:
"""Get a list of all Composio apps with actions"""
apps = self.composio_client.apps.get()
apps_with_actions = []
for app in apps:
if app.meta["actionsCount"] > 0:
apps_with_actions.append(app)

return apps_with_actions

def get_composio_actions_from_app_name(self, composio_app_name: str) -> List["ActionModel"]:
actions = self.composio_client.actions.get(apps=[composio_app_name])
return actions
Loading

0 comments on commit b2a583c

Please sign in to comment.