MemGPT/letta/server/rest_api/routers/v1/tools.py

246 lines
9.3 KiB
Python

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
from letta.orm.errors import UniqueConstraintViolationError
from letta.schemas.letta_message import FunctionReturn
from letta.schemas.tool import Tool, ToolCreate, ToolRunFromSource, ToolUpdate
from letta.server.rest_api.utils import get_letta_server
from letta.server.server import SyncServer
router = APIRouter(prefix="/tools", tags=["tools"])
@router.delete("/{tool_id}", operation_id="delete_tool")
def delete_tool(
tool_id: str,
server: SyncServer = Depends(get_letta_server),
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
):
"""
Delete a tool by name
"""
actor = server.get_user_or_default(user_id=user_id)
server.tool_manager.delete_tool_by_id(tool_id=tool_id, actor=actor)
@router.get("/{tool_id}", response_model=Tool, operation_id="get_tool")
def get_tool(
tool_id: str,
server: SyncServer = Depends(get_letta_server),
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
):
"""
Get a tool by ID
"""
actor = server.get_user_or_default(user_id=user_id)
tool = server.tool_manager.get_tool_by_id(tool_id=tool_id, actor=actor)
if tool is None:
# return 404 error
raise HTTPException(status_code=404, detail=f"Tool with id {tool_id} not found.")
return tool
@router.get("/name/{tool_name}", response_model=str, operation_id="get_tool_id_by_name")
def get_tool_id(
tool_name: str,
server: SyncServer = Depends(get_letta_server),
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
):
"""
Get a tool ID by name
"""
actor = server.get_user_or_default(user_id=user_id)
tool = server.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
if tool:
return tool.id
else:
raise HTTPException(status_code=404, detail=f"Tool with name {tool_name} and organization id {actor.organization_id} not found.")
@router.get("/", response_model=List[Tool], operation_id="list_tools")
def list_tools(
cursor: Optional[str] = None,
limit: Optional[int] = 50,
server: SyncServer = Depends(get_letta_server),
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
):
"""
Get a list of all tools available to agents belonging to the org of the user
"""
try:
actor = server.get_user_or_default(user_id=user_id)
return server.tool_manager.list_tools(actor=actor, cursor=cursor, limit=limit)
except Exception as e:
# Log or print the full exception here for debugging
print(f"Error occurred: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/", response_model=Tool, operation_id="create_tool")
def create_tool(
request: ToolCreate = Body(...),
server: SyncServer = Depends(get_letta_server),
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
):
"""
Create a new tool
"""
try:
actor = server.get_user_or_default(user_id=user_id)
tool = Tool(**request.model_dump())
return server.tool_manager.create_tool(pydantic_tool=tool, actor=actor)
except UniqueConstraintViolationError as e:
# Log or print the full exception here for debugging
print(f"Error occurred: {e}")
clean_error_message = f"Tool with name {request.name} already exists."
raise HTTPException(status_code=409, detail=clean_error_message)
except LettaToolCreateError as e:
# HTTP 400 == Bad Request
print(f"Error occurred during tool creation: {e}")
# print the full stack trace
import traceback
print(traceback.format_exc())
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
# Catch other unexpected errors and raise an internal server error
print(f"Unexpected error occurred: {e}")
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
@router.put("/", response_model=Tool, operation_id="upsert_tool")
def upsert_tool(
request: ToolCreate = Body(...),
server: SyncServer = Depends(get_letta_server),
user_id: Optional[str] = Header(None, alias="user_id"),
):
"""
Create or update a tool
"""
try:
actor = server.get_user_or_default(user_id=user_id)
tool = server.tool_manager.create_or_update_tool(pydantic_tool=Tool(**request.model_dump()), actor=actor)
return tool
except UniqueConstraintViolationError as e:
# Log the error and raise a conflict exception
print(f"Unique constraint violation occurred: {e}")
raise HTTPException(status_code=409, detail=str(e))
except Exception as e:
# Catch other unexpected errors and raise an internal server error
print(f"Unexpected error occurred: {e}")
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
@router.patch("/{tool_id}", response_model=Tool, operation_id="update_tool")
def update_tool(
tool_id: str,
request: ToolUpdate = Body(...),
server: SyncServer = Depends(get_letta_server),
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
):
"""
Update an existing tool
"""
actor = server.get_user_or_default(user_id=user_id)
return server.tool_manager.update_tool_by_id(tool_id=tool_id, tool_update=request, actor=actor)
@router.post("/add-base-tools", response_model=List[Tool], operation_id="add_base_tools")
def add_base_tools(
server: SyncServer = Depends(get_letta_server),
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
):
"""
Add base tools
"""
actor = server.get_user_or_default(user_id=user_id)
return server.tool_manager.add_base_tools(actor=actor)
# NOTE: can re-enable if needed
# @router.post("/{tool_id}/run", response_model=FunctionReturn, operation_id="run_tool")
# def run_tool(
# server: SyncServer = Depends(get_letta_server),
# request: ToolRun = Body(...),
# user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
# ):
# """
# Run an existing tool on provided arguments
# """
# actor = server.get_user_or_default(user_id=user_id)
# return server.run_tool(tool_id=request.tool_id, tool_args=request.tool_args, user_id=actor.id)
@router.post("/run", response_model=FunctionReturn, operation_id="run_tool_from_source")
def run_tool_from_source(
server: SyncServer = Depends(get_letta_server),
request: ToolRunFromSource = Body(...),
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
):
"""
Attempt to build a tool from source, then run it on the provided arguments
"""
actor = server.get_user_or_default(user_id=user_id)
try:
return server.run_tool_from_source(
tool_source=request.source_code,
tool_source_type=request.source_type,
tool_args=request.args,
tool_name=request.name,
user_id=actor.id,
)
except LettaToolCreateError as e:
# HTTP 400 == Bad Request
print(f"Error occurred during tool creation: {e}")
# print the full stack trace
import traceback
print(traceback.format_exc())
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
# Catch other unexpected errors and raise an internal server error
print(f"Unexpected error occurred: {e}")
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
# 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)