diff --git a/letta/functions/function_sets/extras.py b/letta/functions/function_sets/extras.py deleted file mode 100644 index 4c91af76..00000000 --- a/letta/functions/function_sets/extras.py +++ /dev/null @@ -1,135 +0,0 @@ -import os -import uuid -from typing import Optional - -import requests - -from letta.constants import MESSAGE_CHATGPT_FUNCTION_MODEL, MESSAGE_CHATGPT_FUNCTION_SYSTEM_MESSAGE -from letta.helpers.json_helpers import json_dumps, json_loads -from letta.llm_api.llm_api_tools import create -from letta.schemas.letta_message_content import TextContent -from letta.schemas.message import Message - - -def message_chatgpt(self, message: str): - """ - Send a message to a more basic AI, ChatGPT. A useful resource for asking questions. ChatGPT does not retain memory of previous interactions. - - Args: - message (str): Message to send ChatGPT. Phrase your message as a full English sentence. - - Returns: - str: Reply message from ChatGPT - """ - dummy_user_id = uuid.uuid4() - dummy_agent_id = uuid.uuid4() - message_sequence = [ - Message( - user_id=dummy_user_id, - agent_id=dummy_agent_id, - role="system", - content=[TextContent(text=MESSAGE_CHATGPT_FUNCTION_SYSTEM_MESSAGE)], - ), - Message(user_id=dummy_user_id, agent_id=dummy_agent_id, role="user", content=[TextContent(text=str(message))]), - ] - # TODO: this will error without an LLMConfig - response = create( - model=MESSAGE_CHATGPT_FUNCTION_MODEL, - messages=message_sequence, - ) - - reply = response.choices[0].message.content - return reply - - -def read_from_text_file(self, filename: str, line_start: int, num_lines: Optional[int] = 1): - """ - Read lines from a text file. - - Args: - filename (str): The name of the file to read. - line_start (int): Line to start reading from. - num_lines (Optional[int]): How many lines to read (defaults to 1). - - Returns: - str: Text read from the file - """ - max_chars = 500 - trunc_message = True - if not os.path.exists(filename): - raise FileNotFoundError(f"The file '{filename}' does not exist.") - - if line_start < 1 or num_lines < 1: - raise ValueError("Both line_start and num_lines must be positive integers.") - - lines = [] - chars_read = 0 - with open(filename, "r", encoding="utf-8") as file: - for current_line_number, line in enumerate(file, start=1): - if line_start <= current_line_number < line_start + num_lines: - chars_to_add = len(line) - if max_chars is not None and chars_read + chars_to_add > max_chars: - # If adding this line exceeds MAX_CHARS, truncate the line if needed and stop reading further. - excess_chars = (chars_read + chars_to_add) - max_chars - lines.append(line[:-excess_chars].rstrip("\n")) - if trunc_message: - lines.append(f"[SYSTEM ALERT - max chars ({max_chars}) reached during file read]") - break - else: - lines.append(line.rstrip("\n")) - chars_read += chars_to_add - if current_line_number >= line_start + num_lines - 1: - break - - return "\n".join(lines) - - -def append_to_text_file(self, filename: str, content: str): - """ - Append to a text file. - - Args: - filename (str): The name of the file to append to. - content (str): Content to append to the file. - - Returns: - Optional[str]: None is always returned as this function does not produce a response. - """ - if not os.path.exists(filename): - raise FileNotFoundError(f"The file '{filename}' does not exist.") - - with open(filename, "a", encoding="utf-8") as file: - file.write(content + "\n") - - -def http_request(self, method: str, url: str, payload_json: Optional[str] = None): - """ - Generates an HTTP request and returns the response. - - Args: - method (str): The HTTP method (e.g., 'GET', 'POST'). - url (str): The URL for the request. - payload_json (Optional[str]): A JSON string representing the request payload. - - Returns: - dict: The response from the HTTP request. - """ - try: - headers = {"Content-Type": "application/json"} - - # For GET requests, ignore the payload - if method.upper() == "GET": - print(f"[HTTP] launching GET request to {url}") - response = requests.get(url, headers=headers) - else: - # Validate and convert the payload for other types of requests - if payload_json: - payload = json_loads(payload_json) - else: - payload = {} - print(f"[HTTP] launching {method} request to {url}, payload=\n{json_dumps(payload, indent=2)}") - response = requests.request(method, url, json=payload, headers=headers) - - return {"status_code": response.status_code, "headers": dict(response.headers), "body": response.text} - except Exception as e: - return {"error": str(e)} diff --git a/letta/llm_api/google_ai_client.py b/letta/llm_api/google_ai_client.py index 15190e3c..8a90e7cb 100644 --- a/letta/llm_api/google_ai_client.py +++ b/letta/llm_api/google_ai_client.py @@ -116,42 +116,6 @@ async def google_ai_get_model_list_async( await client.aclose() -def google_ai_get_model_details(base_url: str, api_key: str, model: str, key_in_header: bool = True) -> dict: - """Synchronous version to get model details from Google AI API using httpx.""" - import httpx - - from letta.utils import printd - - url, headers = get_gemini_endpoint_and_headers(base_url, model, api_key, key_in_header) - - try: - with httpx.Client() as client: - response = client.get(url, headers=headers) - printd(f"response = {response}") - response.raise_for_status() # Raises HTTPStatusError for 4XX/5XX status - response_data = response.json() # convert to dict from string - printd(f"response.json = {response_data}") - - # Return the model details - return response_data - - except httpx.HTTPStatusError as http_err: - # Handle HTTP errors (e.g., response 4XX, 5XX) - printd(f"Got HTTPError, exception={http_err}") - logger.error(f"HTTP Error: {http_err.response.status_code}, Message: {http_err.response.text}") - raise http_err - - except httpx.RequestError as req_err: - # Handle other httpx-related errors (e.g., connection error) - printd(f"Got RequestException, exception={req_err}") - raise req_err - - except Exception as e: - # Handle other potential errors - printd(f"Got unknown Exception, exception={e}") - raise e - - async def google_ai_get_model_details_async( base_url: str, api_key: str, model: str, key_in_header: bool = True, client: Optional[httpx.AsyncClient] = None ) -> dict: @@ -200,13 +164,6 @@ async def google_ai_get_model_details_async( await client.aclose() -def google_ai_get_model_context_window(base_url: str, api_key: str, model: str, key_in_header: bool = True) -> int: - model_details = google_ai_get_model_details(base_url=base_url, api_key=api_key, model=model, key_in_header=key_in_header) - # TODO should this be: - # return model_details["inputTokenLimit"] + model_details["outputTokenLimit"] - return int(model_details["inputTokenLimit"]) - - async def google_ai_get_model_context_window_async(base_url: str, api_key: str, model: str, key_in_header: bool = True) -> int: model_details = await google_ai_get_model_details_async(base_url=base_url, api_key=api_key, model=model, key_in_header=key_in_header) # TODO should this be: diff --git a/letta/llm_api/helpers.py b/letta/llm_api/helpers.py index 624c13d7..aee2b73b 100644 --- a/letta/llm_api/helpers.py +++ b/letta/llm_api/helpers.py @@ -4,8 +4,6 @@ import logging from collections import OrderedDict from typing import Any, List, Optional, Union -import requests - from letta.constants import OPENAI_CONTEXT_WINDOW_ERROR_SUBSTRING from letta.helpers.json_helpers import json_dumps from letta.log import get_logger @@ -229,68 +227,6 @@ def convert_response_format_to_responses_api( } -def make_post_request(url: str, headers: dict[str, str], data: dict[str, Any]) -> dict[str, Any]: - printd(f"Sending request to {url}") - try: - # Make the POST request - response = requests.post(url, headers=headers, json=data) - printd(f"Response status code: {response.status_code}") - - # Raise for 4XX/5XX HTTP errors - response.raise_for_status() - - # Check if the response content type indicates JSON and attempt to parse it - content_type = response.headers.get("Content-Type", "") - if "application/json" in content_type.lower(): - try: - response_data = response.json() # Attempt to parse the response as JSON - printd(f"Response JSON: {response_data}") - except ValueError as json_err: - # Handle the case where the content type says JSON but the body is invalid - error_message = f"Failed to parse JSON despite Content-Type being {content_type}: {json_err}" - printd(error_message) - raise ValueError(error_message) from json_err - else: - error_message = f"Unexpected content type returned: {response.headers.get('Content-Type')}" - printd(error_message) - raise ValueError(error_message) - - # Process the response using the callback function - return response_data - - except requests.exceptions.HTTPError as http_err: - # HTTP errors (4XX, 5XX) - error_message = f"HTTP error occurred: {http_err}" - if http_err.response is not None: - error_message += f" | Status code: {http_err.response.status_code}, Message: {http_err.response.text}" - printd(error_message) - raise requests.exceptions.HTTPError(error_message) from http_err - - except requests.exceptions.Timeout as timeout_err: - # Handle timeout errors - error_message = f"Request timed out: {timeout_err}" - printd(error_message) - raise requests.exceptions.Timeout(error_message) from timeout_err - - except requests.exceptions.RequestException as req_err: - # Non-HTTP errors (e.g., connection, SSL errors) - error_message = f"Request failed: {req_err}" - printd(error_message) - raise requests.exceptions.RequestException(error_message) from req_err - - except ValueError as val_err: - # Handle content-type or non-JSON response issues - error_message = f"ValueError: {val_err}" - printd(error_message) - raise ValueError(error_message) from val_err - - except Exception as e: - # Catch any other unknown exceptions - error_message = f"An unexpected error occurred: {e}" - printd(error_message) - raise Exception(error_message) from e - - # TODO update to use better types def add_inner_thoughts_to_functions( functions: List[dict], diff --git a/letta/llm_api/openai.py b/letta/llm_api/openai.py index b3afb268..8c8692b8 100644 --- a/letta/llm_api/openai.py +++ b/letta/llm_api/openai.py @@ -1,13 +1,11 @@ from typing import Generator, List, Optional, Union import httpx -import requests from openai import OpenAI from letta.constants import LETTA_MODEL_ENDPOINT -from letta.errors import ErrorCode, LLMAuthenticationError, LLMError from letta.helpers.datetime_helpers import timestamp_to_datetime -from letta.llm_api.helpers import add_inner_thoughts_to_functions, convert_to_structured_output, make_post_request +from letta.llm_api.helpers import add_inner_thoughts_to_functions, convert_to_structured_output from letta.llm_api.openai_client import ( accepts_developer_role, requires_auto_tool_choice, @@ -38,7 +36,6 @@ from letta.schemas.openai.chat_completion_response import ( ToolCall, UsageStatistics, ) -from letta.schemas.openai.embedding_response import EmbeddingResponse from letta.settings import model_settings from letta.streaming_interface import AgentChunkStreamingInterface, AgentRefreshStreamingInterface from letta.utils import get_tool_call_id, smart_urljoin @@ -46,88 +43,6 @@ from letta.utils import get_tool_call_id, smart_urljoin logger = get_logger(__name__) -# TODO: MOVE THIS TO OPENAI_CLIENT -def openai_check_valid_api_key(base_url: str, api_key: Union[str, None]) -> None: - if api_key: - try: - # just get model list to check if the api key is valid until we find a cheaper / quicker endpoint - openai_get_model_list(url=base_url, api_key=api_key) - except requests.HTTPError as e: - if e.response.status_code == 401: - raise LLMAuthenticationError(message=f"Failed to authenticate with OpenAI: {e}", code=ErrorCode.UNAUTHENTICATED) - raise e - except Exception as e: - raise LLMError(message=f"{e}", code=ErrorCode.INTERNAL_SERVER_ERROR) - else: - raise ValueError("No API key provided") - - -def openai_get_model_list(url: str, api_key: Optional[str] = None, fix_url: bool = False, extra_params: Optional[dict] = None) -> dict: - """https://platform.openai.com/docs/api-reference/models/list""" - - # In some cases we may want to double-check the URL and do basic correction, eg: - # In Letta config the address for vLLM is w/o a /v1 suffix for simplicity - # However if we're treating the server as an OpenAI proxy we want the /v1 suffix on our model hit - - logger.warning( - "The synchronous version of openai_get_model_list function is deprecated. Use the async one instead.", - stacklevel=2, - ) - - if fix_url: - if not url.endswith("/v1"): - url = smart_urljoin(url, "v1") - - url = smart_urljoin(url, "models") - - headers = {"Content-Type": "application/json"} - if api_key is not None: - headers["Authorization"] = f"Bearer {api_key}" - # Add optional OpenRouter headers if hitting OpenRouter - if "openrouter.ai" in url: - if model_settings.openrouter_referer: - headers["HTTP-Referer"] = model_settings.openrouter_referer - if model_settings.openrouter_title: - headers["X-Title"] = model_settings.openrouter_title - - logger.debug(f"Sending request to {url}") - response = None - try: - # TODO add query param "tool" to be true - response = requests.get(url, headers=headers, params=extra_params) - response.raise_for_status() # Raises HTTPError for 4XX/5XX status - response = response.json() # convert to dict from string - logger.debug(f"response = {response}") - return response - except requests.exceptions.HTTPError as http_err: - # Handle HTTP errors (e.g., response 4XX, 5XX) - try: - if response: - response = response.json() - except: - pass - logger.debug(f"Got HTTPError, exception={http_err}, response={response}") - raise http_err - except requests.exceptions.RequestException as req_err: - # Handle other requests-related errors (e.g., connection error) - try: - if response: - response = response.json() - except: - pass - logger.debug(f"Got RequestException, exception={req_err}, response={response}") - raise req_err - except Exception as e: - # Handle other potential errors - try: - if response: - response = response.json() - except: - pass - logger.debug(f"Got unknown Exception, exception={e}, response={response}") - raise e - - async def openai_get_model_list_async( url: str, api_key: Optional[str] = None, @@ -611,15 +526,6 @@ def openai_chat_completions_request( return ChatCompletionResponse(**chat_completion.model_dump()) -def openai_embeddings_request(url: str, api_key: str, data: dict) -> EmbeddingResponse: - """https://platform.openai.com/docs/api-reference/embeddings/create""" - - url = smart_urljoin(url, "embeddings") - headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"} - response_json = make_post_request(url, headers, data) - return EmbeddingResponse(**response_json) - - def prepare_openai_payload(chat_completion_request: ChatCompletionRequest): data = chat_completion_request.model_dump(exclude_none=True) diff --git a/letta/schemas/providers/google_gemini.py b/letta/schemas/providers/google_gemini.py index 1659a6cd..c5080de5 100644 --- a/letta/schemas/providers/google_gemini.py +++ b/letta/schemas/providers/google_gemini.py @@ -95,18 +95,6 @@ class GoogleAIProvider(Provider): ) return configs - def get_model_context_window(self, model_name: str) -> int | None: - import warnings - - logger.warning("This is deprecated, use get_model_context_window_async when possible.") - from letta.llm_api.google_ai_client import google_ai_get_model_context_window - - if model_name in LLM_MAX_CONTEXT_WINDOW: - return LLM_MAX_CONTEXT_WINDOW[model_name] - else: - api_key = self.api_key_enc.get_plaintext() if self.api_key_enc else None - return google_ai_get_model_context_window(self.base_url, api_key, model_name) - async def get_model_context_window_async(self, model_name: str) -> int | None: from letta.llm_api.google_ai_client import google_ai_get_model_context_window_async