From cfb48a112fdf0ddbcd9bf030151e715566a5b38d Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Tue, 26 Nov 2024 17:06:44 -0800 Subject: [PATCH] fix: pass tool errors through the HTTP POST `/tools` requests with detailed error messages (#2110) --- letta/errors.py | 12 ++++++++++++ letta/functions/functions.py | 10 ++++------ letta/functions/schema_generator.py | 11 ++++++----- letta/server/rest_api/routers/v1/tools.py | 17 ++++++++++++++--- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/letta/errors.py b/letta/errors.py index bd1e5421..cb10c5d9 100644 --- a/letta/errors.py +++ b/letta/errors.py @@ -10,6 +10,18 @@ class LettaError(Exception): """Base class for all Letta related errors.""" +class LettaToolCreateError(LettaError): + """Error raised when a tool cannot be created.""" + + default_error_message = "Error creating tool." + + def __init__(self, message=None): + if message is None: + message = self.default_error_message + self.message = message + super().__init__(self.message) + + class LLMError(LettaError): pass diff --git a/letta/functions/functions.py b/letta/functions/functions.py index 6dabc2e0..fae7ca16 100644 --- a/letta/functions/functions.py +++ b/letta/functions/functions.py @@ -3,9 +3,10 @@ import inspect import os from textwrap import dedent # remove indentation from types import ModuleType -from typing import Optional, List +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 @@ -13,10 +14,7 @@ def derive_openai_json_schema(source_code: str, name: Optional[str] = None) -> d # auto-generate openai schema try: # Define a custom environment with necessary imports - env = { - "Optional": Optional, # Add any other required imports here - "List": List - } + env = {"Optional": Optional, "List": List, "Dict": Dict} # Add any other required imports here env.update(globals()) exec(source_code, env) @@ -29,7 +27,7 @@ def derive_openai_json_schema(source_code: str, name: Optional[str] = None) -> d json_schema = generate_schema(func, name=name) return json_schema except Exception as e: - raise RuntimeError(f"Failed to execute source code: {e}") + raise LettaToolCreateError(f"Failed to derive JSON schema from source code: {e}") def parse_source_code(func) -> str: diff --git a/letta/functions/schema_generator.py b/letta/functions/schema_generator.py index 2969f6f8..01ff969c 100644 --- a/letta/functions/schema_generator.py +++ b/letta/functions/schema_generator.py @@ -131,11 +131,12 @@ def generate_schema(function, name: Optional[str] = None, description: Optional[ else: # Add parameter details to the schema param_doc = next((d for d in docstring.params if d.arg_name == param.name), None) - schema["parameters"]["properties"][param.name] = { - # "type": "string" if param.annotation == str else str(param.annotation), - "type": type_to_json_schema_type(param.annotation) if param.annotation != inspect.Parameter.empty else "string", - "description": param_doc.description, - } + if param_doc: + schema["parameters"]["properties"][param.name] = { + # "type": "string" if param.annotation == str else str(param.annotation), + "type": type_to_json_schema_type(param.annotation) if param.annotation != inspect.Parameter.empty else "string", + "description": param_doc.description, + } if param.default == inspect.Parameter.empty: schema["parameters"]["required"].append(param.name) diff --git a/letta/server/rest_api/routers/v1/tools.py b/letta/server/rest_api/routers/v1/tools.py index 0e3af6e2..57204a85 100644 --- a/letta/server/rest_api/routers/v1/tools.py +++ b/letta/server/rest_api/routers/v1/tools.py @@ -2,6 +2,7 @@ from typing import List, Optional from fastapi import APIRouter, Body, Depends, Header, HTTPException +from letta.errors import LettaToolCreateError from letta.orm.errors import UniqueConstraintViolationError from letta.schemas.tool import Tool, ToolCreate, ToolUpdate from letta.server.rest_api.utils import get_letta_server @@ -14,12 +15,13 @@ router = APIRouter(prefix="/tools", tags=["tools"]) 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(tool_id=tool_id) + 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") @@ -91,7 +93,16 @@ def create_tool( except UniqueConstraintViolationError as e: # Log or print the full exception here for debugging print(f"Error occurred: {e}") - raise HTTPException(status_code=409, detail=str(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}")