Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add endpoints to list Composio apps and actions #2140

Merged
merged 2 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading