Skip to content

Commit

Permalink
feat: Add endpoints to list Composio apps and actions (#2140)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattzh72 authored Dec 2, 2024
1 parent 551ea1a commit aa9dda5
Show file tree
Hide file tree
Showing 10 changed files with 434 additions and 90 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
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
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 @@ -156,3 +157,39 @@ def add_base_tools(
"""
actor = server.get_user_or_default(user_id=user_id)
return server.tool_manager.add_base_tools(actor=actor)


# 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)
22 changes: 22 additions & 0 deletions letta/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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 @@ -227,6 +229,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 @@ -1750,3 +1757,18 @@ def get_agent_block_by_label(self, user_id: str, agent_id: str, label: str) -> B
if block.label == label:
return block
return None

# 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
52 changes: 26 additions & 26 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit aa9dda5

Please sign in to comment.