mirror of
https://github.com/cpacker/MemGPT.git
synced 2025-06-03 04:30:22 +00:00
chore: Various bug fixes (#1350)
Co-authored-by: Sarah Wooders <sarahwooders@gmail.com> Co-authored-by: cthomas <caren@letta.com> Co-authored-by: tarunkumark <tkksctwo@gmail.com> Co-authored-by: Kevin Lin <klin5061@gmail.com> Co-authored-by: Charles Packer <packercharles@gmail.com> Co-authored-by: Miao <one.lemorage@gmail.com> Co-authored-by: Krishnakumar R (KK) <65895020+kk-src@users.noreply.github.com> Co-authored-by: Shubham Naik <shub@memgpt.ai> Co-authored-by: Shubham Naik <shub@letta.com> Co-authored-by: Will Sargent <will.sargent@gmail.com> Co-authored-by: Shubham Naik <shubham.naik10@gmail.com> Co-authored-by: mlong93 <35275280+mlong93@users.noreply.github.com> Co-authored-by: Mindy Long <mindy@letta.com> Co-authored-by: Stephan Fitzpatrick <stephan@knowsuchagency.com> Co-authored-by: dboyliao <qmalliao@gmail.com> Co-authored-by: Jyotirmaya Mahanta <jyotirmaya.mahanta@gmail.com> Co-authored-by: Nicholas <102550462+ndisalvio3@users.noreply.github.com> Co-authored-by: Tristan Morris <tristanbmorris@gmail.com> Co-authored-by: Daniel Shin <88547237+kyuds@users.noreply.github.com> Co-authored-by: Jindřich Šíma <67415662+JindrichSima@users.noreply.github.com> Co-authored-by: Azin Asgarian <31479845+azinasg@users.noreply.github.com> Co-authored-by: Connor Shorten <connorshorten300@gmail.com> Co-authored-by: Lucas Mohallem Ferraz <ferraz.m.lucas@gmail.com> Co-authored-by: kyuds <kyuds@everspin.co.kr>
This commit is contained in:
parent
f995f7a4cf
commit
4deaafdb49
@ -60,8 +60,8 @@ alembic upgrade head
|
|||||||
Now when you want to use `letta`, make sure you first activate the `poetry` environment using poetry shell:
|
Now when you want to use `letta`, make sure you first activate the `poetry` environment using poetry shell:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ poetry shell
|
$ eval $(poetry env activate)
|
||||||
(pyletta-py3.12) $ letta run
|
(letta-py3.12) $ letta run
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, you can use `poetry run` (which will activate the `poetry` environment for the `letta run` command only):
|
Alternatively, you can use `poetry run` (which will activate the `poetry` environment for the `letta run` command only):
|
||||||
|
@ -67,6 +67,7 @@ ENV LETTA_ENVIRONMENT=${LETTA_ENVIRONMENT} \
|
|||||||
COMPOSIO_DISABLE_VERSION_CHECK=true \
|
COMPOSIO_DISABLE_VERSION_CHECK=true \
|
||||||
LETTA_OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
|
LETTA_OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
|
||||||
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy virtual environment and app from builder
|
# Copy virtual environment and app from builder
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
__version__ = "0.6.41"
|
__version__ = "0.6.43"
|
||||||
|
|
||||||
# import clients
|
# import clients
|
||||||
from letta.client.client import LocalClient, RESTClient, create_client
|
from letta.client.client import LocalClient, RESTClient, create_client
|
||||||
|
@ -2937,7 +2937,6 @@ class LocalClient(AbstractClient):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
func (callable): The function to create a tool for.
|
func (callable): The function to create a tool for.
|
||||||
name: (str): Name of the tool (must be unique per-user.)
|
|
||||||
tags (Optional[List[str]], optional): Tags for the tool. Defaults to None.
|
tags (Optional[List[str]], optional): Tags for the tool. Defaults to None.
|
||||||
description (str, optional): The description.
|
description (str, optional): The description.
|
||||||
return_char_limit (int): The character limit for the tool's return value. Defaults to FUNCTION_RETURN_CHAR_LIMIT.
|
return_char_limit (int): The character limit for the tool's return value. Defaults to FUNCTION_RETURN_CHAR_LIMIT.
|
||||||
@ -2950,6 +2949,7 @@ class LocalClient(AbstractClient):
|
|||||||
# parse source code/schema
|
# parse source code/schema
|
||||||
source_code = parse_source_code(func)
|
source_code = parse_source_code(func)
|
||||||
source_type = "python"
|
source_type = "python"
|
||||||
|
name = func.__name__ # Initialize name using function's __name__
|
||||||
if not tags:
|
if not tags:
|
||||||
tags = []
|
tags = []
|
||||||
|
|
||||||
|
@ -246,19 +246,6 @@ def embedding_model(config: EmbeddingConfig, user_id: Optional[uuid.UUID] = None
|
|||||||
model_settings.azure_api_version is not None,
|
model_settings.azure_api_version is not None,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
# from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
|
|
||||||
|
|
||||||
## https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#embeddings
|
|
||||||
# model = "text-embedding-ada-002"
|
|
||||||
# deployment = credentials.azure_embedding_deployment if credentials.azure_embedding_deployment is not None else model
|
|
||||||
# return AzureOpenAIEmbedding(
|
|
||||||
# model=model,
|
|
||||||
# deployment_name=deployment,
|
|
||||||
# api_key=credentials.azure_key,
|
|
||||||
# azure_endpoint=credentials.azure_endpoint,
|
|
||||||
# api_version=credentials.azure_version,
|
|
||||||
# )
|
|
||||||
|
|
||||||
return AzureOpenAIEmbedding(
|
return AzureOpenAIEmbedding(
|
||||||
api_endpoint=model_settings.azure_base_url,
|
api_endpoint=model_settings.azure_base_url,
|
||||||
api_key=model_settings.azure_api_key,
|
api_key=model_settings.azure_api_key,
|
||||||
|
@ -844,6 +844,7 @@ def anthropic_chat_completions_process_stream(
|
|||||||
total_tokens=prompt_tokens,
|
total_tokens=prompt_tokens,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
log_event(name="llm_request_sent", attributes=chat_completion_request.model_dump())
|
log_event(name="llm_request_sent", attributes=chat_completion_request.model_dump())
|
||||||
|
|
||||||
if stream_interface:
|
if stream_interface:
|
||||||
|
@ -72,7 +72,7 @@ def bedrock_get_model_details(region_name: str, model_id: str) -> Dict[str, Any]
|
|||||||
response = bedrock.get_foundation_model(modelIdentifier=model_id)
|
response = bedrock.get_foundation_model(modelIdentifier=model_id)
|
||||||
return response["modelDetails"]
|
return response["modelDetails"]
|
||||||
except ClientError as e:
|
except ClientError as e:
|
||||||
print(f"Error getting model details: {str(e)}")
|
logger.exception(f"Error getting model details: {str(e)}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from openai import AzureOpenAI
|
||||||
|
|
||||||
from letta.llm_api.helpers import make_post_request
|
from letta.llm_api.openai import prepare_openai_payload
|
||||||
from letta.schemas.llm_config import LLMConfig
|
from letta.schemas.llm_config import LLMConfig
|
||||||
from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
|
from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
|
||||||
from letta.schemas.openai.chat_completions import ChatCompletionRequest
|
from letta.schemas.openai.chat_completions import ChatCompletionRequest
|
||||||
from letta.schemas.openai.embedding_response import EmbeddingResponse
|
|
||||||
from letta.settings import ModelSettings
|
from letta.settings import ModelSettings
|
||||||
from letta.tracing import log_event
|
|
||||||
|
|
||||||
|
|
||||||
def get_azure_chat_completions_endpoint(base_url: str, model: str, api_version: str):
|
def get_azure_chat_completions_endpoint(base_url: str, model: str, api_version: str):
|
||||||
@ -33,20 +32,20 @@ def get_azure_deployment_list_endpoint(base_url: str):
|
|||||||
def azure_openai_get_deployed_model_list(base_url: str, api_key: str, api_version: str) -> list:
|
def azure_openai_get_deployed_model_list(base_url: str, api_key: str, api_version: str) -> list:
|
||||||
"""https://learn.microsoft.com/en-us/rest/api/azureopenai/models/list?view=rest-azureopenai-2023-05-15&tabs=HTTP"""
|
"""https://learn.microsoft.com/en-us/rest/api/azureopenai/models/list?view=rest-azureopenai-2023-05-15&tabs=HTTP"""
|
||||||
|
|
||||||
|
client = AzureOpenAI(api_key=api_key, api_version=api_version, azure_endpoint=base_url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
models_list = client.models.list()
|
||||||
|
except requests.RequestException as e:
|
||||||
|
raise RuntimeError(f"Failed to retrieve model list: {e}")
|
||||||
|
|
||||||
|
all_available_models = [model.to_dict() for model in models_list.data]
|
||||||
|
|
||||||
# https://xxx.openai.azure.com/openai/models?api-version=xxx
|
# https://xxx.openai.azure.com/openai/models?api-version=xxx
|
||||||
headers = {"Content-Type": "application/json"}
|
headers = {"Content-Type": "application/json"}
|
||||||
if api_key is not None:
|
if api_key is not None:
|
||||||
headers["api-key"] = f"{api_key}"
|
headers["api-key"] = f"{api_key}"
|
||||||
|
|
||||||
# 1. Get all available models
|
|
||||||
url = get_azure_model_list_endpoint(base_url, api_version)
|
|
||||||
try:
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
response.raise_for_status()
|
|
||||||
except requests.RequestException as e:
|
|
||||||
raise RuntimeError(f"Failed to retrieve model list: {e}")
|
|
||||||
all_available_models = response.json().get("data", [])
|
|
||||||
|
|
||||||
# 2. Get all the deployed models
|
# 2. Get all the deployed models
|
||||||
url = get_azure_deployment_list_endpoint(base_url)
|
url = get_azure_deployment_list_endpoint(base_url)
|
||||||
try:
|
try:
|
||||||
@ -102,42 +101,18 @@ def azure_openai_get_embeddings_model_list(base_url: str, api_key: str, api_vers
|
|||||||
|
|
||||||
|
|
||||||
def azure_openai_chat_completions_request(
|
def azure_openai_chat_completions_request(
|
||||||
model_settings: ModelSettings, llm_config: LLMConfig, api_key: str, chat_completion_request: ChatCompletionRequest
|
model_settings: ModelSettings, llm_config: LLMConfig, chat_completion_request: ChatCompletionRequest
|
||||||
) -> ChatCompletionResponse:
|
) -> ChatCompletionResponse:
|
||||||
"""https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions"""
|
"""https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#chat-completions"""
|
||||||
|
|
||||||
assert api_key is not None, "Missing required field when calling Azure OpenAI"
|
assert model_settings.azure_api_key is not None, "Missing required api key field when calling Azure OpenAI"
|
||||||
|
assert model_settings.azure_api_version is not None, "Missing required api version field when calling Azure OpenAI"
|
||||||
|
assert model_settings.azure_base_url is not None, "Missing required base url field when calling Azure OpenAI"
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json", "api-key": f"{api_key}"}
|
data = prepare_openai_payload(chat_completion_request)
|
||||||
data = chat_completion_request.model_dump(exclude_none=True)
|
client = AzureOpenAI(
|
||||||
|
api_key=model_settings.azure_api_key, api_version=model_settings.azure_api_version, azure_endpoint=model_settings.azure_base_url
|
||||||
|
)
|
||||||
|
chat_completion = client.chat.completions.create(**data)
|
||||||
|
|
||||||
# If functions == None, strip from the payload
|
return ChatCompletionResponse(**chat_completion.model_dump())
|
||||||
if "functions" in data and data["functions"] is None:
|
|
||||||
data.pop("functions")
|
|
||||||
data.pop("function_call", None) # extra safe, should exist always (default="auto")
|
|
||||||
|
|
||||||
if "tools" in data and data["tools"] is None:
|
|
||||||
data.pop("tools")
|
|
||||||
data.pop("tool_choice", None) # extra safe, should exist always (default="auto")
|
|
||||||
|
|
||||||
url = get_azure_chat_completions_endpoint(model_settings.azure_base_url, llm_config.model, model_settings.azure_api_version)
|
|
||||||
log_event(name="llm_request_sent", attributes=data)
|
|
||||||
response_json = make_post_request(url, headers, data)
|
|
||||||
# NOTE: azure openai does not include "content" in the response when it is None, so we need to add it
|
|
||||||
if "content" not in response_json["choices"][0].get("message"):
|
|
||||||
response_json["choices"][0]["message"]["content"] = None
|
|
||||||
log_event(name="llm_response_received", attributes=response_json)
|
|
||||||
response = ChatCompletionResponse(**response_json) # convert to 'dot-dict' style which is the openai python client default
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def azure_openai_embeddings_request(
|
|
||||||
resource_name: str, deployment_id: str, api_version: str, api_key: str, data: dict
|
|
||||||
) -> EmbeddingResponse:
|
|
||||||
"""https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#embeddings"""
|
|
||||||
|
|
||||||
url = f"https://{resource_name}.openai.azure.com/openai/deployments/{deployment_id}/embeddings?api-version={api_version}"
|
|
||||||
headers = {"Content-Type": "application/json", "api-key": f"{api_key}"}
|
|
||||||
|
|
||||||
response_json = make_post_request(url, headers, data)
|
|
||||||
return EmbeddingResponse(**response_json)
|
|
||||||
|
@ -306,7 +306,6 @@ def create(
|
|||||||
response = azure_openai_chat_completions_request(
|
response = azure_openai_chat_completions_request(
|
||||||
model_settings=model_settings,
|
model_settings=model_settings,
|
||||||
llm_config=llm_config,
|
llm_config=llm_config,
|
||||||
api_key=model_settings.azure_api_key,
|
|
||||||
chat_completion_request=chat_completion_request,
|
chat_completion_request=chat_completion_request,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -286,7 +286,45 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|||||||
Raises:
|
Raises:
|
||||||
NoResultFound: if the object is not found
|
NoResultFound: if the object is not found
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Reading {cls.__name__} with ID: {identifier} with actor={actor}")
|
# this is ok because read_multiple will check if the
|
||||||
|
identifiers = [] if identifier is None else [identifier]
|
||||||
|
found = cls.read_multiple(db_session, identifiers, actor, access, access_type, **kwargs)
|
||||||
|
if len(found) == 0:
|
||||||
|
# for backwards compatibility.
|
||||||
|
conditions = []
|
||||||
|
if identifier:
|
||||||
|
conditions.append(f"id={identifier}")
|
||||||
|
if actor:
|
||||||
|
conditions.append(f"access level in {access} for {actor}")
|
||||||
|
if hasattr(cls, "is_deleted"):
|
||||||
|
conditions.append("is_deleted=False")
|
||||||
|
raise NoResultFound(f"{cls.__name__} not found with {', '.join(conditions if conditions else ['no conditions'])}")
|
||||||
|
return found[0]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@handle_db_timeout
|
||||||
|
def read_multiple(
|
||||||
|
cls,
|
||||||
|
db_session: "Session",
|
||||||
|
identifiers: List[str] = [],
|
||||||
|
actor: Optional["User"] = None,
|
||||||
|
access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
|
||||||
|
access_type: AccessType = AccessType.ORGANIZATION,
|
||||||
|
**kwargs,
|
||||||
|
) -> List["SqlalchemyBase"]:
|
||||||
|
"""The primary accessor for ORM record(s)
|
||||||
|
Args:
|
||||||
|
db_session: the database session to use when retrieving the record
|
||||||
|
identifiers: a list of identifiers of the records to read, can be the id string or the UUID object for backwards compatibility
|
||||||
|
actor: if specified, results will be scoped only to records the user is able to access
|
||||||
|
access: if actor is specified, records will be filtered to the minimum permission level for the actor
|
||||||
|
kwargs: additional arguments to pass to the read, used for more complex objects
|
||||||
|
Returns:
|
||||||
|
The matching object
|
||||||
|
Raises:
|
||||||
|
NoResultFound: if the object is not found
|
||||||
|
"""
|
||||||
|
logger.debug(f"Reading {cls.__name__} with ID(s): {identifiers} with actor={actor}")
|
||||||
|
|
||||||
# Start the query
|
# Start the query
|
||||||
query = select(cls)
|
query = select(cls)
|
||||||
@ -294,9 +332,9 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|||||||
query_conditions = []
|
query_conditions = []
|
||||||
|
|
||||||
# If an identifier is provided, add it to the query conditions
|
# If an identifier is provided, add it to the query conditions
|
||||||
if identifier is not None:
|
if len(identifiers) > 0:
|
||||||
query = query.where(cls.id == identifier)
|
query = query.where(cls.id.in_(identifiers))
|
||||||
query_conditions.append(f"id='{identifier}'")
|
query_conditions.append(f"id='{identifiers}'")
|
||||||
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
query = query.filter_by(**kwargs)
|
query = query.filter_by(**kwargs)
|
||||||
@ -309,12 +347,29 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|||||||
if hasattr(cls, "is_deleted"):
|
if hasattr(cls, "is_deleted"):
|
||||||
query = query.where(cls.is_deleted == False)
|
query = query.where(cls.is_deleted == False)
|
||||||
query_conditions.append("is_deleted=False")
|
query_conditions.append("is_deleted=False")
|
||||||
if found := db_session.execute(query).scalar():
|
|
||||||
return found
|
results = db_session.execute(query).scalars().all()
|
||||||
|
if results: # if empty list a.k.a. no results
|
||||||
|
if len(identifiers) > 0:
|
||||||
|
# find which identifiers were not found
|
||||||
|
# only when identifier length is greater than 0 (so it was used in the actual query)
|
||||||
|
identifier_set = set(identifiers)
|
||||||
|
results_set = set(map(lambda obj: obj.id, results))
|
||||||
|
|
||||||
|
# we log a warning message if any of the queried IDs were not found.
|
||||||
|
# TODO: should we error out instead?
|
||||||
|
if identifier_set != results_set:
|
||||||
|
# Construct a detailed error message based on query conditions
|
||||||
|
conditions_str = ", ".join(query_conditions) if query_conditions else "no specific conditions"
|
||||||
|
logger.warning(
|
||||||
|
f"{cls.__name__} not found with {conditions_str}. Queried ids: {identifier_set}, Found ids: {results_set}"
|
||||||
|
)
|
||||||
|
return results
|
||||||
|
|
||||||
# Construct a detailed error message based on query conditions
|
# Construct a detailed error message based on query conditions
|
||||||
conditions_str = ", ".join(query_conditions) if query_conditions else "no specific conditions"
|
conditions_str = ", ".join(query_conditions) if query_conditions else "no specific conditions"
|
||||||
raise NoResultFound(f"{cls.__name__} not found with {conditions_str}")
|
logger.warning(f"{cls.__name__} not found with {conditions_str}")
|
||||||
|
return []
|
||||||
|
|
||||||
@handle_db_timeout
|
@handle_db_timeout
|
||||||
def create(self, db_session: "Session", actor: Optional["User"] = None) -> "SqlalchemyBase":
|
def create(self, db_session: "Session", actor: Optional["User"] = None) -> "SqlalchemyBase":
|
||||||
|
@ -748,6 +748,12 @@ class Message(BaseMessage):
|
|||||||
else:
|
else:
|
||||||
raise ValueError(self.role)
|
raise ValueError(self.role)
|
||||||
|
|
||||||
|
# Validate that parts is never empty before returning
|
||||||
|
if "parts" not in google_ai_message or not google_ai_message["parts"]:
|
||||||
|
# If parts is empty, add a default text part
|
||||||
|
google_ai_message["parts"] = [{"text": "empty message"}]
|
||||||
|
warnings.warn(f"Empty 'parts' detected in message with role '{self.role}'. Added default empty text part.")
|
||||||
|
|
||||||
return google_ai_message
|
return google_ai_message
|
||||||
|
|
||||||
def to_cohere_dict(
|
def to_cohere_dict(
|
||||||
|
@ -207,9 +207,259 @@ class OpenAIProvider(Provider):
|
|||||||
def get_model_context_window_size(self, model_name: str):
|
def get_model_context_window_size(self, model_name: str):
|
||||||
if model_name in LLM_MAX_TOKENS:
|
if model_name in LLM_MAX_TOKENS:
|
||||||
return LLM_MAX_TOKENS[model_name]
|
return LLM_MAX_TOKENS[model_name]
|
||||||
|
else:
|
||||||
|
return LLM_MAX_TOKENS["DEFAULT"]
|
||||||
|
|
||||||
|
|
||||||
|
class xAIProvider(OpenAIProvider):
|
||||||
|
"""https://docs.x.ai/docs/api-reference"""
|
||||||
|
|
||||||
|
name: str = "xai"
|
||||||
|
api_key: str = Field(..., description="API key for the xAI/Grok API.")
|
||||||
|
base_url: str = Field("https://api.x.ai/v1", description="Base URL for the xAI/Grok API.")
|
||||||
|
|
||||||
|
def get_model_context_window_size(self, model_name: str) -> Optional[int]:
|
||||||
|
# xAI doesn't return context window in the model listing,
|
||||||
|
# so these are hardcoded from their website
|
||||||
|
if model_name == "grok-2-1212":
|
||||||
|
return 131072
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def list_llm_models(self) -> List[LLMConfig]:
|
||||||
|
from letta.llm_api.openai import openai_get_model_list
|
||||||
|
|
||||||
|
response = openai_get_model_list(self.base_url, api_key=self.api_key)
|
||||||
|
|
||||||
|
if "data" in response:
|
||||||
|
data = response["data"]
|
||||||
|
else:
|
||||||
|
data = response
|
||||||
|
|
||||||
|
configs = []
|
||||||
|
for model in data:
|
||||||
|
assert "id" in model, f"xAI/Grok model missing 'id' field: {model}"
|
||||||
|
model_name = model["id"]
|
||||||
|
|
||||||
|
# In case xAI starts supporting it in the future:
|
||||||
|
if "context_length" in model:
|
||||||
|
context_window_size = model["context_length"]
|
||||||
|
else:
|
||||||
|
context_window_size = self.get_model_context_window_size(model_name)
|
||||||
|
|
||||||
|
if not context_window_size:
|
||||||
|
warnings.warn(f"Couldn't find context window size for model {model_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
configs.append(
|
||||||
|
LLMConfig(
|
||||||
|
model=model_name,
|
||||||
|
model_endpoint_type="xai",
|
||||||
|
model_endpoint=self.base_url,
|
||||||
|
context_window=context_window_size,
|
||||||
|
handle=self.get_handle(model_name),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return configs
|
||||||
|
|
||||||
|
def list_embedding_models(self) -> List[EmbeddingConfig]:
|
||||||
|
# No embeddings supported
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class DeepSeekProvider(OpenAIProvider):
|
||||||
|
"""
|
||||||
|
DeepSeek ChatCompletions API is similar to OpenAI's reasoning API,
|
||||||
|
but with slight differences:
|
||||||
|
* For example, DeepSeek's API requires perfect interleaving of user/assistant
|
||||||
|
* It also does not support native function calling
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str = "deepseek"
|
||||||
|
base_url: str = Field("https://api.deepseek.com/v1", description="Base URL for the DeepSeek API.")
|
||||||
|
api_key: str = Field(..., description="API key for the DeepSeek API.")
|
||||||
|
|
||||||
|
def get_model_context_window_size(self, model_name: str) -> Optional[int]:
|
||||||
|
# DeepSeek doesn't return context window in the model listing,
|
||||||
|
# so these are hardcoded from their website
|
||||||
|
if model_name == "deepseek-reasoner":
|
||||||
|
return 64000
|
||||||
|
elif model_name == "deepseek-chat":
|
||||||
|
return 64000
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list_llm_models(self) -> List[LLMConfig]:
|
||||||
|
from letta.llm_api.openai import openai_get_model_list
|
||||||
|
|
||||||
|
response = openai_get_model_list(self.base_url, api_key=self.api_key)
|
||||||
|
|
||||||
|
if "data" in response:
|
||||||
|
data = response["data"]
|
||||||
|
else:
|
||||||
|
data = response
|
||||||
|
|
||||||
|
configs = []
|
||||||
|
for model in data:
|
||||||
|
assert "id" in model, f"DeepSeek model missing 'id' field: {model}"
|
||||||
|
model_name = model["id"]
|
||||||
|
|
||||||
|
# In case DeepSeek starts supporting it in the future:
|
||||||
|
if "context_length" in model:
|
||||||
|
# Context length is returned in OpenRouter as "context_length"
|
||||||
|
context_window_size = model["context_length"]
|
||||||
|
else:
|
||||||
|
context_window_size = self.get_model_context_window_size(model_name)
|
||||||
|
|
||||||
|
if not context_window_size:
|
||||||
|
warnings.warn(f"Couldn't find context window size for model {model_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Not used for deepseek-reasoner, but otherwise is true
|
||||||
|
put_inner_thoughts_in_kwargs = False if model_name == "deepseek-reasoner" else True
|
||||||
|
|
||||||
|
configs.append(
|
||||||
|
LLMConfig(
|
||||||
|
model=model_name,
|
||||||
|
model_endpoint_type="deepseek",
|
||||||
|
model_endpoint=self.base_url,
|
||||||
|
context_window=context_window_size,
|
||||||
|
handle=self.get_handle(model_name),
|
||||||
|
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return configs
|
||||||
|
|
||||||
|
def list_embedding_models(self) -> List[EmbeddingConfig]:
|
||||||
|
# No embeddings supported
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class LMStudioOpenAIProvider(OpenAIProvider):
|
||||||
|
name: str = "lmstudio-openai"
|
||||||
|
base_url: str = Field(..., description="Base URL for the LMStudio OpenAI API.")
|
||||||
|
api_key: Optional[str] = Field(None, description="API key for the LMStudio API.")
|
||||||
|
|
||||||
|
def list_llm_models(self) -> List[LLMConfig]:
|
||||||
|
from letta.llm_api.openai import openai_get_model_list
|
||||||
|
|
||||||
|
# For LMStudio, we want to hit 'GET /api/v0/models' instead of 'GET /v1/models'
|
||||||
|
MODEL_ENDPOINT_URL = f"{self.base_url.strip('/v1')}/api/v0"
|
||||||
|
response = openai_get_model_list(MODEL_ENDPOINT_URL)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
{
|
||||||
|
"object": "list",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "qwen2-vl-7b-instruct",
|
||||||
|
"object": "model",
|
||||||
|
"type": "vlm",
|
||||||
|
"publisher": "mlx-community",
|
||||||
|
"arch": "qwen2_vl",
|
||||||
|
"compatibility_type": "mlx",
|
||||||
|
"quantization": "4bit",
|
||||||
|
"state": "not-loaded",
|
||||||
|
"max_context_length": 32768
|
||||||
|
},
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
if "data" not in response:
|
||||||
|
warnings.warn(f"LMStudio OpenAI model query response missing 'data' field: {response}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
configs = []
|
||||||
|
for model in response["data"]:
|
||||||
|
assert "id" in model, f"Model missing 'id' field: {model}"
|
||||||
|
model_name = model["id"]
|
||||||
|
|
||||||
|
if "type" not in model:
|
||||||
|
warnings.warn(f"LMStudio OpenAI model missing 'type' field: {model}")
|
||||||
|
continue
|
||||||
|
elif model["type"] not in ["vlm", "llm"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "max_context_length" in model:
|
||||||
|
context_window_size = model["max_context_length"]
|
||||||
|
else:
|
||||||
|
warnings.warn(f"LMStudio OpenAI model missing 'max_context_length' field: {model}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
configs.append(
|
||||||
|
LLMConfig(
|
||||||
|
model=model_name,
|
||||||
|
model_endpoint_type="openai",
|
||||||
|
model_endpoint=self.base_url,
|
||||||
|
context_window=context_window_size,
|
||||||
|
handle=self.get_handle(model_name),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return configs
|
||||||
|
|
||||||
|
def list_embedding_models(self) -> List[EmbeddingConfig]:
|
||||||
|
from letta.llm_api.openai import openai_get_model_list
|
||||||
|
|
||||||
|
# For LMStudio, we want to hit 'GET /api/v0/models' instead of 'GET /v1/models'
|
||||||
|
MODEL_ENDPOINT_URL = f"{self.base_url.strip('/v1')}/api/v0"
|
||||||
|
response = openai_get_model_list(MODEL_ENDPOINT_URL)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Example response:
|
||||||
|
{
|
||||||
|
"object": "list",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "text-embedding-nomic-embed-text-v1.5",
|
||||||
|
"object": "model",
|
||||||
|
"type": "embeddings",
|
||||||
|
"publisher": "nomic-ai",
|
||||||
|
"arch": "nomic-bert",
|
||||||
|
"compatibility_type": "gguf",
|
||||||
|
"quantization": "Q4_0",
|
||||||
|
"state": "not-loaded",
|
||||||
|
"max_context_length": 2048
|
||||||
|
}
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
if "data" not in response:
|
||||||
|
warnings.warn(f"LMStudio OpenAI model query response missing 'data' field: {response}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
configs = []
|
||||||
|
for model in response["data"]:
|
||||||
|
assert "id" in model, f"Model missing 'id' field: {model}"
|
||||||
|
model_name = model["id"]
|
||||||
|
|
||||||
|
if "type" not in model:
|
||||||
|
warnings.warn(f"LMStudio OpenAI model missing 'type' field: {model}")
|
||||||
|
continue
|
||||||
|
elif model["type"] not in ["embeddings"]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "max_context_length" in model:
|
||||||
|
context_window_size = model["max_context_length"]
|
||||||
|
else:
|
||||||
|
warnings.warn(f"LMStudio OpenAI model missing 'max_context_length' field: {model}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
configs.append(
|
||||||
|
EmbeddingConfig(
|
||||||
|
embedding_model=model_name,
|
||||||
|
embedding_endpoint_type="openai",
|
||||||
|
embedding_endpoint=self.base_url,
|
||||||
|
embedding_dim=context_window_size,
|
||||||
|
embedding_chunk_size=300, # NOTE: max is 2048
|
||||||
|
handle=self.get_handle(model_name),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return configs
|
||||||
|
|
||||||
|
|
||||||
class xAIProvider(OpenAIProvider):
|
class xAIProvider(OpenAIProvider):
|
||||||
"""https://docs.x.ai/docs/api-reference"""
|
"""https://docs.x.ai/docs/api-reference"""
|
||||||
|
@ -43,16 +43,6 @@ interface: StreamingServerInterface = StreamingServerInterface
|
|||||||
server = SyncServer(default_interface_factory=lambda: interface())
|
server = SyncServer(default_interface_factory=lambda: interface())
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
# TODO: remove
|
|
||||||
password = None
|
|
||||||
## TODO(ethan): eventuall remove
|
|
||||||
# if password := settings.server_pass:
|
|
||||||
# # if the pass was specified in the environment, use it
|
|
||||||
# print(f"Using existing admin server password from environment.")
|
|
||||||
# else:
|
|
||||||
# # Autogenerate a password for this session and dump it to stdout
|
|
||||||
# password = secrets.token_urlsafe(16)
|
|
||||||
# #typer.secho(f"Generated admin server password for this session: {password}", fg=typer.colors.GREEN)
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import platform
|
import platform
|
||||||
@ -287,7 +277,7 @@ def create_application() -> "FastAPI":
|
|||||||
app.include_router(openai_chat_completions_router, prefix=OPENAI_API_PREFIX)
|
app.include_router(openai_chat_completions_router, prefix=OPENAI_API_PREFIX)
|
||||||
|
|
||||||
# /api/auth endpoints
|
# /api/auth endpoints
|
||||||
app.include_router(setup_auth_router(server, interface, password), prefix=API_PREFIX)
|
app.include_router(setup_auth_router(server, interface, random_password), prefix=API_PREFIX)
|
||||||
|
|
||||||
# / static files
|
# / static files
|
||||||
mount_static_files(app)
|
mount_static_files(app)
|
||||||
|
@ -32,7 +32,7 @@ class OptimisticJSONParser:
|
|||||||
self.on_extra_token = self.default_on_extra_token
|
self.on_extra_token = self.default_on_extra_token
|
||||||
|
|
||||||
def default_on_extra_token(self, text, data, reminding):
|
def default_on_extra_token(self, text, data, reminding):
|
||||||
pass
|
print(f"Parsed JSON with extra tokens: {data}, remaining: {reminding}")
|
||||||
|
|
||||||
def parse(self, input_str):
|
def parse(self, input_str):
|
||||||
"""
|
"""
|
||||||
@ -130,8 +130,8 @@ class OptimisticJSONParser:
|
|||||||
if end == -1:
|
if end == -1:
|
||||||
# Incomplete string
|
# Incomplete string
|
||||||
if not self.strict:
|
if not self.strict:
|
||||||
return input_str[1:], ""
|
return input_str[1:], "" # Lenient mode returns partial string
|
||||||
return json.loads(f'"{input_str[1:]}"'), ""
|
raise decode_error # Raise error for incomplete string in strict mode
|
||||||
|
|
||||||
str_val = input_str[: end + 1]
|
str_val = input_str[: end + 1]
|
||||||
input_str = input_str[end + 1 :]
|
input_str = input_str[end + 1 :]
|
||||||
@ -152,8 +152,8 @@ class OptimisticJSONParser:
|
|||||||
num_str = input_str[:idx]
|
num_str = input_str[:idx]
|
||||||
remainder = input_str[idx:]
|
remainder = input_str[idx:]
|
||||||
|
|
||||||
# If it's only a sign or just '.', return as-is with empty remainder
|
# If not strict, and it's only a sign or just '.', return as-is with empty remainder
|
||||||
if not num_str or num_str in {"-", "."}:
|
if not self.strict and (not num_str or num_str in {"-", "."}):
|
||||||
return num_str, ""
|
return num_str, ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -106,12 +106,14 @@ class BlockManager:
|
|||||||
|
|
||||||
@enforce_types
|
@enforce_types
|
||||||
def get_all_blocks_by_ids(self, block_ids: List[str], actor: Optional[PydanticUser] = None) -> List[PydanticBlock]:
|
def get_all_blocks_by_ids(self, block_ids: List[str], actor: Optional[PydanticUser] = None) -> List[PydanticBlock]:
|
||||||
# TODO: We can do this much more efficiently by listing, instead of executing individual queries per block_id
|
"""Retrieve blocks by their names."""
|
||||||
blocks = []
|
with self.session_maker() as session:
|
||||||
for block_id in block_ids:
|
blocks = list(
|
||||||
block = self.get_block_by_id(block_id, actor=actor)
|
map(lambda obj: obj.to_pydantic(), BlockModel.read_multiple(db_session=session, identifiers=block_ids, actor=actor))
|
||||||
blocks.append(block)
|
)
|
||||||
return blocks
|
# backwards compatibility. previous implementation added None for every block not found.
|
||||||
|
blocks.extend([None for _ in range(len(block_ids) - len(blocks))])
|
||||||
|
return blocks
|
||||||
|
|
||||||
@enforce_types
|
@enforce_types
|
||||||
def add_default_blocks(self, actor: PydanticUser):
|
def add_default_blocks(self, actor: PydanticUser):
|
||||||
|
@ -129,6 +129,44 @@ class MessageManager:
|
|||||||
# raise error if message type got modified
|
# raise error if message type got modified
|
||||||
raise ValueError(f"Message type got modified: {letta_message_update.message_type}")
|
raise ValueError(f"Message type got modified: {letta_message_update.message_type}")
|
||||||
|
|
||||||
|
@enforce_types
|
||||||
|
def update_message_by_letta_message(
|
||||||
|
self, message_id: str, letta_message_update: LettaMessageUpdateUnion, actor: PydanticUser
|
||||||
|
) -> PydanticMessage:
|
||||||
|
"""
|
||||||
|
Updated the underlying messages table giving an update specified to the user-facing LettaMessage
|
||||||
|
"""
|
||||||
|
message = self.get_message_by_id(message_id=message_id, actor=actor)
|
||||||
|
if letta_message_update.message_type == "assistant_message":
|
||||||
|
# modify the tool call for send_message
|
||||||
|
# TODO: fix this if we add parallel tool calls
|
||||||
|
# TODO: note this only works if the AssistantMessage is generated by the standard send_message
|
||||||
|
assert (
|
||||||
|
message.tool_calls[0].function.name == "send_message"
|
||||||
|
), f"Expected the first tool call to be send_message, but got {message.tool_calls[0].function.name}"
|
||||||
|
original_args = json.loads(message.tool_calls[0].function.arguments)
|
||||||
|
original_args["message"] = letta_message_update.content # override the assistant message
|
||||||
|
update_tool_call = message.tool_calls[0].__deepcopy__()
|
||||||
|
update_tool_call.function.arguments = json.dumps(original_args)
|
||||||
|
|
||||||
|
update_message = MessageUpdate(tool_calls=[update_tool_call])
|
||||||
|
elif letta_message_update.message_type == "reasoning_message":
|
||||||
|
update_message = MessageUpdate(content=letta_message_update.reasoning)
|
||||||
|
elif letta_message_update.message_type == "user_message" or letta_message_update.message_type == "system_message":
|
||||||
|
update_message = MessageUpdate(content=letta_message_update.content)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported message type for modification: {letta_message_update.message_type}")
|
||||||
|
|
||||||
|
message = self.update_message_by_id(message_id=message_id, message_update=update_message, actor=actor)
|
||||||
|
|
||||||
|
# convert back to LettaMessage
|
||||||
|
for letta_msg in message.to_letta_message(use_assistant_message=True):
|
||||||
|
if letta_msg.message_type == letta_message_update.message_type:
|
||||||
|
return letta_msg
|
||||||
|
|
||||||
|
# raise error if message type got modified
|
||||||
|
raise ValueError(f"Message type got modified: {letta_message_update.message_type}")
|
||||||
|
|
||||||
@enforce_types
|
@enforce_types
|
||||||
def update_message_by_id(self, message_id: str, message_update: MessageUpdate, actor: PydanticUser) -> PydanticMessage:
|
def update_message_by_id(self, message_id: str, message_update: MessageUpdate, actor: PydanticUser) -> PydanticMessage:
|
||||||
"""
|
"""
|
||||||
|
52
poetry.lock
generated
52
poetry.lock
generated
@ -500,10 +500,6 @@ files = [
|
|||||||
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"},
|
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"},
|
||||||
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"},
|
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"},
|
||||||
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"},
|
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"},
|
||||||
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c"},
|
|
||||||
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1"},
|
|
||||||
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2"},
|
|
||||||
{file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec"},
|
|
||||||
{file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"},
|
{file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"},
|
||||||
{file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"},
|
{file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"},
|
||||||
{file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"},
|
{file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"},
|
||||||
@ -516,14 +512,8 @@ files = [
|
|||||||
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"},
|
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"},
|
||||||
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"},
|
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"},
|
||||||
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"},
|
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"},
|
||||||
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f"},
|
|
||||||
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757"},
|
|
||||||
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0"},
|
|
||||||
{file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b"},
|
|
||||||
{file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"},
|
{file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"},
|
||||||
{file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"},
|
{file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"},
|
||||||
{file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28"},
|
|
||||||
{file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f"},
|
|
||||||
{file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"},
|
{file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"},
|
||||||
{file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"},
|
{file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"},
|
||||||
{file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"},
|
{file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"},
|
||||||
@ -534,24 +524,8 @@ files = [
|
|||||||
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"},
|
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"},
|
||||||
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"},
|
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"},
|
||||||
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"},
|
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"},
|
||||||
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9"},
|
|
||||||
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb"},
|
|
||||||
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111"},
|
|
||||||
{file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839"},
|
|
||||||
{file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"},
|
{file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"},
|
||||||
{file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"},
|
{file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"},
|
||||||
{file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5"},
|
|
||||||
{file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8"},
|
|
||||||
{file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f"},
|
|
||||||
{file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648"},
|
|
||||||
{file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0"},
|
|
||||||
{file = "Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089"},
|
|
||||||
{file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368"},
|
|
||||||
{file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c"},
|
|
||||||
{file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284"},
|
|
||||||
{file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7"},
|
|
||||||
{file = "Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0"},
|
|
||||||
{file = "Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b"},
|
|
||||||
{file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"},
|
{file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"},
|
||||||
{file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"},
|
{file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"},
|
||||||
{file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"},
|
{file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"},
|
||||||
@ -561,10 +535,6 @@ files = [
|
|||||||
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"},
|
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"},
|
||||||
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"},
|
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"},
|
||||||
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"},
|
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"},
|
||||||
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75"},
|
|
||||||
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c"},
|
|
||||||
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2"},
|
|
||||||
{file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52"},
|
|
||||||
{file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"},
|
{file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"},
|
||||||
{file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"},
|
{file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"},
|
||||||
{file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"},
|
{file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"},
|
||||||
@ -576,10 +546,6 @@ files = [
|
|||||||
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"},
|
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"},
|
||||||
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"},
|
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"},
|
||||||
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"},
|
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"},
|
||||||
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01"},
|
|
||||||
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547"},
|
|
||||||
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38"},
|
|
||||||
{file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c"},
|
|
||||||
{file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"},
|
{file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"},
|
||||||
{file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"},
|
{file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"},
|
||||||
{file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"},
|
{file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"},
|
||||||
@ -592,10 +558,6 @@ files = [
|
|||||||
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"},
|
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"},
|
||||||
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"},
|
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"},
|
||||||
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"},
|
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"},
|
||||||
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7"},
|
|
||||||
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5"},
|
|
||||||
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943"},
|
|
||||||
{file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a"},
|
|
||||||
{file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"},
|
{file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"},
|
||||||
{file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"},
|
{file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"},
|
||||||
{file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"},
|
{file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"},
|
||||||
@ -608,10 +570,6 @@ files = [
|
|||||||
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"},
|
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"},
|
||||||
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"},
|
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"},
|
||||||
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"},
|
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"},
|
||||||
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419"},
|
|
||||||
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2"},
|
|
||||||
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f"},
|
|
||||||
{file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb"},
|
|
||||||
{file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"},
|
{file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"},
|
||||||
{file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"},
|
{file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"},
|
||||||
{file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"},
|
{file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"},
|
||||||
@ -1035,9 +993,9 @@ isort = ">=4.3.21,<6.0"
|
|||||||
jinja2 = ">=2.10.1,<4.0"
|
jinja2 = ">=2.10.1,<4.0"
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
pydantic = [
|
pydantic = [
|
||||||
|
{version = ">=1.10.0,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.4.0 || >2.4.0,<3.0", extras = ["email"], markers = "python_version >= \"3.12\" and python_version < \"4.0\""},
|
||||||
{version = ">=1.10.0,<2.4.0 || >2.4.0,<3.0", extras = ["email"], markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
|
{version = ">=1.10.0,<2.4.0 || >2.4.0,<3.0", extras = ["email"], markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
|
||||||
{version = ">=1.9.0,<2.4.0 || >2.4.0,<3.0", extras = ["email"], markers = "python_version >= \"3.10\" and python_version < \"3.11\""},
|
{version = ">=1.9.0,<2.4.0 || >2.4.0,<3.0", extras = ["email"], markers = "python_version >= \"3.10\" and python_version < \"3.11\""},
|
||||||
{version = ">=1.10.0,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.4.0 || >2.4.0,<3.0", extras = ["email"], markers = "python_version >= \"3.12\" and python_version < \"4.0\""},
|
|
||||||
]
|
]
|
||||||
pyyaml = ">=6.0.1"
|
pyyaml = ">=6.0.1"
|
||||||
toml = {version = ">=0.10.0,<1.0.0", markers = "python_version < \"3.11\""}
|
toml = {version = ">=0.10.0,<1.0.0", markers = "python_version < \"3.11\""}
|
||||||
@ -3035,8 +2993,8 @@ psutil = ">=5.9.1"
|
|||||||
pywin32 = {version = "*", markers = "sys_platform == \"win32\""}
|
pywin32 = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
pyzmq = ">=25.0.0"
|
pyzmq = ">=25.0.0"
|
||||||
requests = [
|
requests = [
|
||||||
{version = ">=2.26.0", markers = "python_version <= \"3.11\""},
|
|
||||||
{version = ">=2.32.2", markers = "python_version > \"3.11\""},
|
{version = ">=2.32.2", markers = "python_version > \"3.11\""},
|
||||||
|
{version = ">=2.26.0", markers = "python_version <= \"3.11\""},
|
||||||
]
|
]
|
||||||
setuptools = ">=70.0.0"
|
setuptools = ">=70.0.0"
|
||||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||||
@ -3899,9 +3857,9 @@ files = [
|
|||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
numpy = [
|
numpy = [
|
||||||
|
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||||
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
|
{version = ">=1.23.2", markers = "python_version == \"3.11\""},
|
||||||
{version = ">=1.22.4", markers = "python_version < \"3.11\""},
|
{version = ">=1.22.4", markers = "python_version < \"3.11\""},
|
||||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
|
||||||
]
|
]
|
||||||
python-dateutil = ">=2.8.2"
|
python-dateutil = ">=2.8.2"
|
||||||
pytz = ">=2020.1"
|
pytz = ">=2020.1"
|
||||||
@ -4387,7 +4345,6 @@ files = [
|
|||||||
{file = "psycopg2-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:0435034157049f6846e95103bd8f5a668788dd913a7c30162ca9503fdf542cb4"},
|
{file = "psycopg2-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:0435034157049f6846e95103bd8f5a668788dd913a7c30162ca9503fdf542cb4"},
|
||||||
{file = "psycopg2-2.9.10-cp312-cp312-win32.whl", hash = "sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067"},
|
{file = "psycopg2-2.9.10-cp312-cp312-win32.whl", hash = "sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067"},
|
||||||
{file = "psycopg2-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e"},
|
{file = "psycopg2-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e"},
|
||||||
{file = "psycopg2-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:91fd603a2155da8d0cfcdbf8ab24a2d54bca72795b90d2a3ed2b6da8d979dee2"},
|
|
||||||
{file = "psycopg2-2.9.10-cp39-cp39-win32.whl", hash = "sha256:9d5b3b94b79a844a986d029eee38998232451119ad653aea42bb9220a8c5066b"},
|
{file = "psycopg2-2.9.10-cp39-cp39-win32.whl", hash = "sha256:9d5b3b94b79a844a986d029eee38998232451119ad653aea42bb9220a8c5066b"},
|
||||||
{file = "psycopg2-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:88138c8dedcbfa96408023ea2b0c369eda40fe5d75002c0964c78f46f11fa442"},
|
{file = "psycopg2-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:88138c8dedcbfa96408023ea2b0c369eda40fe5d75002c0964c78f46f11fa442"},
|
||||||
{file = "psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11"},
|
{file = "psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11"},
|
||||||
@ -4447,7 +4404,6 @@ files = [
|
|||||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"},
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"},
|
||||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"},
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"},
|
||||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"},
|
{file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"},
|
||||||
{file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"},
|
|
||||||
{file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"},
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"},
|
||||||
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"},
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"},
|
||||||
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"},
|
{file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"},
|
||||||
@ -5192,8 +5148,8 @@ grpcio = ">=1.41.0"
|
|||||||
grpcio-tools = ">=1.41.0"
|
grpcio-tools = ">=1.41.0"
|
||||||
httpx = {version = ">=0.20.0", extras = ["http2"]}
|
httpx = {version = ">=0.20.0", extras = ["http2"]}
|
||||||
numpy = [
|
numpy = [
|
||||||
{version = ">=1.21", markers = "python_version >= \"3.10\" and python_version < \"3.12\""},
|
|
||||||
{version = ">=1.26", markers = "python_version == \"3.12\""},
|
{version = ">=1.26", markers = "python_version == \"3.12\""},
|
||||||
|
{version = ">=1.21", markers = "python_version >= \"3.10\" and python_version < \"3.12\""},
|
||||||
]
|
]
|
||||||
portalocker = ">=2.7.0,<3.0.0"
|
portalocker = ">=2.7.0,<3.0.0"
|
||||||
pydantic = ">=1.10.8"
|
pydantic = ">=1.10.8"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "letta"
|
name = "letta"
|
||||||
version = "0.6.42"
|
version = "0.6.43"
|
||||||
packages = [
|
packages = [
|
||||||
{include = "letta"},
|
{include = "letta"},
|
||||||
]
|
]
|
||||||
|
@ -96,7 +96,7 @@ def test_parse_number_cases(strict_parser):
|
|||||||
def test_parse_boolean_true(strict_parser):
|
def test_parse_boolean_true(strict_parser):
|
||||||
assert strict_parser.parse("true") is True, "Should parse 'true'."
|
assert strict_parser.parse("true") is True, "Should parse 'true'."
|
||||||
# Check leftover
|
# Check leftover
|
||||||
assert strict_parser.last_parse_reminding == "", "No extra tokens expected."
|
assert strict_parser.last_parse_reminding == None, "No extra tokens expected."
|
||||||
|
|
||||||
|
|
||||||
def test_parse_boolean_false(strict_parser):
|
def test_parse_boolean_false(strict_parser):
|
||||||
@ -246,3 +246,35 @@ def test_multiple_parse_calls(strict_parser):
|
|||||||
result_2 = strict_parser.parse(input_2)
|
result_2 = strict_parser.parse(input_2)
|
||||||
assert result_2 == [2, 3]
|
assert result_2 == [2, 3]
|
||||||
assert strict_parser.last_parse_reminding.strip() == "trailing2"
|
assert strict_parser.last_parse_reminding.strip() == "trailing2"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_incomplete_string_streaming_strict(strict_parser):
|
||||||
|
"""
|
||||||
|
Test how a strict parser handles an incomplete string received in chunks.
|
||||||
|
"""
|
||||||
|
# Simulate streaming chunks
|
||||||
|
chunk1 = '{"message": "This is an incomplete'
|
||||||
|
chunk2 = " string with a newline\\n"
|
||||||
|
chunk3 = 'and more text"}'
|
||||||
|
|
||||||
|
with pytest.raises(json.JSONDecodeError, match="Unterminated string"):
|
||||||
|
strict_parser.parse(chunk1)
|
||||||
|
|
||||||
|
incomplete_json = chunk1 + chunk2
|
||||||
|
with pytest.raises(json.JSONDecodeError, match="Unterminated string"):
|
||||||
|
strict_parser.parse(incomplete_json)
|
||||||
|
|
||||||
|
complete_json = incomplete_json + chunk3
|
||||||
|
result = strict_parser.parse(complete_json)
|
||||||
|
expected = {"message": "This is an incomplete string with a newline\nand more text"}
|
||||||
|
assert result == expected, "Should parse complete JSON correctly"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unescaped_control_characters_strict(strict_parser):
|
||||||
|
"""
|
||||||
|
Test parsing JSON containing unescaped control characters in strict mode.
|
||||||
|
"""
|
||||||
|
input_str = '{"message": "This has a newline\nand tab\t"}'
|
||||||
|
|
||||||
|
with pytest.raises(json.JSONDecodeError, match="Invalid control character"):
|
||||||
|
strict_parser.parse(input_str)
|
||||||
|
Loading…
Reference in New Issue
Block a user