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:
|
||||
|
||||
```shell
|
||||
$ poetry shell
|
||||
(pyletta-py3.12) $ letta run
|
||||
$ eval $(poetry env activate)
|
||||
(letta-py3.12) $ letta run
|
||||
```
|
||||
|
||||
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 \
|
||||
LETTA_OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy virtual environment and app from builder
|
||||
|
@ -1,4 +1,4 @@
|
||||
__version__ = "0.6.41"
|
||||
__version__ = "0.6.43"
|
||||
|
||||
# import clients
|
||||
from letta.client.client import LocalClient, RESTClient, create_client
|
||||
|
@ -2937,7 +2937,6 @@ class LocalClient(AbstractClient):
|
||||
|
||||
Args:
|
||||
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.
|
||||
description (str, optional): The description.
|
||||
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
|
||||
source_code = parse_source_code(func)
|
||||
source_type = "python"
|
||||
name = func.__name__ # Initialize name using function's __name__
|
||||
if not 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,
|
||||
]
|
||||
)
|
||||
# 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(
|
||||
api_endpoint=model_settings.azure_base_url,
|
||||
api_key=model_settings.azure_api_key,
|
||||
|
@ -844,6 +844,7 @@ def anthropic_chat_completions_process_stream(
|
||||
total_tokens=prompt_tokens,
|
||||
),
|
||||
)
|
||||
|
||||
log_event(name="llm_request_sent", attributes=chat_completion_request.model_dump())
|
||||
|
||||
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)
|
||||
return response["modelDetails"]
|
||||
except ClientError as e:
|
||||
print(f"Error getting model details: {str(e)}")
|
||||
logger.exception(f"Error getting model details: {str(e)}")
|
||||
raise e
|
||||
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
from collections import defaultdict
|
||||
|
||||
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.openai.chat_completion_response import ChatCompletionResponse
|
||||
from letta.schemas.openai.chat_completions import ChatCompletionRequest
|
||||
from letta.schemas.openai.embedding_response import EmbeddingResponse
|
||||
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):
|
||||
@ -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:
|
||||
"""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
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if api_key is not None:
|
||||
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
|
||||
url = get_azure_deployment_list_endpoint(base_url)
|
||||
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(
|
||||
model_settings: ModelSettings, llm_config: LLMConfig, api_key: str, chat_completion_request: ChatCompletionRequest
|
||||
model_settings: ModelSettings, llm_config: LLMConfig, chat_completion_request: ChatCompletionRequest
|
||||
) -> ChatCompletionResponse:
|
||||
"""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 = chat_completion_request.model_dump(exclude_none=True)
|
||||
data = prepare_openai_payload(chat_completion_request)
|
||||
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
|
||||
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)
|
||||
return ChatCompletionResponse(**chat_completion.model_dump())
|
||||
|
@ -306,7 +306,6 @@ def create(
|
||||
response = azure_openai_chat_completions_request(
|
||||
model_settings=model_settings,
|
||||
llm_config=llm_config,
|
||||
api_key=model_settings.azure_api_key,
|
||||
chat_completion_request=chat_completion_request,
|
||||
)
|
||||
|
||||
|
@ -286,7 +286,45 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
||||
Raises:
|
||||
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
|
||||
query = select(cls)
|
||||
@ -294,9 +332,9 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
||||
query_conditions = []
|
||||
|
||||
# If an identifier is provided, add it to the query conditions
|
||||
if identifier is not None:
|
||||
query = query.where(cls.id == identifier)
|
||||
query_conditions.append(f"id='{identifier}'")
|
||||
if len(identifiers) > 0:
|
||||
query = query.where(cls.id.in_(identifiers))
|
||||
query_conditions.append(f"id='{identifiers}'")
|
||||
|
||||
if kwargs:
|
||||
query = query.filter_by(**kwargs)
|
||||
@ -309,12 +347,29 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
||||
if hasattr(cls, "is_deleted"):
|
||||
query = query.where(cls.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
|
||||
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
|
||||
def create(self, db_session: "Session", actor: Optional["User"] = None) -> "SqlalchemyBase":
|
||||
|
@ -748,6 +748,12 @@ class Message(BaseMessage):
|
||||
else:
|
||||
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
|
||||
|
||||
def to_cohere_dict(
|
||||
|
@ -207,9 +207,259 @@ class OpenAIProvider(Provider):
|
||||
def get_model_context_window_size(self, model_name: str):
|
||||
if model_name in LLM_MAX_TOKENS:
|
||||
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:
|
||||
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):
|
||||
"""https://docs.x.ai/docs/api-reference"""
|
||||
|
@ -43,16 +43,6 @@ interface: StreamingServerInterface = StreamingServerInterface
|
||||
server = SyncServer(default_interface_factory=lambda: interface())
|
||||
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 platform
|
||||
@ -287,7 +277,7 @@ def create_application() -> "FastAPI":
|
||||
app.include_router(openai_chat_completions_router, prefix=OPENAI_API_PREFIX)
|
||||
|
||||
# /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
|
||||
mount_static_files(app)
|
||||
|
@ -32,7 +32,7 @@ class OptimisticJSONParser:
|
||||
self.on_extra_token = self.default_on_extra_token
|
||||
|
||||
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):
|
||||
"""
|
||||
@ -130,8 +130,8 @@ class OptimisticJSONParser:
|
||||
if end == -1:
|
||||
# Incomplete string
|
||||
if not self.strict:
|
||||
return input_str[1:], ""
|
||||
return json.loads(f'"{input_str[1:]}"'), ""
|
||||
return input_str[1:], "" # Lenient mode returns partial string
|
||||
raise decode_error # Raise error for incomplete string in strict mode
|
||||
|
||||
str_val = input_str[: end + 1]
|
||||
input_str = input_str[end + 1 :]
|
||||
@ -152,8 +152,8 @@ class OptimisticJSONParser:
|
||||
num_str = input_str[:idx]
|
||||
remainder = input_str[idx:]
|
||||
|
||||
# If it's only a sign or just '.', return as-is with empty remainder
|
||||
if not num_str or num_str in {"-", "."}:
|
||||
# If not strict, and it's only a sign or just '.', return as-is with empty remainder
|
||||
if not self.strict and (not num_str or num_str in {"-", "."}):
|
||||
return num_str, ""
|
||||
|
||||
try:
|
||||
|
@ -106,11 +106,13 @@ class BlockManager:
|
||||
|
||||
@enforce_types
|
||||
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
|
||||
blocks = []
|
||||
for block_id in block_ids:
|
||||
block = self.get_block_by_id(block_id, actor=actor)
|
||||
blocks.append(block)
|
||||
"""Retrieve blocks by their names."""
|
||||
with self.session_maker() as session:
|
||||
blocks = list(
|
||||
map(lambda obj: obj.to_pydantic(), BlockModel.read_multiple(db_session=session, identifiers=block_ids, actor=actor))
|
||||
)
|
||||
# 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
|
||||
|
@ -129,6 +129,44 @@ class MessageManager:
|
||||
# raise error if message type got modified
|
||||
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
|
||||
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_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_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-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"},
|
||||
{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_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_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-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_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"},
|
||||
{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_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_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-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-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"},
|
||||
@ -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_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_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-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"},
|
||||
{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_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_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-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"},
|
||||
{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_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_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-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"},
|
||||
{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_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_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-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"},
|
||||
{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"
|
||||
packaging = "*"
|
||||
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.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"
|
||||
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\""}
|
||||
pyzmq = ">=25.0.0"
|
||||
requests = [
|
||||
{version = ">=2.26.0", 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"
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
@ -3899,9 +3857,9 @@ files = [
|
||||
|
||||
[package.dependencies]
|
||||
numpy = [
|
||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||
{version = ">=1.23.2", 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"
|
||||
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-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-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-win_amd64.whl", hash = "sha256:88138c8dedcbfa96408023ea2b0c369eda40fe5d75002c0964c78f46f11fa442"},
|
||||
{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_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-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-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"},
|
||||
@ -5192,8 +5148,8 @@ grpcio = ">=1.41.0"
|
||||
grpcio-tools = ">=1.41.0"
|
||||
httpx = {version = ">=0.20.0", extras = ["http2"]}
|
||||
numpy = [
|
||||
{version = ">=1.21", markers = "python_version >= \"3.10\" and 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"
|
||||
pydantic = ">=1.10.8"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "letta"
|
||||
version = "0.6.42"
|
||||
version = "0.6.43"
|
||||
packages = [
|
||||
{include = "letta"},
|
||||
]
|
||||
|
@ -96,7 +96,7 @@ def test_parse_number_cases(strict_parser):
|
||||
def test_parse_boolean_true(strict_parser):
|
||||
assert strict_parser.parse("true") is True, "Should parse 'true'."
|
||||
# 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):
|
||||
@ -246,3 +246,35 @@ def test_multiple_parse_calls(strict_parser):
|
||||
result_2 = strict_parser.parse(input_2)
|
||||
assert result_2 == [2, 3]
|
||||
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