mirror of
https://github.com/cpacker/MemGPT.git
synced 2025-06-03 04:30:22 +00:00
chore: clean up api (#2384)
Co-authored-by: Shubham Naik <shubham.naik10@gmail.com> Co-authored-by: Shubham Naik <shub@memgpt.ai> Co-authored-by: Matthew Zhou <mattzh1314@gmail.com> Co-authored-by: mlong93 <35275280+mlong93@users.noreply.github.com> Co-authored-by: Mindy Long <mindy@letta.com> Co-authored-by: Kevin Lin <klin5061@gmail.com> Co-authored-by: Charles Packer <packercharles@gmail.com>
This commit is contained in:
parent
44293191ec
commit
dbe58abe92
43
alembic/versions/6fbe9cace832_adding_indexes_to_models.py
Normal file
43
alembic/versions/6fbe9cace832_adding_indexes_to_models.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""adding indexes to models
|
||||
|
||||
Revision ID: 6fbe9cace832
|
||||
Revises: f895232c144a
|
||||
Create Date: 2025-01-23 11:02:59.534372
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "6fbe9cace832"
|
||||
down_revision: Union[str, None] = "f895232c144a"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_index("agent_passages_created_at_id_idx", "agent_passages", ["created_at", "id"], unique=False)
|
||||
op.create_index("ix_agents_created_at", "agents", ["created_at", "id"], unique=False)
|
||||
op.create_index("created_at_label_idx", "block", ["created_at", "label"], unique=False)
|
||||
op.create_index("ix_jobs_created_at", "jobs", ["created_at", "id"], unique=False)
|
||||
op.create_index("ix_messages_created_at", "messages", ["created_at", "id"], unique=False)
|
||||
op.create_index("source_passages_created_at_id_idx", "source_passages", ["created_at", "id"], unique=False)
|
||||
op.create_index("source_created_at_id_idx", "sources", ["created_at", "id"], unique=False)
|
||||
op.create_index("ix_tools_created_at_name", "tools", ["created_at", "name"], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index("ix_tools_created_at_name", table_name="tools")
|
||||
op.drop_index("source_created_at_id_idx", table_name="sources")
|
||||
op.drop_index("source_passages_created_at_id_idx", table_name="source_passages")
|
||||
op.drop_index("ix_messages_created_at", table_name="messages")
|
||||
op.drop_index("ix_jobs_created_at", table_name="jobs")
|
||||
op.drop_index("created_at_label_idx", table_name="block")
|
||||
op.drop_index("ix_agents_created_at", table_name="agents")
|
||||
op.drop_index("agent_passages_created_at_id_idx", table_name="agent_passages")
|
||||
# ### end Alembic commands ###
|
@ -46,7 +46,7 @@ response = client.agents.messages.send(
|
||||
messages=[
|
||||
MessageCreate(
|
||||
role="user",
|
||||
text="hello",
|
||||
content="hello",
|
||||
)
|
||||
],
|
||||
)
|
||||
@ -59,7 +59,7 @@ response = client.agents.messages.send(
|
||||
messages=[
|
||||
MessageCreate(
|
||||
role="system",
|
||||
text="[system] user has logged in. send a friendly message.",
|
||||
content="[system] user has logged in. send a friendly message.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
@ -29,7 +29,7 @@ response = client.agents.messages.send(
|
||||
messages=[
|
||||
MessageCreate(
|
||||
role="user",
|
||||
text="hello",
|
||||
content="hello",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
@ -43,7 +43,7 @@ def main():
|
||||
messages=[
|
||||
MessageCreate(
|
||||
role="user",
|
||||
text="Whats my name?",
|
||||
content="Whats my name?",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
@ -64,7 +64,7 @@ response = client.agents.messages.send(
|
||||
messages=[
|
||||
MessageCreate(
|
||||
role="user",
|
||||
text="roll a dice",
|
||||
content="roll a dice",
|
||||
)
|
||||
],
|
||||
)
|
||||
@ -100,7 +100,7 @@ client.agents.messages.send(
|
||||
messages=[
|
||||
MessageCreate(
|
||||
role="user",
|
||||
text="search your archival memory",
|
||||
content="search your archival memory",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
@ -246,7 +246,7 @@
|
||||
" messages=[\n",
|
||||
" MessageCreate(\n",
|
||||
" role=\"user\",\n",
|
||||
" text=\"Search archival for our company's vacation policies\",\n",
|
||||
" content=\"Search archival for our company's vacation policies\",\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
@ -528,7 +528,7 @@
|
||||
" messages=[\n",
|
||||
" MessageCreate(\n",
|
||||
" role=\"user\",\n",
|
||||
" text=\"When is my birthday?\",\n",
|
||||
" content=\"When is my birthday?\",\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
@ -814,7 +814,7 @@
|
||||
" messages=[\n",
|
||||
" MessageCreate(\n",
|
||||
" role=\"user\",\n",
|
||||
" text=\"Who founded OpenAI?\",\n",
|
||||
" content=\"Who founded OpenAI?\",\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
@ -952,7 +952,7 @@
|
||||
" messages=[\n",
|
||||
" MessageCreate(\n",
|
||||
" role=\"user\",\n",
|
||||
" text=\"Who founded OpenAI?\",\n",
|
||||
" content=\"Who founded OpenAI?\",\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
|
@ -169,7 +169,7 @@
|
||||
" messages=[\n",
|
||||
" MessageCreate(\n",
|
||||
" role=\"user\",\n",
|
||||
" text=\"hello!\",\n",
|
||||
" content=\"hello!\",\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
@ -529,7 +529,7 @@
|
||||
" messages=[\n",
|
||||
" MessageCreate(\n",
|
||||
" role=\"user\",\n",
|
||||
" text=\"My name is actually Bob\",\n",
|
||||
" content=\"My name is actually Bob\",\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
@ -682,7 +682,7 @@
|
||||
" messages=[\n",
|
||||
" MessageCreate(\n",
|
||||
" role=\"user\",\n",
|
||||
" text=\"In the future, never use emojis to communicate\",\n",
|
||||
" content=\"In the future, never use emojis to communicate\",\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
@ -870,7 +870,7 @@
|
||||
" messages=[\n",
|
||||
" MessageCreate(\n",
|
||||
" role=\"user\",\n",
|
||||
" text=\"Save the information that 'bob loves cats' to archival\",\n",
|
||||
" content=\"Save the information that 'bob loves cats' to archival\",\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
@ -1039,7 +1039,7 @@
|
||||
" messages=[\n",
|
||||
" MessageCreate(\n",
|
||||
" role=\"user\",\n",
|
||||
" text=\"What animals do I like? Search archival.\",\n",
|
||||
" content=\"What animals do I like? Search archival.\",\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
|
@ -276,7 +276,7 @@
|
||||
" messages=[\n",
|
||||
" MessageCreate(\n",
|
||||
" role=\"user\",\n",
|
||||
" text=\"Candidate: Tony Stark\",\n",
|
||||
" content=\"Candidate: Tony Stark\",\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
")"
|
||||
@ -403,7 +403,7 @@
|
||||
" messages=[\n",
|
||||
" MessageCreate(\n",
|
||||
" role=\"user\",\n",
|
||||
" text=feedback,\n",
|
||||
" content=feedback,\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
")"
|
||||
@ -423,7 +423,7 @@
|
||||
" messages=[\n",
|
||||
" MessageCreate(\n",
|
||||
" role=\"user\",\n",
|
||||
" text=feedback,\n",
|
||||
" content=feedback,\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
")"
|
||||
@ -540,7 +540,7 @@
|
||||
" messages=[\n",
|
||||
" MessageCreate(\n",
|
||||
" role=\"system\",\n",
|
||||
" text=\"Candidate: Spongebob Squarepants\",\n",
|
||||
" content=\"Candidate: Spongebob Squarepants\",\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
")"
|
||||
@ -758,7 +758,7 @@
|
||||
" messages=[\n",
|
||||
" MessageCreate(\n",
|
||||
" role=\"system\",\n",
|
||||
" text=\"Run generation\",\n",
|
||||
" content=\"Run generation\",\n",
|
||||
" )\n",
|
||||
" ],\n",
|
||||
")"
|
||||
|
@ -206,7 +206,7 @@ class AbstractClient(object):
|
||||
) -> Tool:
|
||||
raise NotImplementedError
|
||||
|
||||
def list_tools(self, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[Tool]:
|
||||
def list_tools(self, after: Optional[str] = None, limit: Optional[int] = 50) -> List[Tool]:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_tool(self, id: str) -> Tool:
|
||||
@ -266,7 +266,7 @@ class AbstractClient(object):
|
||||
def list_attached_sources(self, agent_id: str) -> List[Source]:
|
||||
raise NotImplementedError
|
||||
|
||||
def list_files_from_source(self, source_id: str, limit: int = 1000, cursor: Optional[str] = None) -> List[FileMetadata]:
|
||||
def list_files_from_source(self, source_id: str, limit: int = 1000, after: Optional[str] = None) -> List[FileMetadata]:
|
||||
raise NotImplementedError
|
||||
|
||||
def update_source(self, source_id: str, name: Optional[str] = None) -> Source:
|
||||
@ -279,12 +279,12 @@ class AbstractClient(object):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_archival_memory(
|
||||
self, agent_id: str, before: Optional[str] = None, after: Optional[str] = None, limit: Optional[int] = 1000
|
||||
self, agent_id: str, after: Optional[str] = None, before: Optional[str] = None, limit: Optional[int] = 1000
|
||||
) -> List[Passage]:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_messages(
|
||||
self, agent_id: str, before: Optional[str] = None, after: Optional[str] = None, limit: Optional[int] = 1000
|
||||
self, agent_id: str, after: Optional[str] = None, before: Optional[str] = None, limit: Optional[int] = 1000
|
||||
) -> List[Message]:
|
||||
raise NotImplementedError
|
||||
|
||||
@ -297,7 +297,7 @@ class AbstractClient(object):
|
||||
def create_org(self, name: Optional[str] = None) -> Organization:
|
||||
raise NotImplementedError
|
||||
|
||||
def list_orgs(self, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[Organization]:
|
||||
def list_orgs(self, after: Optional[str] = None, limit: Optional[int] = 50) -> List[Organization]:
|
||||
raise NotImplementedError
|
||||
|
||||
def delete_org(self, org_id: str) -> Organization:
|
||||
@ -337,13 +337,13 @@ class AbstractClient(object):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def list_sandbox_configs(self, limit: int = 50, cursor: Optional[str] = None) -> List[SandboxConfig]:
|
||||
def list_sandbox_configs(self, limit: int = 50, after: Optional[str] = None) -> List[SandboxConfig]:
|
||||
"""
|
||||
List all sandbox configurations.
|
||||
|
||||
Args:
|
||||
limit (int, optional): The maximum number of sandbox configurations to return. Defaults to 50.
|
||||
cursor (Optional[str], optional): The pagination cursor for retrieving the next set of results.
|
||||
after (Optional[str], optional): The pagination cursor for retrieving the next set of results.
|
||||
|
||||
Returns:
|
||||
List[SandboxConfig]: A list of sandbox configurations.
|
||||
@ -394,7 +394,7 @@ class AbstractClient(object):
|
||||
raise NotImplementedError
|
||||
|
||||
def list_sandbox_env_vars(
|
||||
self, sandbox_config_id: str, limit: int = 50, cursor: Optional[str] = None
|
||||
self, sandbox_config_id: str, limit: int = 50, after: Optional[str] = None
|
||||
) -> List[SandboxEnvironmentVariable]:
|
||||
"""
|
||||
List all environment variables associated with a sandbox configuration.
|
||||
@ -402,7 +402,7 @@ class AbstractClient(object):
|
||||
Args:
|
||||
sandbox_config_id (str): The ID of the sandbox configuration to retrieve environment variables for.
|
||||
limit (int, optional): The maximum number of environment variables to return. Defaults to 50.
|
||||
cursor (Optional[str], optional): The pagination cursor for retrieving the next set of results.
|
||||
after (Optional[str], optional): The pagination cursor for retrieving the next set of results.
|
||||
|
||||
Returns:
|
||||
List[SandboxEnvironmentVariable]: A list of environment variables.
|
||||
@ -477,7 +477,12 @@ class RESTClient(AbstractClient):
|
||||
self._default_embedding_config = default_embedding_config
|
||||
|
||||
def list_agents(
|
||||
self, tags: Optional[List[str]] = None, query_text: Optional[str] = None, limit: int = 50, cursor: Optional[str] = None
|
||||
self,
|
||||
tags: Optional[List[str]] = None,
|
||||
query_text: Optional[str] = None,
|
||||
limit: int = 50,
|
||||
before: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
) -> List[AgentState]:
|
||||
params = {"limit": limit}
|
||||
if tags:
|
||||
@ -487,11 +492,13 @@ class RESTClient(AbstractClient):
|
||||
if query_text:
|
||||
params["query_text"] = query_text
|
||||
|
||||
if cursor:
|
||||
params["cursor"] = cursor
|
||||
if before:
|
||||
params["before"] = before
|
||||
|
||||
if after:
|
||||
params["after"] = after
|
||||
|
||||
response = requests.get(f"{self.base_url}/{self.api_prefix}/agents", headers=self.headers, params=params)
|
||||
print(f"\nLIST RESPONSE\n{response.json()}\n")
|
||||
return [AgentState(**agent) for agent in response.json()]
|
||||
|
||||
def agent_exists(self, agent_id: str) -> bool:
|
||||
@ -636,7 +643,7 @@ class RESTClient(AbstractClient):
|
||||
) -> Message:
|
||||
request = MessageUpdate(
|
||||
role=role,
|
||||
text=text,
|
||||
content=text,
|
||||
name=name,
|
||||
tool_calls=tool_calls,
|
||||
tool_call_id=tool_call_id,
|
||||
@ -1009,7 +1016,7 @@ class RESTClient(AbstractClient):
|
||||
response (LettaResponse): Response from the agent
|
||||
"""
|
||||
# TODO: implement include_full_message
|
||||
messages = [MessageCreate(role=MessageRole(role), text=message, name=name)]
|
||||
messages = [MessageCreate(role=MessageRole(role), content=message, name=name)]
|
||||
# TODO: figure out how to handle stream_steps and stream_tokens
|
||||
|
||||
# When streaming steps is True, stream_tokens must be False
|
||||
@ -1056,7 +1063,7 @@ class RESTClient(AbstractClient):
|
||||
Returns:
|
||||
job (Job): Information about the async job
|
||||
"""
|
||||
messages = [MessageCreate(role=MessageRole(role), text=message, name=name)]
|
||||
messages = [MessageCreate(role=MessageRole(role), content=message, name=name)]
|
||||
|
||||
request = LettaRequest(messages=messages)
|
||||
response = requests.post(
|
||||
@ -1359,7 +1366,7 @@ class RESTClient(AbstractClient):
|
||||
def load_data(self, connector: DataConnector, source_name: str):
|
||||
raise NotImplementedError
|
||||
|
||||
def load_file_to_source(self, filename: str, source_id: str, blocking=True):
|
||||
def load_file_to_source(self, filename: str, source_id: str, blocking=True) -> Job:
|
||||
"""
|
||||
Load a file into a source
|
||||
|
||||
@ -1427,20 +1434,20 @@ class RESTClient(AbstractClient):
|
||||
raise ValueError(f"Failed to list attached sources: {response.text}")
|
||||
return [Source(**source) for source in response.json()]
|
||||
|
||||
def list_files_from_source(self, source_id: str, limit: int = 1000, cursor: Optional[str] = None) -> List[FileMetadata]:
|
||||
def list_files_from_source(self, source_id: str, limit: int = 1000, after: Optional[str] = None) -> List[FileMetadata]:
|
||||
"""
|
||||
List files from source with pagination support.
|
||||
|
||||
Args:
|
||||
source_id (str): ID of the source
|
||||
limit (int): Number of files to return
|
||||
cursor (Optional[str]): Pagination cursor for fetching the next page
|
||||
after (str): Get files after a certain time
|
||||
|
||||
Returns:
|
||||
List[FileMetadata]: List of files
|
||||
"""
|
||||
# Prepare query parameters for pagination
|
||||
params = {"limit": limit, "cursor": cursor}
|
||||
params = {"limit": limit, "after": after}
|
||||
|
||||
# Make the request to the FastAPI endpoint
|
||||
response = requests.get(f"{self.base_url}/{self.api_prefix}/sources/{source_id}/files", headers=self.headers, params=params)
|
||||
@ -1640,7 +1647,7 @@ class RESTClient(AbstractClient):
|
||||
raise ValueError(f"Failed to update tool: {response.text}")
|
||||
return Tool(**response.json())
|
||||
|
||||
def list_tools(self, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[Tool]:
|
||||
def list_tools(self, after: Optional[str] = None, limit: Optional[int] = 50) -> List[Tool]:
|
||||
"""
|
||||
List available tools for the user.
|
||||
|
||||
@ -1648,8 +1655,8 @@ class RESTClient(AbstractClient):
|
||||
tools (List[Tool]): List of tools
|
||||
"""
|
||||
params = {}
|
||||
if cursor:
|
||||
params["cursor"] = str(cursor)
|
||||
if after:
|
||||
params["after"] = after
|
||||
if limit:
|
||||
params["limit"] = limit
|
||||
|
||||
@ -1728,15 +1735,15 @@ class RESTClient(AbstractClient):
|
||||
raise ValueError(f"Failed to list embedding configs: {response.text}")
|
||||
return [EmbeddingConfig(**config) for config in response.json()]
|
||||
|
||||
def list_orgs(self, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[Organization]:
|
||||
def list_orgs(self, after: Optional[str] = None, limit: Optional[int] = 50) -> List[Organization]:
|
||||
"""
|
||||
Retrieves a list of all organizations in the database, with optional pagination.
|
||||
|
||||
@param cursor: the pagination cursor, if any
|
||||
@param after: the pagination cursor, if any
|
||||
@param limit: the maximum number of organizations to retrieve
|
||||
@return: a list of Organization objects
|
||||
"""
|
||||
params = {"cursor": cursor, "limit": limit}
|
||||
params = {"after": after, "limit": limit}
|
||||
response = requests.get(f"{self.base_url}/{ADMIN_PREFIX}/orgs", headers=self.headers, params=params)
|
||||
if response.status_code != 200:
|
||||
raise ValueError(f"Failed to retrieve organizations: {response.text}")
|
||||
@ -1779,6 +1786,12 @@ class RESTClient(AbstractClient):
|
||||
def create_sandbox_config(self, config: Union[LocalSandboxConfig, E2BSandboxConfig]) -> SandboxConfig:
|
||||
"""
|
||||
Create a new sandbox configuration.
|
||||
|
||||
Args:
|
||||
config (Union[LocalSandboxConfig, E2BSandboxConfig]): The sandbox settings.
|
||||
|
||||
Returns:
|
||||
SandboxConfig: The created sandbox configuration.
|
||||
"""
|
||||
payload = {
|
||||
"config": config.model_dump(),
|
||||
@ -1791,6 +1804,13 @@ class RESTClient(AbstractClient):
|
||||
def update_sandbox_config(self, sandbox_config_id: str, config: Union[LocalSandboxConfig, E2BSandboxConfig]) -> SandboxConfig:
|
||||
"""
|
||||
Update an existing sandbox configuration.
|
||||
|
||||
Args:
|
||||
sandbox_config_id (str): The ID of the sandbox configuration to update.
|
||||
config (Union[LocalSandboxConfig, E2BSandboxConfig]): The updated sandbox settings.
|
||||
|
||||
Returns:
|
||||
SandboxConfig: The updated sandbox configuration.
|
||||
"""
|
||||
payload = {
|
||||
"config": config.model_dump(),
|
||||
@ -1807,6 +1827,9 @@ class RESTClient(AbstractClient):
|
||||
def delete_sandbox_config(self, sandbox_config_id: str) -> None:
|
||||
"""
|
||||
Delete a sandbox configuration.
|
||||
|
||||
Args:
|
||||
sandbox_config_id (str): The ID of the sandbox configuration to delete.
|
||||
"""
|
||||
response = requests.delete(f"{self.base_url}/{self.api_prefix}/sandbox-config/{sandbox_config_id}", headers=self.headers)
|
||||
if response.status_code == 404:
|
||||
@ -1814,11 +1837,18 @@ class RESTClient(AbstractClient):
|
||||
elif response.status_code != 204:
|
||||
raise ValueError(f"Failed to delete sandbox config with ID '{sandbox_config_id}': {response.text}")
|
||||
|
||||
def list_sandbox_configs(self, limit: int = 50, cursor: Optional[str] = None) -> List[SandboxConfig]:
|
||||
def list_sandbox_configs(self, limit: int = 50, after: Optional[str] = None) -> List[SandboxConfig]:
|
||||
"""
|
||||
List all sandbox configurations.
|
||||
|
||||
Args:
|
||||
limit (int, optional): The maximum number of sandbox configurations to return. Defaults to 50.
|
||||
after (Optional[str], optional): The pagination cursor for retrieving the next set of results.
|
||||
|
||||
Returns:
|
||||
List[SandboxConfig]: A list of sandbox configurations.
|
||||
"""
|
||||
params = {"limit": limit, "cursor": cursor}
|
||||
params = {"limit": limit, "after": after}
|
||||
response = requests.get(f"{self.base_url}/{self.api_prefix}/sandbox-config", headers=self.headers, params=params)
|
||||
if response.status_code != 200:
|
||||
raise ValueError(f"Failed to list sandbox configs: {response.text}")
|
||||
@ -1829,6 +1859,15 @@ class RESTClient(AbstractClient):
|
||||
) -> SandboxEnvironmentVariable:
|
||||
"""
|
||||
Create a new environment variable for a sandbox configuration.
|
||||
|
||||
Args:
|
||||
sandbox_config_id (str): The ID of the sandbox configuration to associate the environment variable with.
|
||||
key (str): The name of the environment variable.
|
||||
value (str): The value of the environment variable.
|
||||
description (Optional[str], optional): A description of the environment variable. Defaults to None.
|
||||
|
||||
Returns:
|
||||
SandboxEnvironmentVariable: The created environment variable.
|
||||
"""
|
||||
payload = {"key": key, "value": value, "description": description}
|
||||
response = requests.post(
|
||||
@ -1845,6 +1884,15 @@ class RESTClient(AbstractClient):
|
||||
) -> SandboxEnvironmentVariable:
|
||||
"""
|
||||
Update an existing environment variable.
|
||||
|
||||
Args:
|
||||
env_var_id (str): The ID of the environment variable to update.
|
||||
key (Optional[str], optional): The updated name of the environment variable. Defaults to None.
|
||||
value (Optional[str], optional): The updated value of the environment variable. Defaults to None.
|
||||
description (Optional[str], optional): The updated description of the environment variable. Defaults to None.
|
||||
|
||||
Returns:
|
||||
SandboxEnvironmentVariable: The updated environment variable.
|
||||
"""
|
||||
payload = {k: v for k, v in {"key": key, "value": value, "description": description}.items() if v is not None}
|
||||
response = requests.patch(
|
||||
@ -1859,6 +1907,9 @@ class RESTClient(AbstractClient):
|
||||
def delete_sandbox_env_var(self, env_var_id: str) -> None:
|
||||
"""
|
||||
Delete an environment variable by its ID.
|
||||
|
||||
Args:
|
||||
env_var_id (str): The ID of the environment variable to delete.
|
||||
"""
|
||||
response = requests.delete(
|
||||
f"{self.base_url}/{self.api_prefix}/sandbox-config/environment-variable/{env_var_id}", headers=self.headers
|
||||
@ -1869,12 +1920,20 @@ class RESTClient(AbstractClient):
|
||||
raise ValueError(f"Failed to delete environment variable with ID '{env_var_id}': {response.text}")
|
||||
|
||||
def list_sandbox_env_vars(
|
||||
self, sandbox_config_id: str, limit: int = 50, cursor: Optional[str] = None
|
||||
self, sandbox_config_id: str, limit: int = 50, after: Optional[str] = None
|
||||
) -> List[SandboxEnvironmentVariable]:
|
||||
"""
|
||||
List all environment variables associated with a sandbox configuration.
|
||||
|
||||
Args:
|
||||
sandbox_config_id (str): The ID of the sandbox configuration to retrieve environment variables for.
|
||||
limit (int, optional): The maximum number of environment variables to return. Defaults to 50.
|
||||
after (Optional[str], optional): The pagination cursor for retrieving the next set of results.
|
||||
|
||||
Returns:
|
||||
List[SandboxEnvironmentVariable]: A list of environment variables.
|
||||
"""
|
||||
params = {"limit": limit, "cursor": cursor}
|
||||
params = {"limit": limit, "after": after}
|
||||
response = requests.get(
|
||||
f"{self.base_url}/{self.api_prefix}/sandbox-config/{sandbox_config_id}/environment-variable",
|
||||
headers=self.headers,
|
||||
@ -2035,7 +2094,8 @@ class RESTClient(AbstractClient):
|
||||
def get_run_messages(
|
||||
self,
|
||||
run_id: str,
|
||||
cursor: Optional[str] = None,
|
||||
before: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
limit: Optional[int] = 100,
|
||||
ascending: bool = True,
|
||||
role: Optional[MessageRole] = None,
|
||||
@ -2045,7 +2105,8 @@ class RESTClient(AbstractClient):
|
||||
|
||||
Args:
|
||||
job_id: ID of the job
|
||||
cursor: Cursor for pagination
|
||||
before: Cursor for pagination
|
||||
after: Cursor for pagination
|
||||
limit: Maximum number of messages to return
|
||||
ascending: Sort order by creation time
|
||||
role: Filter by message role (user/assistant/system/tool)
|
||||
@ -2053,7 +2114,8 @@ class RESTClient(AbstractClient):
|
||||
List of messages matching the filter criteria
|
||||
"""
|
||||
params = {
|
||||
"cursor": cursor,
|
||||
"before": before,
|
||||
"after": after,
|
||||
"limit": limit,
|
||||
"ascending": ascending,
|
||||
"role": role,
|
||||
@ -2151,15 +2213,15 @@ class RESTClient(AbstractClient):
|
||||
|
||||
def get_tags(
|
||||
self,
|
||||
cursor: Optional[str] = None,
|
||||
limit: Optional[int] = None,
|
||||
after: Optional[str] = None,
|
||||
limit: int = 100,
|
||||
query_text: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
"""
|
||||
Get a list of all unique tags.
|
||||
|
||||
Args:
|
||||
cursor: Optional cursor for pagination (last tag seen)
|
||||
after: Optional cursor for pagination (first tag seen)
|
||||
limit: Optional maximum number of tags to return
|
||||
query_text: Optional text to filter tags
|
||||
|
||||
@ -2167,8 +2229,8 @@ class RESTClient(AbstractClient):
|
||||
List[str]: List of unique tags
|
||||
"""
|
||||
params = {}
|
||||
if cursor:
|
||||
params["cursor"] = cursor
|
||||
if after:
|
||||
params["after"] = after
|
||||
if limit:
|
||||
params["limit"] = limit
|
||||
if query_text:
|
||||
@ -2238,11 +2300,18 @@ class LocalClient(AbstractClient):
|
||||
|
||||
# agents
|
||||
def list_agents(
|
||||
self, query_text: Optional[str] = None, tags: Optional[List[str]] = None, limit: int = 100, cursor: Optional[str] = None
|
||||
self,
|
||||
query_text: Optional[str] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
limit: int = 100,
|
||||
before: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
) -> List[AgentState]:
|
||||
self.interface.clear()
|
||||
|
||||
return self.server.agent_manager.list_agents(actor=self.user, tags=tags, query_text=query_text, limit=limit, cursor=cursor)
|
||||
return self.server.agent_manager.list_agents(
|
||||
actor=self.user, tags=tags, query_text=query_text, limit=limit, before=before, after=after
|
||||
)
|
||||
|
||||
def agent_exists(self, agent_id: Optional[str] = None, agent_name: Optional[str] = None) -> bool:
|
||||
"""
|
||||
@ -2374,7 +2443,7 @@ class LocalClient(AbstractClient):
|
||||
message_id=message_id,
|
||||
request=MessageUpdate(
|
||||
role=role,
|
||||
text=text,
|
||||
content=text,
|
||||
name=name,
|
||||
tool_calls=tool_calls,
|
||||
tool_call_id=tool_call_id,
|
||||
@ -2673,7 +2742,7 @@ class LocalClient(AbstractClient):
|
||||
usage = self.server.send_messages(
|
||||
actor=self.user,
|
||||
agent_id=agent_id,
|
||||
messages=[MessageCreate(role=MessageRole(role), text=message, name=name)],
|
||||
messages=[MessageCreate(role=MessageRole(role), content=message, name=name)],
|
||||
)
|
||||
|
||||
## TODO: need to make sure date/timestamp is propely passed
|
||||
@ -2990,7 +3059,7 @@ class LocalClient(AbstractClient):
|
||||
id: str,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
func: Optional[callable] = None,
|
||||
func: Optional[Callable] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
return_char_limit: int = FUNCTION_RETURN_CHAR_LIMIT,
|
||||
) -> Tool:
|
||||
@ -3021,14 +3090,14 @@ class LocalClient(AbstractClient):
|
||||
|
||||
return self.server.tool_manager.update_tool_by_id(tool_id=id, tool_update=ToolUpdate(**update_data), actor=self.user)
|
||||
|
||||
def list_tools(self, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[Tool]:
|
||||
def list_tools(self, after: Optional[str] = None, limit: Optional[int] = 50) -> List[Tool]:
|
||||
"""
|
||||
List available tools for the user.
|
||||
|
||||
Returns:
|
||||
tools (List[Tool]): List of tools
|
||||
"""
|
||||
return self.server.tool_manager.list_tools(cursor=cursor, limit=limit, actor=self.user)
|
||||
return self.server.tool_manager.list_tools(after=after, limit=limit, actor=self.user)
|
||||
|
||||
def get_tool(self, id: str) -> Optional[Tool]:
|
||||
"""
|
||||
@ -3227,19 +3296,19 @@ class LocalClient(AbstractClient):
|
||||
"""
|
||||
return self.server.agent_manager.list_attached_sources(agent_id=agent_id, actor=self.user)
|
||||
|
||||
def list_files_from_source(self, source_id: str, limit: int = 1000, cursor: Optional[str] = None) -> List[FileMetadata]:
|
||||
def list_files_from_source(self, source_id: str, limit: int = 1000, after: Optional[str] = None) -> List[FileMetadata]:
|
||||
"""
|
||||
List files from source.
|
||||
|
||||
Args:
|
||||
source_id (str): ID of the source
|
||||
limit (int): The # of items to return
|
||||
cursor (str): The cursor for fetching the next page
|
||||
after (str): The cursor for fetching the next page
|
||||
|
||||
Returns:
|
||||
files (List[FileMetadata]): List of files
|
||||
"""
|
||||
return self.server.source_manager.list_files(source_id=source_id, limit=limit, cursor=cursor, actor=self.user)
|
||||
return self.server.source_manager.list_files(source_id=source_id, limit=limit, after=after, actor=self.user)
|
||||
|
||||
def update_source(self, source_id: str, name: Optional[str] = None) -> Source:
|
||||
"""
|
||||
@ -3297,17 +3366,20 @@ class LocalClient(AbstractClient):
|
||||
passages (List[Passage]): List of passages
|
||||
"""
|
||||
|
||||
return self.server.get_agent_archival_cursor(user_id=self.user_id, agent_id=agent_id, limit=limit)
|
||||
return self.server.get_agent_archival(user_id=self.user_id, agent_id=agent_id, limit=limit)
|
||||
|
||||
# recall memory
|
||||
|
||||
def get_messages(self, agent_id: str, cursor: Optional[str] = None, limit: Optional[int] = 1000) -> List[Message]:
|
||||
def get_messages(
|
||||
self, agent_id: str, before: Optional[str] = None, after: Optional[str] = None, limit: Optional[int] = 1000
|
||||
) -> List[Message]:
|
||||
"""
|
||||
Get messages from an agent with pagination.
|
||||
|
||||
Args:
|
||||
agent_id (str): ID of the agent
|
||||
cursor (str): Get messages after a certain time
|
||||
before (str): Get messages before a certain time
|
||||
after (str): Get messages after a certain time
|
||||
limit (int): Limit number of messages
|
||||
|
||||
Returns:
|
||||
@ -3315,10 +3387,11 @@ class LocalClient(AbstractClient):
|
||||
"""
|
||||
|
||||
self.interface.clear()
|
||||
return self.server.get_agent_recall_cursor(
|
||||
return self.server.get_agent_recall(
|
||||
user_id=self.user_id,
|
||||
agent_id=agent_id,
|
||||
before=cursor,
|
||||
before=before,
|
||||
after=after,
|
||||
limit=limit,
|
||||
reverse=True,
|
||||
)
|
||||
@ -3437,8 +3510,8 @@ class LocalClient(AbstractClient):
|
||||
def create_org(self, name: Optional[str] = None) -> Organization:
|
||||
return self.server.organization_manager.create_organization(pydantic_org=Organization(name=name))
|
||||
|
||||
def list_orgs(self, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[Organization]:
|
||||
return self.server.organization_manager.list_organizations(cursor=cursor, limit=limit)
|
||||
def list_orgs(self, after: Optional[str] = None, limit: Optional[int] = 50) -> List[Organization]:
|
||||
return self.server.organization_manager.list_organizations(limit=limit, after=after)
|
||||
|
||||
def delete_org(self, org_id: str) -> Organization:
|
||||
return self.server.organization_manager.delete_organization_by_id(org_id=org_id)
|
||||
@ -3465,11 +3538,11 @@ class LocalClient(AbstractClient):
|
||||
"""
|
||||
return self.server.sandbox_config_manager.delete_sandbox_config(sandbox_config_id=sandbox_config_id, actor=self.user)
|
||||
|
||||
def list_sandbox_configs(self, limit: int = 50, cursor: Optional[str] = None) -> List[SandboxConfig]:
|
||||
def list_sandbox_configs(self, limit: int = 50, after: Optional[str] = None) -> List[SandboxConfig]:
|
||||
"""
|
||||
List all sandbox configurations.
|
||||
"""
|
||||
return self.server.sandbox_config_manager.list_sandbox_configs(actor=self.user, limit=limit, cursor=cursor)
|
||||
return self.server.sandbox_config_manager.list_sandbox_configs(actor=self.user, limit=limit, after=after)
|
||||
|
||||
def create_sandbox_env_var(
|
||||
self, sandbox_config_id: str, key: str, value: str, description: Optional[str] = None
|
||||
@ -3500,13 +3573,13 @@ class LocalClient(AbstractClient):
|
||||
return self.server.sandbox_config_manager.delete_sandbox_env_var(env_var_id=env_var_id, actor=self.user)
|
||||
|
||||
def list_sandbox_env_vars(
|
||||
self, sandbox_config_id: str, limit: int = 50, cursor: Optional[str] = None
|
||||
self, sandbox_config_id: str, limit: int = 50, after: Optional[str] = None
|
||||
) -> List[SandboxEnvironmentVariable]:
|
||||
"""
|
||||
List all environment variables associated with a sandbox configuration.
|
||||
"""
|
||||
return self.server.sandbox_config_manager.list_sandbox_env_vars(
|
||||
sandbox_config_id=sandbox_config_id, actor=self.user, limit=limit, cursor=cursor
|
||||
sandbox_config_id=sandbox_config_id, actor=self.user, limit=limit, after=after
|
||||
)
|
||||
|
||||
def update_agent_memory_block_label(self, agent_id: str, current_label: str, new_label: str) -> Memory:
|
||||
@ -3627,7 +3700,8 @@ class LocalClient(AbstractClient):
|
||||
def get_run_messages(
|
||||
self,
|
||||
run_id: str,
|
||||
cursor: Optional[str] = None,
|
||||
before: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
limit: Optional[int] = 100,
|
||||
ascending: bool = True,
|
||||
role: Optional[MessageRole] = None,
|
||||
@ -3637,21 +3711,23 @@ class LocalClient(AbstractClient):
|
||||
|
||||
Args:
|
||||
run_id: ID of the run
|
||||
cursor: Cursor for pagination
|
||||
before: Cursor for pagination
|
||||
after: Cursor for pagination
|
||||
limit: Maximum number of messages to return
|
||||
ascending: Sort order by creation time
|
||||
role: Filter by message role (user/assistant/system/tool)
|
||||
|
||||
Returns:
|
||||
List of messages matching the filter criteria
|
||||
"""
|
||||
params = {
|
||||
"cursor": cursor,
|
||||
"before": before,
|
||||
"after": after,
|
||||
"limit": limit,
|
||||
"ascending": ascending,
|
||||
"role": role,
|
||||
}
|
||||
return self.server.job_manager.get_run_messages_cursor(run_id=run_id, actor=self.user, **params)
|
||||
|
||||
return self.server.job_manager.get_run_messages(run_id=run_id, actor=self.user, **params)
|
||||
|
||||
def get_run_usage(
|
||||
self,
|
||||
@ -3713,9 +3789,9 @@ class LocalClient(AbstractClient):
|
||||
|
||||
def get_tags(
|
||||
self,
|
||||
cursor: str = None,
|
||||
limit: int = 100,
|
||||
query_text: str = None,
|
||||
after: Optional[str] = None,
|
||||
limit: Optional[int] = None,
|
||||
query_text: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
"""
|
||||
Get all tags.
|
||||
@ -3723,4 +3799,4 @@ class LocalClient(AbstractClient):
|
||||
Returns:
|
||||
tags (List[str]): List of tags
|
||||
"""
|
||||
return self.server.agent_manager.list_tags(actor=self.user, cursor=cursor, limit=limit, query_text=query_text)
|
||||
return self.server.agent_manager.list_tags(actor=self.user, after=after, limit=limit, query_text=query_text)
|
||||
|
@ -50,7 +50,7 @@ def _sse_post(url: str, data: dict, headers: dict) -> Generator[LettaStreamingRe
|
||||
chunk_data = json.loads(sse.data)
|
||||
if "reasoning" in chunk_data:
|
||||
yield ReasoningMessage(**chunk_data)
|
||||
elif "assistant_message" in chunk_data:
|
||||
elif "message_type" in chunk_data and chunk_data["message_type"] == "assistant_message":
|
||||
yield AssistantMessage(**chunk_data)
|
||||
elif "tool_call" in chunk_data:
|
||||
yield ToolCallMessage(**chunk_data)
|
||||
|
@ -6,7 +6,7 @@ import requests
|
||||
|
||||
from letta.constants import MESSAGE_CHATGPT_FUNCTION_MODEL, MESSAGE_CHATGPT_FUNCTION_SYSTEM_MESSAGE
|
||||
from letta.llm_api.llm_api_tools import create
|
||||
from letta.schemas.message import Message
|
||||
from letta.schemas.message import Message, TextContent
|
||||
from letta.utils import json_dumps, json_loads
|
||||
|
||||
|
||||
@ -23,8 +23,13 @@ def message_chatgpt(self, message: str):
|
||||
dummy_user_id = uuid.uuid4()
|
||||
dummy_agent_id = uuid.uuid4()
|
||||
message_sequence = [
|
||||
Message(user_id=dummy_user_id, agent_id=dummy_agent_id, role="system", text=MESSAGE_CHATGPT_FUNCTION_SYSTEM_MESSAGE),
|
||||
Message(user_id=dummy_user_id, agent_id=dummy_agent_id, role="user", text=str(message)),
|
||||
Message(
|
||||
user_id=dummy_user_id,
|
||||
agent_id=dummy_agent_id,
|
||||
role="system",
|
||||
content=[TextContent(text=MESSAGE_CHATGPT_FUNCTION_SYSTEM_MESSAGE)],
|
||||
),
|
||||
Message(user_id=dummy_user_id, agent_id=dummy_agent_id, role="user", content=[TextContent(text=str(message))]),
|
||||
]
|
||||
# TODO: this will error without an LLMConfig
|
||||
response = create(
|
||||
|
@ -74,7 +74,7 @@ def send_message_to_agents_matching_all_tags(self: "Agent", message: str, tags:
|
||||
server = get_letta_server()
|
||||
|
||||
# Retrieve agents that match ALL specified tags
|
||||
matching_agents = server.agent_manager.list_agents(actor=self.user, tags=tags, match_all_tags=True, cursor=None, limit=100)
|
||||
matching_agents = server.agent_manager.list_agents(actor=self.user, tags=tags, match_all_tags=True, limit=100)
|
||||
|
||||
async def send_messages_to_all_agents():
|
||||
tasks = [
|
||||
|
@ -246,7 +246,7 @@ def parse_letta_response_for_assistant_message(
|
||||
reasoning_message = ""
|
||||
for m in letta_response.messages:
|
||||
if isinstance(m, AssistantMessage):
|
||||
return m.assistant_message
|
||||
return m.content
|
||||
elif isinstance(m, ToolCallMessage) and m.tool_call.name == assistant_message_tool_name:
|
||||
try:
|
||||
return json.loads(m.tool_call.arguments)[assistant_message_tool_kwarg]
|
||||
@ -290,7 +290,7 @@ async def async_send_message_with_retries(
|
||||
logging_prefix = logging_prefix or "[async_send_message_with_retries]"
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
messages = [MessageCreate(role=MessageRole.user, text=message_text, name=sender_agent.agent_state.name)]
|
||||
messages = [MessageCreate(role=MessageRole.user, content=message_text, name=sender_agent.agent_state.name)]
|
||||
# Wrap in a timeout
|
||||
response = await asyncio.wait_for(
|
||||
server.send_message_to_agent(
|
||||
|
@ -237,6 +237,7 @@ def create(
|
||||
data=dict(
|
||||
contents=[m.to_google_ai_dict() for m in messages],
|
||||
tools=tools,
|
||||
generation_config={"temperature": llm_config.temperature},
|
||||
),
|
||||
inner_thoughts_in_kwargs=llm_config.put_inner_thoughts_in_kwargs,
|
||||
)
|
||||
@ -261,6 +262,7 @@ def create(
|
||||
# user=str(user_id),
|
||||
# NOTE: max_tokens is required for Anthropic API
|
||||
max_tokens=1024, # TODO make dynamic
|
||||
temperature=llm_config.temperature,
|
||||
),
|
||||
)
|
||||
|
||||
@ -290,7 +292,6 @@ def create(
|
||||
# # max_tokens=1024, # TODO make dynamic
|
||||
# ),
|
||||
# )
|
||||
|
||||
elif llm_config.model_endpoint_type == "groq":
|
||||
if stream:
|
||||
raise NotImplementedError(f"Streaming not yet implemented for Groq.")
|
||||
@ -329,7 +330,6 @@ def create(
|
||||
try:
|
||||
# groq uses the openai chat completions API, so this component should be reusable
|
||||
response = openai_chat_completions_request(
|
||||
url=llm_config.model_endpoint,
|
||||
api_key=model_settings.groq_api_key,
|
||||
chat_completion_request=data,
|
||||
)
|
||||
|
@ -1,14 +1,9 @@
|
||||
import json
|
||||
import warnings
|
||||
from typing import Generator, List, Optional, Union
|
||||
|
||||
import httpx
|
||||
import requests
|
||||
from httpx_sse import connect_sse
|
||||
from httpx_sse._exceptions import SSEError
|
||||
from openai import OpenAI
|
||||
|
||||
from letta.constants import OPENAI_CONTEXT_WINDOW_ERROR_SUBSTRING
|
||||
from letta.errors import LLMError
|
||||
from letta.llm_api.helpers import add_inner_thoughts_to_functions, convert_to_structured_output, make_post_request
|
||||
from letta.local_llm.constants import INNER_THOUGHTS_KWARG, INNER_THOUGHTS_KWARG_DESCRIPTION
|
||||
from letta.local_llm.utils import num_tokens_from_functions, num_tokens_from_messages
|
||||
@ -130,7 +125,8 @@ def build_openai_chat_completions_request(
|
||||
tools=[Tool(type="function", function=f) for f in functions] if functions else None,
|
||||
tool_choice=tool_choice,
|
||||
user=str(user_id),
|
||||
max_tokens=max_tokens,
|
||||
max_completion_tokens=max_tokens,
|
||||
temperature=llm_config.temperature,
|
||||
)
|
||||
else:
|
||||
data = ChatCompletionRequest(
|
||||
@ -139,7 +135,8 @@ def build_openai_chat_completions_request(
|
||||
functions=functions,
|
||||
function_call=function_call,
|
||||
user=str(user_id),
|
||||
max_tokens=max_tokens,
|
||||
max_completion_tokens=max_tokens,
|
||||
temperature=llm_config.temperature,
|
||||
)
|
||||
# https://platform.openai.com/docs/guides/text-generation/json-mode
|
||||
# only supported by gpt-4o, gpt-4-turbo, or gpt-3.5-turbo
|
||||
@ -378,126 +375,21 @@ def openai_chat_completions_process_stream(
|
||||
return chat_completion_response
|
||||
|
||||
|
||||
def _sse_post(url: str, data: dict, headers: dict) -> Generator[ChatCompletionChunkResponse, None, None]:
|
||||
|
||||
with httpx.Client() as client:
|
||||
with connect_sse(client, method="POST", url=url, json=data, headers=headers) as event_source:
|
||||
|
||||
# Inspect for errors before iterating (see https://github.com/florimondmanca/httpx-sse/pull/12)
|
||||
if not event_source.response.is_success:
|
||||
# handle errors
|
||||
from letta.utils import printd
|
||||
|
||||
printd("Caught error before iterating SSE request:", vars(event_source.response))
|
||||
printd(event_source.response.read())
|
||||
|
||||
try:
|
||||
response_bytes = event_source.response.read()
|
||||
response_dict = json.loads(response_bytes.decode("utf-8"))
|
||||
error_message = response_dict["error"]["message"]
|
||||
# e.g.: This model's maximum context length is 8192 tokens. However, your messages resulted in 8198 tokens (7450 in the messages, 748 in the functions). Please reduce the length of the messages or functions.
|
||||
if OPENAI_CONTEXT_WINDOW_ERROR_SUBSTRING in error_message:
|
||||
raise LLMError(error_message)
|
||||
except LLMError:
|
||||
raise
|
||||
except:
|
||||
print(f"Failed to parse SSE message, throwing SSE HTTP error up the stack")
|
||||
event_source.response.raise_for_status()
|
||||
|
||||
try:
|
||||
for sse in event_source.iter_sse():
|
||||
# printd(sse.event, sse.data, sse.id, sse.retry)
|
||||
if sse.data == OPENAI_SSE_DONE:
|
||||
# print("finished")
|
||||
break
|
||||
else:
|
||||
chunk_data = json.loads(sse.data)
|
||||
# print("chunk_data::", chunk_data)
|
||||
chunk_object = ChatCompletionChunkResponse(**chunk_data)
|
||||
# print("chunk_object::", chunk_object)
|
||||
# id=chunk_data["id"],
|
||||
# choices=[ChunkChoice],
|
||||
# model=chunk_data["model"],
|
||||
# system_fingerprint=chunk_data["system_fingerprint"]
|
||||
# )
|
||||
yield chunk_object
|
||||
|
||||
except SSEError as e:
|
||||
print("Caught an error while iterating the SSE stream:", str(e))
|
||||
if "application/json" in str(e): # Check if the error is because of JSON response
|
||||
# TODO figure out a better way to catch the error other than re-trying with a POST
|
||||
response = client.post(url=url, json=data, headers=headers) # Make the request again to get the JSON response
|
||||
if response.headers["Content-Type"].startswith("application/json"):
|
||||
error_details = response.json() # Parse the JSON to get the error message
|
||||
print("Request:", vars(response.request))
|
||||
print("POST Error:", error_details)
|
||||
print("Original SSE Error:", str(e))
|
||||
else:
|
||||
print("Failed to retrieve JSON error message via retry.")
|
||||
else:
|
||||
print("SSEError not related to 'application/json' content type.")
|
||||
|
||||
# Optionally re-raise the exception if you need to propagate it
|
||||
raise e
|
||||
|
||||
except Exception as e:
|
||||
if event_source.response.request is not None:
|
||||
print("HTTP Request:", vars(event_source.response.request))
|
||||
if event_source.response is not None:
|
||||
print("HTTP Status:", event_source.response.status_code)
|
||||
print("HTTP Headers:", event_source.response.headers)
|
||||
# print("HTTP Body:", event_source.response.text)
|
||||
print("Exception message:", str(e))
|
||||
raise e
|
||||
|
||||
|
||||
def openai_chat_completions_request_stream(
|
||||
url: str,
|
||||
api_key: str,
|
||||
chat_completion_request: ChatCompletionRequest,
|
||||
) -> Generator[ChatCompletionChunkResponse, None, None]:
|
||||
from letta.utils import printd
|
||||
|
||||
url = smart_urljoin(url, "chat/completions")
|
||||
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
|
||||
data = chat_completion_request.model_dump(exclude_none=True)
|
||||
|
||||
printd("Request:\n", json.dumps(data, indent=2))
|
||||
|
||||
# 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")
|
||||
|
||||
if "tools" in data:
|
||||
for tool in data["tools"]:
|
||||
# tool["strict"] = True
|
||||
try:
|
||||
tool["function"] = convert_to_structured_output(tool["function"])
|
||||
except ValueError as e:
|
||||
warnings.warn(f"Failed to convert tool function to structured output, tool={tool}, error={e}")
|
||||
|
||||
# print(f"\n\n\n\nData[tools]: {json.dumps(data['tools'], indent=2)}")
|
||||
|
||||
printd(f"Sending request to {url}")
|
||||
try:
|
||||
return _sse_post(url=url, data=data, headers=headers)
|
||||
except requests.exceptions.HTTPError as http_err:
|
||||
# Handle HTTP errors (e.g., response 4XX, 5XX)
|
||||
printd(f"Got HTTPError, exception={http_err}, payload={data}")
|
||||
raise http_err
|
||||
except requests.exceptions.RequestException as req_err:
|
||||
# Handle other requests-related errors (e.g., connection error)
|
||||
printd(f"Got RequestException, exception={req_err}")
|
||||
raise req_err
|
||||
except Exception as e:
|
||||
# Handle other potential errors
|
||||
printd(f"Got unknown Exception, exception={e}")
|
||||
raise e
|
||||
data = prepare_openai_payload(chat_completion_request)
|
||||
data["stream"] = True
|
||||
client = OpenAI(
|
||||
api_key=api_key,
|
||||
base_url=url,
|
||||
)
|
||||
stream = client.chat.completions.create(**data)
|
||||
for chunk in stream:
|
||||
# TODO: Use the native OpenAI objects here?
|
||||
yield ChatCompletionChunkResponse(**chunk.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
def openai_chat_completions_request(
|
||||
@ -512,18 +404,28 @@ def openai_chat_completions_request(
|
||||
|
||||
https://platform.openai.com/docs/guides/text-generation?lang=curl
|
||||
"""
|
||||
from letta.utils import printd
|
||||
data = prepare_openai_payload(chat_completion_request)
|
||||
client = OpenAI(api_key=api_key, base_url=url)
|
||||
chat_completion = client.chat.completions.create(**data)
|
||||
return ChatCompletionResponse(**chat_completion.model_dump())
|
||||
|
||||
url = smart_urljoin(url, "chat/completions")
|
||||
|
||||
def openai_embeddings_request(url: str, api_key: str, data: dict) -> EmbeddingResponse:
|
||||
"""https://platform.openai.com/docs/api-reference/embeddings/create"""
|
||||
|
||||
url = smart_urljoin(url, "embeddings")
|
||||
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
|
||||
response_json = make_post_request(url, headers, data)
|
||||
return EmbeddingResponse(**response_json)
|
||||
|
||||
|
||||
def prepare_openai_payload(chat_completion_request: ChatCompletionRequest):
|
||||
data = chat_completion_request.model_dump(exclude_none=True)
|
||||
|
||||
# add check otherwise will cause error: "Invalid value for 'parallel_tool_calls': 'parallel_tool_calls' is only allowed when 'tools' are specified."
|
||||
if chat_completion_request.tools is not None:
|
||||
data["parallel_tool_calls"] = False
|
||||
|
||||
printd("Request:\n", json.dumps(data, indent=2))
|
||||
|
||||
# If functions == None, strip from the payload
|
||||
if "functions" in data and data["functions"] is None:
|
||||
data.pop("functions")
|
||||
@ -540,14 +442,4 @@ def openai_chat_completions_request(
|
||||
except ValueError as e:
|
||||
warnings.warn(f"Failed to convert tool function to structured output, tool={tool}, error={e}")
|
||||
|
||||
response_json = make_post_request(url, headers, data)
|
||||
return ChatCompletionResponse(**response_json)
|
||||
|
||||
|
||||
def openai_embeddings_request(url: str, api_key: str, data: dict) -> EmbeddingResponse:
|
||||
"""https://platform.openai.com/docs/api-reference/embeddings/create"""
|
||||
|
||||
url = smart_urljoin(url, "embeddings")
|
||||
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
|
||||
response_json = make_post_request(url, headers, data)
|
||||
return EmbeddingResponse(**response_json)
|
||||
return data
|
||||
|
@ -6,7 +6,7 @@ from letta.prompts.gpt_summarize import SYSTEM as SUMMARY_PROMPT_SYSTEM
|
||||
from letta.schemas.agent import AgentState
|
||||
from letta.schemas.enums import MessageRole
|
||||
from letta.schemas.memory import Memory
|
||||
from letta.schemas.message import Message
|
||||
from letta.schemas.message import Message, TextContent
|
||||
from letta.settings import summarizer_settings
|
||||
from letta.utils import count_tokens, printd
|
||||
|
||||
@ -60,9 +60,9 @@ def summarize_messages(
|
||||
|
||||
dummy_agent_id = agent_state.id
|
||||
message_sequence = [
|
||||
Message(agent_id=dummy_agent_id, role=MessageRole.system, text=summary_prompt),
|
||||
Message(agent_id=dummy_agent_id, role=MessageRole.assistant, text=MESSAGE_SUMMARY_REQUEST_ACK),
|
||||
Message(agent_id=dummy_agent_id, role=MessageRole.user, text=summary_input),
|
||||
Message(agent_id=dummy_agent_id, role=MessageRole.system, content=[TextContent(text=summary_prompt)]),
|
||||
Message(agent_id=dummy_agent_id, role=MessageRole.assistant, content=[TextContent(text=MESSAGE_SUMMARY_REQUEST_ACK)]),
|
||||
Message(agent_id=dummy_agent_id, role=MessageRole.user, content=[TextContent(text=summary_input)]),
|
||||
]
|
||||
|
||||
# TODO: We need to eventually have a separate LLM config for the summarizer LLM
|
||||
|
@ -8,12 +8,12 @@ from letta.schemas.openai.chat_completion_response import UsageStatistics
|
||||
from letta.schemas.usage import LettaUsageStatistics
|
||||
|
||||
|
||||
def trigger_rethink_memory(agent_state: "AgentState", message: Optional[str]) -> Optional[str]: # type: ignore
|
||||
def trigger_rethink_memory(agent_state: "AgentState", message: str) -> None: # type: ignore
|
||||
"""
|
||||
Called if and only when user says the word trigger_rethink_memory". It will trigger the re-evaluation of the memory.
|
||||
|
||||
Args:
|
||||
message (Optional[str]): Description of what aspect of the memory should be re-evaluated.
|
||||
message (str): Description of what aspect of the memory should be re-evaluated.
|
||||
|
||||
"""
|
||||
from letta import create_client
|
||||
@ -25,12 +25,12 @@ def trigger_rethink_memory(agent_state: "AgentState", message: Optional[str]) ->
|
||||
client.user_message(agent_id=agent.id, message=message)
|
||||
|
||||
|
||||
def trigger_rethink_memory_convo(agent_state: "AgentState", message: Optional[str]) -> Optional[str]: # type: ignore
|
||||
def trigger_rethink_memory_convo(agent_state: "AgentState", message: str) -> None: # type: ignore
|
||||
"""
|
||||
Called if and only when user says the word "trigger_rethink_memory". It will trigger the re-evaluation of the memory.
|
||||
|
||||
Args:
|
||||
message (Optional[str]): Description of what aspect of the memory should be re-evaluated.
|
||||
message (str): Description of what aspect of the memory should be re-evaluated.
|
||||
|
||||
"""
|
||||
from letta import create_client
|
||||
@ -48,7 +48,7 @@ def trigger_rethink_memory_convo(agent_state: "AgentState", message: Optional[st
|
||||
client.user_message(agent_id=agent.id, message=message)
|
||||
|
||||
|
||||
def rethink_memory_convo(agent_state: "AgentState", new_memory: str, target_block_label: Optional[str], source_block_label: Optional[str]) -> Optional[str]: # type: ignore
|
||||
def rethink_memory_convo(agent_state: "AgentState", new_memory: str, target_block_label: str, source_block_label: str) -> None: # type: ignore
|
||||
"""
|
||||
Re-evaluate the memory in block_name, integrating new and updated facts. Replace outdated information with the most likely truths, avoiding redundancy with original memories. Ensure consistency with other memory blocks.
|
||||
|
||||
@ -58,7 +58,7 @@ def rethink_memory_convo(agent_state: "AgentState", new_memory: str, target_bloc
|
||||
target_block_label (str): The name of the block to write to. This should be chat_agent_human_new or chat_agent_persona_new.
|
||||
|
||||
Returns:
|
||||
Optional[str]: None is always returned as this function does not produce a response.
|
||||
None: None is always returned as this function does not produce a response.
|
||||
"""
|
||||
if target_block_label is not None:
|
||||
if agent_state.memory.get_block(target_block_label) is None:
|
||||
@ -67,7 +67,7 @@ def rethink_memory_convo(agent_state: "AgentState", new_memory: str, target_bloc
|
||||
return None
|
||||
|
||||
|
||||
def rethink_memory(agent_state: "AgentState", new_memory: str, target_block_label: Optional[str], source_block_label: Optional[str]) -> Optional[str]: # type: ignore
|
||||
def rethink_memory(agent_state: "AgentState", new_memory: str, target_block_label: str, source_block_label: str) -> None: # type: ignore
|
||||
"""
|
||||
Re-evaluate the memory in block_name, integrating new and updated facts.
|
||||
Replace outdated information with the most likely truths, avoiding redundancy with original memories.
|
||||
@ -78,7 +78,7 @@ def rethink_memory(agent_state: "AgentState", new_memory: str, target_block_labe
|
||||
source_block_label (str): The name of the block to integrate information from. None if all the information has been integrated to terminate the loop.
|
||||
target_block_label (str): The name of the block to write to.
|
||||
Returns:
|
||||
Optional[str]: None is always returned as this function does not produce a response.
|
||||
None: None is always returned as this function does not produce a response.
|
||||
"""
|
||||
|
||||
if target_block_label is not None:
|
||||
@ -88,7 +88,7 @@ def rethink_memory(agent_state: "AgentState", new_memory: str, target_block_labe
|
||||
return None
|
||||
|
||||
|
||||
def finish_rethinking_memory(agent_state: "AgentState") -> Optional[str]: # type: ignore
|
||||
def finish_rethinking_memory(agent_state: "AgentState") -> None: # type: ignore
|
||||
"""
|
||||
This function is called when the agent is done rethinking the memory.
|
||||
|
||||
@ -98,7 +98,7 @@ def finish_rethinking_memory(agent_state: "AgentState") -> Optional[str]: # typ
|
||||
return None
|
||||
|
||||
|
||||
def finish_rethinking_memory_convo(agent_state: "AgentState") -> Optional[str]: # type: ignore
|
||||
def finish_rethinking_memory_convo(agent_state: "AgentState") -> None: # type: ignore
|
||||
"""
|
||||
This function is called when the agent is done rethinking the memory.
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
from sqlalchemy import JSON, String
|
||||
from sqlalchemy import JSON, Index, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from letta.orm.block import Block
|
||||
@ -27,6 +27,7 @@ if TYPE_CHECKING:
|
||||
class Agent(SqlalchemyBase, OrganizationMixin):
|
||||
__tablename__ = "agents"
|
||||
__pydantic_model__ = PydanticAgentState
|
||||
__table_args__ = (Index("ix_agents_created_at", "created_at", "id"),)
|
||||
|
||||
# agent generates its own id
|
||||
# TODO: We want to migrate all the ORM models to do this, so we will need to move this to the SqlalchemyBase
|
||||
@ -69,7 +70,14 @@ class Agent(SqlalchemyBase, OrganizationMixin):
|
||||
)
|
||||
tools: Mapped[List["Tool"]] = relationship("Tool", secondary="tools_agents", lazy="selectin", passive_deletes=True)
|
||||
sources: Mapped[List["Source"]] = relationship("Source", secondary="sources_agents", lazy="selectin")
|
||||
core_memory: Mapped[List["Block"]] = relationship("Block", secondary="blocks_agents", lazy="selectin")
|
||||
core_memory: Mapped[List["Block"]] = relationship(
|
||||
"Block",
|
||||
secondary="blocks_agents",
|
||||
lazy="selectin",
|
||||
passive_deletes=True, # Ensures SQLAlchemy doesn't fetch blocks_agents rows before deleting
|
||||
back_populates="agents",
|
||||
doc="Blocks forming the core memory of the agent.",
|
||||
)
|
||||
messages: Mapped[List["Message"]] = relationship(
|
||||
"Message",
|
||||
back_populates="agent",
|
||||
|
@ -1,6 +1,6 @@
|
||||
from typing import TYPE_CHECKING, Optional, Type
|
||||
from typing import TYPE_CHECKING, List, Optional, Type
|
||||
|
||||
from sqlalchemy import JSON, BigInteger, Integer, UniqueConstraint, event
|
||||
from sqlalchemy import JSON, BigInteger, Index, Integer, UniqueConstraint, event
|
||||
from sqlalchemy.orm import Mapped, attributes, mapped_column, relationship
|
||||
|
||||
from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
|
||||
@ -20,7 +20,10 @@ class Block(OrganizationMixin, SqlalchemyBase):
|
||||
__tablename__ = "block"
|
||||
__pydantic_model__ = PydanticBlock
|
||||
# This may seem redundant, but is necessary for the BlocksAgents composite FK relationship
|
||||
__table_args__ = (UniqueConstraint("id", "label", name="unique_block_id_label"),)
|
||||
__table_args__ = (
|
||||
UniqueConstraint("id", "label", name="unique_block_id_label"),
|
||||
Index("created_at_label_idx", "created_at", "label"),
|
||||
)
|
||||
|
||||
template_name: Mapped[Optional[str]] = mapped_column(
|
||||
nullable=True, doc="the unique name that identifies a block in a human-readable way"
|
||||
@ -36,6 +39,14 @@ class Block(OrganizationMixin, SqlalchemyBase):
|
||||
|
||||
# relationships
|
||||
organization: Mapped[Optional["Organization"]] = relationship("Organization")
|
||||
agents: Mapped[List["Agent"]] = relationship(
|
||||
"Agent",
|
||||
secondary="blocks_agents",
|
||||
lazy="selectin",
|
||||
passive_deletes=True, # Ensures SQLAlchemy doesn't fetch blocks_agents rows before deleting
|
||||
back_populates="core_memory",
|
||||
doc="Agents associated with this block.",
|
||||
)
|
||||
|
||||
def to_pydantic(self) -> Type:
|
||||
match self.label:
|
||||
|
@ -1,7 +1,7 @@
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
from sqlalchemy import JSON, String
|
||||
from sqlalchemy import JSON, Index, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from letta.orm.enums import JobType
|
||||
@ -25,6 +25,7 @@ class Job(SqlalchemyBase, UserMixin):
|
||||
|
||||
__tablename__ = "jobs"
|
||||
__pydantic_model__ = PydanticJob
|
||||
__table_args__ = (Index("ix_jobs_created_at", "created_at", "id"),)
|
||||
|
||||
status: Mapped[JobStatus] = mapped_column(String, default=JobStatus.created, doc="The current status of the job.")
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True, doc="The unix timestamp of when the job was completed.")
|
||||
|
@ -1,30 +0,0 @@
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from letta.orm.job import Job
|
||||
|
||||
|
||||
class JobUsageStatistics(SqlalchemyBase):
|
||||
"""Tracks usage statistics for jobs, with future support for per-step tracking."""
|
||||
|
||||
__tablename__ = "job_usage_statistics"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, doc="Unique identifier for the usage statistics entry")
|
||||
job_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("jobs.id", ondelete="CASCADE"), nullable=False, doc="ID of the job these statistics belong to"
|
||||
)
|
||||
step_id: Mapped[Optional[str]] = mapped_column(
|
||||
nullable=True, doc="ID of the specific step within the job (for future per-step tracking)"
|
||||
)
|
||||
completion_tokens: Mapped[int] = mapped_column(default=0, doc="Number of tokens generated by the agent")
|
||||
prompt_tokens: Mapped[int] = mapped_column(default=0, doc="Number of tokens in the prompt")
|
||||
total_tokens: Mapped[int] = mapped_column(default=0, doc="Total number of tokens processed by the agent")
|
||||
step_count: Mapped[int] = mapped_column(default=0, doc="Number of steps taken by the agent")
|
||||
|
||||
# Relationship back to the job
|
||||
job: Mapped["Job"] = relationship("Job", back_populates="usage_statistics")
|
@ -8,13 +8,17 @@ from letta.orm.custom_columns import ToolCallColumn
|
||||
from letta.orm.mixins import AgentMixin, OrganizationMixin
|
||||
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
||||
from letta.schemas.message import Message as PydanticMessage
|
||||
from letta.schemas.message import TextContent as PydanticTextContent
|
||||
|
||||
|
||||
class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
|
||||
"""Defines data model for storing Message objects"""
|
||||
|
||||
__tablename__ = "messages"
|
||||
__table_args__ = (Index("ix_messages_agent_created_at", "agent_id", "created_at"),)
|
||||
__table_args__ = (
|
||||
Index("ix_messages_agent_created_at", "agent_id", "created_at"),
|
||||
Index("ix_messages_created_at", "created_at", "id"),
|
||||
)
|
||||
__pydantic_model__ = PydanticMessage
|
||||
|
||||
id: Mapped[str] = mapped_column(primary_key=True, doc="Unique message identifier")
|
||||
@ -42,3 +46,10 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
|
||||
def job(self) -> Optional["Job"]:
|
||||
"""Get the job associated with this message, if any."""
|
||||
return self.job_message.job if self.job_message else None
|
||||
|
||||
def to_pydantic(self) -> PydanticMessage:
|
||||
"""custom pydantic conversion for message content mapping"""
|
||||
model = self.__pydantic_model__.model_validate(self)
|
||||
if self.text:
|
||||
model.content = [PydanticTextContent(text=self.text)]
|
||||
return model
|
||||
|
@ -45,8 +45,12 @@ class BasePassage(SqlalchemyBase, OrganizationMixin):
|
||||
@declared_attr
|
||||
def __table_args__(cls):
|
||||
if settings.letta_pg_uri_no_default:
|
||||
return (Index(f"{cls.__tablename__}_org_idx", "organization_id"), {"extend_existing": True})
|
||||
return ({"extend_existing": True},)
|
||||
return (
|
||||
Index(f"{cls.__tablename__}_org_idx", "organization_id"),
|
||||
Index(f"{cls.__tablename__}_created_at_id_idx", "created_at", "id"),
|
||||
{"extend_existing": True},
|
||||
)
|
||||
return (Index(f"{cls.__tablename__}_created_at_id_idx", "created_at", "id"), {"extend_existing": True})
|
||||
|
||||
|
||||
class SourcePassage(BasePassage, FileMixin, SourceMixin):
|
||||
|
@ -1,6 +1,6 @@
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
from sqlalchemy import JSON
|
||||
from sqlalchemy import JSON, Index
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from letta.orm import FileMetadata
|
||||
@ -23,6 +23,11 @@ class Source(SqlalchemyBase, OrganizationMixin):
|
||||
__tablename__ = "sources"
|
||||
__pydantic_model__ = PydanticSource
|
||||
|
||||
__table_args__ = (
|
||||
Index(f"source_created_at_id_idx", "created_at", "id"),
|
||||
{"extend_existing": True},
|
||||
)
|
||||
|
||||
name: Mapped[str] = mapped_column(doc="the name of the source, must be unique within the org", nullable=False)
|
||||
description: Mapped[str] = mapped_column(nullable=True, doc="a human-readable description of the source")
|
||||
embedding_config: Mapped[EmbeddingConfig] = mapped_column(EmbeddingConfigColumn, doc="Configuration settings for embedding.")
|
||||
|
@ -3,7 +3,7 @@ from enum import Enum
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, List, Literal, Optional, Tuple, Union
|
||||
|
||||
from sqlalchemy import String, and_, desc, func, or_, select
|
||||
from sqlalchemy import String, and_, func, or_, select
|
||||
from sqlalchemy.exc import DBAPIError, IntegrityError, TimeoutError
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
|
||||
@ -52,7 +52,8 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
||||
cls,
|
||||
*,
|
||||
db_session: "Session",
|
||||
cursor: Optional[str] = None,
|
||||
before: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
limit: Optional[int] = 50,
|
||||
@ -69,12 +70,13 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
||||
**kwargs,
|
||||
) -> List["SqlalchemyBase"]:
|
||||
"""
|
||||
List records with cursor-based pagination, ordering by created_at.
|
||||
Cursor is an ID, but pagination is based on the cursor object's created_at value.
|
||||
List records with before/after pagination, ordering by created_at.
|
||||
Can use both before and after to fetch a window of records.
|
||||
|
||||
Args:
|
||||
db_session: SQLAlchemy session
|
||||
cursor: ID of the last item seen (for pagination)
|
||||
before: ID of item to paginate before (upper bound)
|
||||
after: ID of item to paginate after (lower bound)
|
||||
start_date: Filter items after this date
|
||||
end_date: Filter items before this date
|
||||
limit: Maximum number of items to return
|
||||
@ -89,13 +91,25 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
||||
raise ValueError("start_date must be earlier than or equal to end_date")
|
||||
|
||||
logger.debug(f"Listing {cls.__name__} with kwarg filters {kwargs}")
|
||||
|
||||
with db_session as session:
|
||||
# If cursor provided, get the reference object
|
||||
cursor_obj = None
|
||||
if cursor:
|
||||
cursor_obj = session.get(cls, cursor)
|
||||
if not cursor_obj:
|
||||
raise NoResultFound(f"No {cls.__name__} found with id {cursor}")
|
||||
# Get the reference objects for pagination
|
||||
before_obj = None
|
||||
after_obj = None
|
||||
|
||||
if before:
|
||||
before_obj = session.get(cls, before)
|
||||
if not before_obj:
|
||||
raise NoResultFound(f"No {cls.__name__} found with id {before}")
|
||||
|
||||
if after:
|
||||
after_obj = session.get(cls, after)
|
||||
if not after_obj:
|
||||
raise NoResultFound(f"No {cls.__name__} found with id {after}")
|
||||
|
||||
# Validate that before comes after the after object if both are provided
|
||||
if before_obj and after_obj and before_obj.created_at < after_obj.created_at:
|
||||
raise ValueError("'before' reference must be later than 'after' reference")
|
||||
|
||||
query = select(cls)
|
||||
|
||||
@ -122,8 +136,8 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
||||
else:
|
||||
# Match ANY tag - use join and filter
|
||||
query = (
|
||||
query.join(cls.tags).filter(cls.tags.property.mapper.class_.tag.in_(tags)).group_by(cls.id) # Deduplicate results
|
||||
)
|
||||
query.join(cls.tags).filter(cls.tags.property.mapper.class_.tag.in_(tags)).group_by(cls.id)
|
||||
) # Deduplicate results
|
||||
|
||||
# Group by primary key and all necessary columns to avoid JSON comparison
|
||||
query = query.group_by(cls.id)
|
||||
@ -150,16 +164,35 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
||||
if end_date:
|
||||
query = query.filter(cls.created_at < end_date)
|
||||
|
||||
# Cursor-based pagination
|
||||
if cursor_obj:
|
||||
if ascending:
|
||||
query = query.where(cls.created_at >= cursor_obj.created_at).where(
|
||||
or_(cls.created_at > cursor_obj.created_at, cls.id > cursor_obj.id)
|
||||
)
|
||||
# Handle pagination based on before/after
|
||||
if before or after:
|
||||
conditions = []
|
||||
|
||||
if before and after:
|
||||
# Window-based query - get records between before and after
|
||||
conditions = [
|
||||
or_(cls.created_at < before_obj.created_at, and_(cls.created_at == before_obj.created_at, cls.id < before_obj.id)),
|
||||
or_(cls.created_at > after_obj.created_at, and_(cls.created_at == after_obj.created_at, cls.id > after_obj.id)),
|
||||
]
|
||||
else:
|
||||
query = query.where(cls.created_at <= cursor_obj.created_at).where(
|
||||
or_(cls.created_at < cursor_obj.created_at, cls.id < cursor_obj.id)
|
||||
)
|
||||
# Pure pagination query
|
||||
if before:
|
||||
conditions.append(
|
||||
or_(
|
||||
cls.created_at < before_obj.created_at,
|
||||
and_(cls.created_at == before_obj.created_at, cls.id < before_obj.id),
|
||||
)
|
||||
)
|
||||
if after:
|
||||
conditions.append(
|
||||
or_(
|
||||
cls.created_at > after_obj.created_at,
|
||||
and_(cls.created_at == after_obj.created_at, cls.id > after_obj.id),
|
||||
)
|
||||
)
|
||||
|
||||
if conditions:
|
||||
query = query.where(and_(*conditions))
|
||||
|
||||
# Text search
|
||||
if query_text:
|
||||
@ -184,7 +217,9 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
||||
# SQLite with custom vector type
|
||||
query_embedding_binary = adapt_array(query_embedding)
|
||||
query = query.order_by(
|
||||
func.cosine_distance(cls.embedding, query_embedding_binary).asc(), cls.created_at.asc(), cls.id.asc()
|
||||
func.cosine_distance(cls.embedding, query_embedding_binary).asc(),
|
||||
cls.created_at.asc() if ascending else cls.created_at.desc(),
|
||||
cls.id.asc(),
|
||||
)
|
||||
is_ordered = True
|
||||
|
||||
@ -195,13 +230,28 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
||||
# Apply ordering
|
||||
if not is_ordered:
|
||||
if ascending:
|
||||
query = query.order_by(cls.created_at, cls.id)
|
||||
query = query.order_by(cls.created_at.asc(), cls.id.asc())
|
||||
else:
|
||||
query = query.order_by(desc(cls.created_at), desc(cls.id))
|
||||
query = query.order_by(cls.created_at.desc(), cls.id.desc())
|
||||
|
||||
query = query.limit(limit)
|
||||
# Apply limit, adjusting for both bounds if necessary
|
||||
if before and after:
|
||||
# When both bounds are provided, we need to fetch enough records to satisfy
|
||||
# the limit while respecting both bounds. We'll fetch more and then trim.
|
||||
query = query.limit(limit * 2)
|
||||
else:
|
||||
query = query.limit(limit)
|
||||
|
||||
return list(session.execute(query).scalars())
|
||||
results = list(session.execute(query).scalars())
|
||||
|
||||
# If we have both bounds, take the middle portion
|
||||
if before and after and len(results) > limit:
|
||||
middle = len(results) // 2
|
||||
start = max(0, middle - limit // 2)
|
||||
end = min(len(results), start + limit)
|
||||
results = results[start:end]
|
||||
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
@handle_db_timeout
|
||||
@ -449,12 +499,10 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
||||
|
||||
def to_pydantic(self) -> "BaseModel":
|
||||
"""converts to the basic pydantic model counterpart"""
|
||||
model = self.__pydantic_model__.model_validate(self)
|
||||
if hasattr(self, "metadata_"):
|
||||
model_dict = {k: v for k, v in self.__dict__.items() if k in self.__pydantic_model__.model_fields}
|
||||
model_dict["metadata"] = self.metadata_
|
||||
return self.__pydantic_model__.model_validate(model_dict)
|
||||
|
||||
return self.__pydantic_model__.model_validate(self)
|
||||
model.metadata = self.metadata_
|
||||
return model
|
||||
|
||||
def to_record(self) -> "BaseModel":
|
||||
"""Deprecated accessor for to_pydantic"""
|
||||
|
@ -1,6 +1,6 @@
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
|
||||
from sqlalchemy import JSON, String, UniqueConstraint
|
||||
from sqlalchemy import JSON, Index, String, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
# TODO everything in functions should live in this model
|
||||
@ -26,7 +26,10 @@ class Tool(SqlalchemyBase, OrganizationMixin):
|
||||
|
||||
# Add unique constraint on (name, _organization_id)
|
||||
# An organization should not have multiple tools with the same name
|
||||
__table_args__ = (UniqueConstraint("name", "organization_id", name="uix_name_organization"),)
|
||||
__table_args__ = (
|
||||
UniqueConstraint("name", "organization_id", name="uix_name_organization"),
|
||||
Index("ix_tools_created_at_name", "created_at", "name"),
|
||||
)
|
||||
|
||||
name: Mapped[str] = mapped_column(doc="The display name of the tool.")
|
||||
tool_type: Mapped[ToolType] = mapped_column(
|
||||
|
3
letta/schemas/embedding_config_overrides.py
Normal file
3
letta/schemas/embedding_config_overrides.py
Normal file
@ -0,0 +1,3 @@
|
||||
from typing import Dict
|
||||
|
||||
EMBEDDING_HANDLE_OVERRIDES: Dict[str, Dict[str, str]] = {}
|
@ -9,6 +9,10 @@ class MessageRole(str, Enum):
|
||||
system = "system"
|
||||
|
||||
|
||||
class MessageContentType(str, Enum):
|
||||
text = "text"
|
||||
|
||||
|
||||
class OptionState(str, Enum):
|
||||
"""Useful for kwargs that are bool + default option"""
|
||||
|
||||
|
@ -12,7 +12,7 @@ class JobBase(OrmMetadataBase):
|
||||
__id_prefix__ = "job"
|
||||
status: JobStatus = Field(default=JobStatus.created, description="The status of the job.")
|
||||
completed_at: Optional[datetime] = Field(None, description="The unix timestamp of when the job was completed.")
|
||||
metadata: Optional[dict] = Field(None, description="The metadata of the job.")
|
||||
metadata: Optional[dict] = Field(None, validation_alias="metadata_", description="The metadata of the job.")
|
||||
job_type: JobType = Field(default=JobType.JOB, description="The type of the job.")
|
||||
|
||||
|
||||
|
@ -4,6 +4,8 @@ from typing import Annotated, List, Literal, Optional, Union
|
||||
|
||||
from pydantic import BaseModel, Field, field_serializer, field_validator
|
||||
|
||||
from letta.schemas.enums import MessageContentType
|
||||
|
||||
# Letta API style responses (intended to be easier to use vs getting true Message types)
|
||||
|
||||
|
||||
@ -32,18 +34,33 @@ class LettaMessage(BaseModel):
|
||||
return dt.isoformat(timespec="seconds")
|
||||
|
||||
|
||||
class MessageContent(BaseModel):
|
||||
type: MessageContentType = Field(..., description="The type of the message.")
|
||||
|
||||
|
||||
class TextContent(MessageContent):
|
||||
type: Literal[MessageContentType.text] = Field(MessageContentType.text, description="The type of the message.")
|
||||
text: str = Field(..., description="The text content of the message.")
|
||||
|
||||
|
||||
MessageContentUnion = Annotated[
|
||||
Union[TextContent],
|
||||
Field(discriminator="type"),
|
||||
]
|
||||
|
||||
|
||||
class SystemMessage(LettaMessage):
|
||||
"""
|
||||
A message generated by the system. Never streamed back on a response, only used for cursor pagination.
|
||||
|
||||
Attributes:
|
||||
message (str): The message sent by the system
|
||||
content (Union[str, List[MessageContentUnion]]): The message content sent by the user (can be a string or an array of content parts)
|
||||
id (str): The ID of the message
|
||||
date (datetime): The date the message was created in ISO format
|
||||
"""
|
||||
|
||||
message_type: Literal["system_message"] = "system_message"
|
||||
message: str
|
||||
content: Union[str, List[MessageContentUnion]]
|
||||
|
||||
|
||||
class UserMessage(LettaMessage):
|
||||
@ -51,13 +68,13 @@ class UserMessage(LettaMessage):
|
||||
A message sent by the user. Never streamed back on a response, only used for cursor pagination.
|
||||
|
||||
Attributes:
|
||||
message (str): The message sent by the user
|
||||
content (Union[str, List[MessageContentUnion]]): The message content sent by the user (can be a string or an array of content parts)
|
||||
id (str): The ID of the message
|
||||
date (datetime): The date the message was created in ISO format
|
||||
"""
|
||||
|
||||
message_type: Literal["user_message"] = "user_message"
|
||||
message: str
|
||||
content: Union[str, List[MessageContentUnion]]
|
||||
|
||||
|
||||
class ReasoningMessage(LettaMessage):
|
||||
@ -167,7 +184,7 @@ class ToolReturnMessage(LettaMessage):
|
||||
|
||||
class AssistantMessage(LettaMessage):
|
||||
message_type: Literal["assistant_message"] = "assistant_message"
|
||||
assistant_message: str
|
||||
content: Union[str, List[MessageContentUnion]]
|
||||
|
||||
|
||||
class LegacyFunctionCallMessage(LettaMessage):
|
||||
|
@ -14,6 +14,7 @@ class LLMConfig(BaseModel):
|
||||
model_wrapper (str): The wrapper for the model. This is used to wrap additional text around the input/output of the model. This is useful for text-to-text completions, such as the Completions API in OpenAI.
|
||||
context_window (int): The context window size for the model.
|
||||
put_inner_thoughts_in_kwargs (bool): Puts `inner_thoughts` as a kwarg in the function call if this is set to True. This helps with function calling performance and also the generation of inner thoughts.
|
||||
temperature (float): The temperature to use when generating text with the model. A higher temperature will result in more random text.
|
||||
"""
|
||||
|
||||
# TODO: 🤮 don't default to a vendor! bug city!
|
||||
@ -46,6 +47,10 @@ class LLMConfig(BaseModel):
|
||||
description="Puts 'inner_thoughts' as a kwarg in the function call if this is set to True. This helps with function calling performance and also the generation of inner thoughts.",
|
||||
)
|
||||
handle: Optional[str] = Field(None, description="The handle for this config, in the format provider/model-name.")
|
||||
temperature: float = Field(
|
||||
0.7,
|
||||
description="The temperature to use when generating text with the model. A higher temperature will result in more random text.",
|
||||
)
|
||||
|
||||
# FIXME hack to silence pydantic protected namespace warning
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
|
38
letta/schemas/llm_config_overrides.py
Normal file
38
letta/schemas/llm_config_overrides.py
Normal file
@ -0,0 +1,38 @@
|
||||
from typing import Dict
|
||||
|
||||
LLM_HANDLE_OVERRIDES: Dict[str, Dict[str, str]] = {
|
||||
"anthropic": {
|
||||
"claude-3-5-haiku-20241022": "claude-3.5-haiku",
|
||||
"claude-3-5-sonnet-20241022": "claude-3.5-sonnet",
|
||||
"claude-3-opus-20240229": "claude-3-opus",
|
||||
},
|
||||
"openai": {
|
||||
"chatgpt-4o-latest": "chatgpt-4o",
|
||||
"gpt-3.5-turbo": "gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-0125": "gpt-3.5-turbo-jan",
|
||||
"gpt-3.5-turbo-1106": "gpt-3.5-turbo-nov",
|
||||
"gpt-3.5-turbo-16k": "gpt-3.5-turbo-16k",
|
||||
"gpt-3.5-turbo-instruct": "gpt-3.5-turbo-instruct",
|
||||
"gpt-4-0125-preview": "gpt-4-preview-jan",
|
||||
"gpt-4-0613": "gpt-4-june",
|
||||
"gpt-4-1106-preview": "gpt-4-preview-nov",
|
||||
"gpt-4-turbo-2024-04-09": "gpt-4-turbo-apr",
|
||||
"gpt-4o-2024-05-13": "gpt-4o-may",
|
||||
"gpt-4o-2024-08-06": "gpt-4o-aug",
|
||||
"gpt-4o-mini-2024-07-18": "gpt-4o-mini-jul",
|
||||
},
|
||||
"together": {
|
||||
"Qwen/Qwen2.5-72B-Instruct-Turbo": "qwen-2.5-72b-instruct",
|
||||
"meta-llama/Llama-3-70b-chat-hf": "llama-3-70b",
|
||||
"meta-llama/Meta-Llama-3-70B-Instruct-Turbo": "llama-3-70b-instruct",
|
||||
"meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo": "llama-3.1-405b-instruct",
|
||||
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo": "llama-3.1-70b-instruct",
|
||||
"meta-llama/Llama-3.3-70B-Instruct-Turbo": "llama-3.3-70b-instruct",
|
||||
"mistralai/Mistral-7B-Instruct-v0.2": "mistral-7b-instruct-v2",
|
||||
"mistralai/Mistral-7B-Instruct-v0.3": "mistral-7b-instruct-v3",
|
||||
"mistralai/Mixtral-8x22B-Instruct-v0.1": "mixtral-8x22b-instruct",
|
||||
"mistralai/Mixtral-8x7B-Instruct-v0.1": "mixtral-8x7b-instruct",
|
||||
"mistralai/Mixtral-8x7B-v0.1": "mixtral-8x7b",
|
||||
"NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO": "hermes-2-mixtral",
|
||||
},
|
||||
}
|
@ -2,21 +2,23 @@ import copy
|
||||
import json
|
||||
import warnings
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Literal, Optional
|
||||
from typing import Any, Dict, List, Literal, Optional, Union
|
||||
|
||||
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall
|
||||
from openai.types.chat.chat_completion_message_tool_call import Function as OpenAIFunction
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
|
||||
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, TOOL_CALL_ID_MAX_LEN
|
||||
from letta.local_llm.constants import INNER_THOUGHTS_KWARG
|
||||
from letta.schemas.enums import MessageRole
|
||||
from letta.schemas.enums import MessageContentType, MessageRole
|
||||
from letta.schemas.letta_base import OrmMetadataBase
|
||||
from letta.schemas.letta_message import (
|
||||
AssistantMessage,
|
||||
LettaMessage,
|
||||
MessageContentUnion,
|
||||
ReasoningMessage,
|
||||
SystemMessage,
|
||||
TextContent,
|
||||
ToolCall,
|
||||
ToolCallMessage,
|
||||
ToolReturnMessage,
|
||||
@ -59,7 +61,7 @@ class MessageCreate(BaseModel):
|
||||
MessageRole.user,
|
||||
MessageRole.system,
|
||||
] = Field(..., description="The role of the participant.")
|
||||
text: str = Field(..., description="The text of the message.")
|
||||
content: Union[str, List[MessageContentUnion]] = Field(..., description="The content of the message.")
|
||||
name: Optional[str] = Field(None, description="The name of the participant.")
|
||||
|
||||
|
||||
@ -67,7 +69,7 @@ class MessageUpdate(BaseModel):
|
||||
"""Request to update a message"""
|
||||
|
||||
role: Optional[MessageRole] = Field(None, description="The role of the participant.")
|
||||
text: Optional[str] = Field(None, description="The text of the message.")
|
||||
content: Optional[Union[str, List[MessageContentUnion]]] = Field(..., description="The content of the message.")
|
||||
# NOTE: probably doesn't make sense to allow remapping user_id or agent_id (vs creating a new message)
|
||||
# user_id: Optional[str] = Field(None, description="The unique identifier of the user.")
|
||||
# agent_id: Optional[str] = Field(None, description="The unique identifier of the agent.")
|
||||
@ -79,6 +81,18 @@ class MessageUpdate(BaseModel):
|
||||
tool_calls: Optional[List[OpenAIToolCall,]] = Field(None, description="The list of tool calls requested.")
|
||||
tool_call_id: Optional[str] = Field(None, description="The id of the tool call.")
|
||||
|
||||
def model_dump(self, to_orm: bool = False, **kwargs) -> Dict[str, Any]:
|
||||
data = super().model_dump(**kwargs)
|
||||
if to_orm and "content" in data:
|
||||
if isinstance(data["content"], str):
|
||||
data["text"] = data["content"]
|
||||
else:
|
||||
for content in data["content"]:
|
||||
if content["type"] == "text":
|
||||
data["text"] = content["text"]
|
||||
del data["content"]
|
||||
return data
|
||||
|
||||
|
||||
class Message(BaseMessage):
|
||||
"""
|
||||
@ -100,7 +114,7 @@ class Message(BaseMessage):
|
||||
|
||||
id: str = BaseMessage.generate_id_field()
|
||||
role: MessageRole = Field(..., description="The role of the participant.")
|
||||
text: Optional[str] = Field(None, description="The text of the message.")
|
||||
content: Optional[List[MessageContentUnion]] = Field(None, description="The content of the message.")
|
||||
organization_id: Optional[str] = Field(None, description="The unique identifier of the organization.")
|
||||
agent_id: Optional[str] = Field(None, description="The unique identifier of the agent.")
|
||||
model: Optional[str] = Field(None, description="The model used to make the function call.")
|
||||
@ -108,6 +122,7 @@ class Message(BaseMessage):
|
||||
tool_calls: Optional[List[OpenAIToolCall,]] = Field(None, description="The list of tool calls requested.")
|
||||
tool_call_id: Optional[str] = Field(None, description="The id of the tool call.")
|
||||
step_id: Optional[str] = Field(None, description="The id of the step that this message was created in.")
|
||||
|
||||
# This overrides the optional base orm schema, created_at MUST exist on all messages objects
|
||||
created_at: datetime = Field(default_factory=get_utc_time, description="The timestamp when the object was created.")
|
||||
|
||||
@ -118,6 +133,37 @@ class Message(BaseMessage):
|
||||
assert v in roles, f"Role must be one of {roles}"
|
||||
return v
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def convert_from_orm(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if isinstance(data, dict):
|
||||
if "text" in data and "content" not in data:
|
||||
data["content"] = [TextContent(text=data["text"])]
|
||||
del data["text"]
|
||||
return data
|
||||
|
||||
def model_dump(self, to_orm: bool = False, **kwargs) -> Dict[str, Any]:
|
||||
data = super().model_dump(**kwargs)
|
||||
if to_orm:
|
||||
for content in data["content"]:
|
||||
if content["type"] == "text":
|
||||
data["text"] = content["text"]
|
||||
del data["content"]
|
||||
return data
|
||||
|
||||
@property
|
||||
def text(self) -> Optional[str]:
|
||||
"""
|
||||
Retrieve the first text content's text.
|
||||
|
||||
Returns:
|
||||
str: The text content, or None if no text content exists
|
||||
"""
|
||||
if not self.content:
|
||||
return None
|
||||
text_content = [content.text for content in self.content if content.type == MessageContentType.text]
|
||||
return text_content[0] if text_content else None
|
||||
|
||||
def to_json(self):
|
||||
json_message = vars(self)
|
||||
if json_message["tool_calls"] is not None:
|
||||
@ -165,7 +211,7 @@ class Message(BaseMessage):
|
||||
AssistantMessage(
|
||||
id=self.id,
|
||||
date=self.created_at,
|
||||
assistant_message=message_string,
|
||||
content=message_string,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@ -221,7 +267,7 @@ class Message(BaseMessage):
|
||||
UserMessage(
|
||||
id=self.id,
|
||||
date=self.created_at,
|
||||
message=self.text,
|
||||
content=self.text,
|
||||
)
|
||||
)
|
||||
elif self.role == MessageRole.system:
|
||||
@ -231,7 +277,7 @@ class Message(BaseMessage):
|
||||
SystemMessage(
|
||||
id=self.id,
|
||||
date=self.created_at,
|
||||
message=self.text,
|
||||
content=self.text,
|
||||
)
|
||||
)
|
||||
else:
|
||||
@ -283,7 +329,7 @@ class Message(BaseMessage):
|
||||
model=model,
|
||||
# standard fields expected in an OpenAI ChatCompletion message object
|
||||
role=MessageRole.tool, # NOTE
|
||||
text=openai_message_dict["content"],
|
||||
content=[TextContent(text=openai_message_dict["content"])],
|
||||
name=openai_message_dict["name"] if "name" in openai_message_dict else None,
|
||||
tool_calls=openai_message_dict["tool_calls"] if "tool_calls" in openai_message_dict else None,
|
||||
tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
|
||||
@ -296,7 +342,7 @@ class Message(BaseMessage):
|
||||
model=model,
|
||||
# standard fields expected in an OpenAI ChatCompletion message object
|
||||
role=MessageRole.tool, # NOTE
|
||||
text=openai_message_dict["content"],
|
||||
content=[TextContent(text=openai_message_dict["content"])],
|
||||
name=openai_message_dict["name"] if "name" in openai_message_dict else None,
|
||||
tool_calls=openai_message_dict["tool_calls"] if "tool_calls" in openai_message_dict else None,
|
||||
tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
|
||||
@ -328,7 +374,7 @@ class Message(BaseMessage):
|
||||
model=model,
|
||||
# standard fields expected in an OpenAI ChatCompletion message object
|
||||
role=MessageRole(openai_message_dict["role"]),
|
||||
text=openai_message_dict["content"],
|
||||
content=[TextContent(text=openai_message_dict["content"])],
|
||||
name=openai_message_dict["name"] if "name" in openai_message_dict else None,
|
||||
tool_calls=tool_calls,
|
||||
tool_call_id=None, # NOTE: None, since this field is only non-null for role=='tool'
|
||||
@ -341,7 +387,7 @@ class Message(BaseMessage):
|
||||
model=model,
|
||||
# standard fields expected in an OpenAI ChatCompletion message object
|
||||
role=MessageRole(openai_message_dict["role"]),
|
||||
text=openai_message_dict["content"],
|
||||
content=[TextContent(text=openai_message_dict["content"])],
|
||||
name=openai_message_dict["name"] if "name" in openai_message_dict else None,
|
||||
tool_calls=tool_calls,
|
||||
tool_call_id=None, # NOTE: None, since this field is only non-null for role=='tool'
|
||||
@ -373,7 +419,7 @@ class Message(BaseMessage):
|
||||
model=model,
|
||||
# standard fields expected in an OpenAI ChatCompletion message object
|
||||
role=MessageRole(openai_message_dict["role"]),
|
||||
text=openai_message_dict["content"],
|
||||
content=[TextContent(text=openai_message_dict["content"])],
|
||||
name=openai_message_dict["name"] if "name" in openai_message_dict else None,
|
||||
tool_calls=tool_calls,
|
||||
tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
|
||||
@ -386,7 +432,7 @@ class Message(BaseMessage):
|
||||
model=model,
|
||||
# standard fields expected in an OpenAI ChatCompletion message object
|
||||
role=MessageRole(openai_message_dict["role"]),
|
||||
text=openai_message_dict["content"],
|
||||
content=[TextContent(text=openai_message_dict["content"] or "")],
|
||||
name=openai_message_dict["name"] if "name" in openai_message_dict else None,
|
||||
tool_calls=tool_calls,
|
||||
tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
|
||||
|
@ -104,7 +104,7 @@ class ChatCompletionRequest(BaseModel):
|
||||
logit_bias: Optional[Dict[str, int]] = None
|
||||
logprobs: Optional[bool] = False
|
||||
top_logprobs: Optional[int] = None
|
||||
max_tokens: Optional[int] = None
|
||||
max_completion_tokens: Optional[int] = None
|
||||
n: Optional[int] = 1
|
||||
presence_penalty: Optional[float] = 0
|
||||
response_format: Optional[ResponseFormat] = None
|
||||
|
@ -23,7 +23,7 @@ class PassageBase(OrmMetadataBase):
|
||||
|
||||
# file association
|
||||
file_id: Optional[str] = Field(None, description="The unique identifier of the file associated with the passage.")
|
||||
metadata: Optional[Dict] = Field({}, description="The metadata of the passage.")
|
||||
metadata: Optional[Dict] = Field({}, validation_alias="metadata_", description="The metadata of the passage.")
|
||||
|
||||
|
||||
class Passage(PassageBase):
|
||||
|
@ -7,8 +7,10 @@ from letta.constants import LLM_MAX_TOKENS, MIN_CONTEXT_WINDOW
|
||||
from letta.llm_api.azure_openai import get_azure_chat_completions_endpoint, get_azure_embeddings_endpoint
|
||||
from letta.llm_api.azure_openai_constants import AZURE_MODEL_TO_CONTEXT_LENGTH
|
||||
from letta.schemas.embedding_config import EmbeddingConfig
|
||||
from letta.schemas.embedding_config_overrides import EMBEDDING_HANDLE_OVERRIDES
|
||||
from letta.schemas.letta_base import LettaBase
|
||||
from letta.schemas.llm_config import LLMConfig
|
||||
from letta.schemas.llm_config_overrides import LLM_HANDLE_OVERRIDES
|
||||
|
||||
|
||||
class ProviderBase(LettaBase):
|
||||
@ -39,7 +41,21 @@ class Provider(ProviderBase):
|
||||
"""String representation of the provider for display purposes"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_handle(self, model_name: str) -> str:
|
||||
def get_handle(self, model_name: str, is_embedding: bool = False) -> str:
|
||||
"""
|
||||
Get the handle for a model, with support for custom overrides.
|
||||
|
||||
Args:
|
||||
model_name (str): The name of the model.
|
||||
is_embedding (bool, optional): Whether the handle is for an embedding model. Defaults to False.
|
||||
|
||||
Returns:
|
||||
str: The handle for the model.
|
||||
"""
|
||||
overrides = EMBEDDING_HANDLE_OVERRIDES if is_embedding else LLM_HANDLE_OVERRIDES
|
||||
if self.name in overrides and model_name in overrides[self.name]:
|
||||
model_name = overrides[self.name][model_name]
|
||||
|
||||
return f"{self.name}/{model_name}"
|
||||
|
||||
|
||||
@ -76,7 +92,7 @@ class LettaProvider(Provider):
|
||||
embedding_endpoint="https://embeddings.memgpt.ai",
|
||||
embedding_dim=1024,
|
||||
embedding_chunk_size=300,
|
||||
handle=self.get_handle("letta-free"),
|
||||
handle=self.get_handle("letta-free", is_embedding=True),
|
||||
)
|
||||
]
|
||||
|
||||
@ -167,7 +183,7 @@ class OpenAIProvider(Provider):
|
||||
embedding_endpoint="https://api.openai.com/v1",
|
||||
embedding_dim=1536,
|
||||
embedding_chunk_size=300,
|
||||
handle=self.get_handle("text-embedding-ada-002"),
|
||||
handle=self.get_handle("text-embedding-ada-002", is_embedding=True),
|
||||
),
|
||||
EmbeddingConfig(
|
||||
embedding_model="text-embedding-3-small",
|
||||
@ -175,7 +191,7 @@ class OpenAIProvider(Provider):
|
||||
embedding_endpoint="https://api.openai.com/v1",
|
||||
embedding_dim=2000,
|
||||
embedding_chunk_size=300,
|
||||
handle=self.get_handle("text-embedding-3-small"),
|
||||
handle=self.get_handle("text-embedding-3-small", is_embedding=True),
|
||||
),
|
||||
EmbeddingConfig(
|
||||
embedding_model="text-embedding-3-large",
|
||||
@ -183,7 +199,7 @@ class OpenAIProvider(Provider):
|
||||
embedding_endpoint="https://api.openai.com/v1",
|
||||
embedding_dim=2000,
|
||||
embedding_chunk_size=300,
|
||||
handle=self.get_handle("text-embedding-3-large"),
|
||||
handle=self.get_handle("text-embedding-3-large", is_embedding=True),
|
||||
),
|
||||
]
|
||||
|
||||
@ -377,7 +393,7 @@ class OllamaProvider(OpenAIProvider):
|
||||
embedding_endpoint=self.base_url,
|
||||
embedding_dim=embedding_dim,
|
||||
embedding_chunk_size=300,
|
||||
handle=self.get_handle(model["name"]),
|
||||
handle=self.get_handle(model["name"], is_embedding=True),
|
||||
)
|
||||
)
|
||||
return configs
|
||||
@ -575,7 +591,7 @@ class GoogleAIProvider(Provider):
|
||||
embedding_endpoint=self.base_url,
|
||||
embedding_dim=768,
|
||||
embedding_chunk_size=300, # NOTE: max is 2048
|
||||
handle=self.get_handle(model),
|
||||
handle=self.get_handle(model, is_embedding=True),
|
||||
)
|
||||
)
|
||||
return configs
|
||||
@ -641,7 +657,7 @@ class AzureProvider(Provider):
|
||||
embedding_endpoint=model_endpoint,
|
||||
embedding_dim=768,
|
||||
embedding_chunk_size=300, # NOTE: max is 2048
|
||||
handle=self.get_handle(model_name),
|
||||
handle=self.get_handle(model_name, is_embedding=True),
|
||||
)
|
||||
)
|
||||
return configs
|
||||
|
@ -33,7 +33,7 @@ class Source(BaseSource):
|
||||
description: Optional[str] = Field(None, description="The description of the source.")
|
||||
embedding_config: EmbeddingConfig = Field(..., description="The embedding configuration used by the source.")
|
||||
organization_id: Optional[str] = Field(None, description="The ID of the organization that created the source.")
|
||||
metadata: Optional[dict] = Field(None, description="Metadata associated with the source.")
|
||||
metadata: Optional[dict] = Field(None, validation_alias="metadata_", description="Metadata associated with the source.")
|
||||
|
||||
# metadata fields
|
||||
created_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
|
||||
|
@ -12,7 +12,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
|
||||
from letta.__init__ import __version__
|
||||
from letta.constants import ADMIN_PREFIX, API_PREFIX, OPENAI_API_PREFIX
|
||||
from letta.constants import ADMIN_PREFIX, API_PREFIX
|
||||
from letta.errors import BedrockPermissionError, LettaAgentNotFoundError, LettaUserNotFoundError
|
||||
from letta.log import get_logger
|
||||
from letta.orm.errors import DatabaseTimeoutError, ForeignKeyConstraintViolationError, NoResultFound, UniqueConstraintViolationError
|
||||
@ -49,9 +49,12 @@ password = None
|
||||
# #typer.secho(f"Generated admin server password for this session: {password}", fg=typer.colors.GREEN)
|
||||
|
||||
import logging
|
||||
import platform
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
is_windows = platform.system() == "Windows"
|
||||
|
||||
log = logging.getLogger("uvicorn")
|
||||
|
||||
|
||||
@ -285,8 +288,14 @@ def start_server(
|
||||
ssl_certfile="certs/localhost.pem",
|
||||
)
|
||||
else:
|
||||
print(f"▶ Server running at: http://{host or 'localhost'}:{port or REST_DEFAULT_PORT}")
|
||||
print(f"▶ View using ADE at: https://app.letta.com/development-servers/local/dashboard\n")
|
||||
if is_windows:
|
||||
# Windows doesn't those the fancy unicode characters
|
||||
print(f"Server running at: http://{host or 'localhost'}:{port or REST_DEFAULT_PORT}")
|
||||
print(f"View using ADE at: https://app.letta.com/development-servers/local/dashboard\n")
|
||||
else:
|
||||
print(f"▶ Server running at: http://{host or 'localhost'}:{port or REST_DEFAULT_PORT}")
|
||||
print(f"▶ View using ADE at: https://app.letta.com/development-servers/local/dashboard\n")
|
||||
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=host or "localhost",
|
||||
|
@ -472,7 +472,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
||||
processed_chunk = AssistantMessage(
|
||||
id=message_id,
|
||||
date=message_date,
|
||||
assistant_message=cleaned_func_args,
|
||||
content=cleaned_func_args,
|
||||
)
|
||||
|
||||
# otherwise we just do a regular passthrough of a ToolCallDelta via a ToolCallMessage
|
||||
@ -613,7 +613,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
||||
processed_chunk = AssistantMessage(
|
||||
id=message_id,
|
||||
date=message_date,
|
||||
assistant_message=combined_chunk,
|
||||
content=combined_chunk,
|
||||
)
|
||||
# Store the ID of the tool call so allow skipping the corresponding response
|
||||
if self.function_id_buffer:
|
||||
@ -627,7 +627,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
||||
processed_chunk = AssistantMessage(
|
||||
id=message_id,
|
||||
date=message_date,
|
||||
assistant_message=updates_main_json,
|
||||
content=updates_main_json,
|
||||
)
|
||||
# Store the ID of the tool call so allow skipping the corresponding response
|
||||
if self.function_id_buffer:
|
||||
@ -959,7 +959,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
||||
processed_chunk = AssistantMessage(
|
||||
id=msg_obj.id,
|
||||
date=msg_obj.created_at,
|
||||
assistant_message=func_args["message"],
|
||||
content=func_args["message"],
|
||||
)
|
||||
self._push_to_buffer(processed_chunk)
|
||||
except Exception as e:
|
||||
@ -981,7 +981,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
||||
processed_chunk = AssistantMessage(
|
||||
id=msg_obj.id,
|
||||
date=msg_obj.created_at,
|
||||
assistant_message=func_args[self.assistant_message_tool_kwarg],
|
||||
content=func_args[self.assistant_message_tool_kwarg],
|
||||
)
|
||||
# Store the ID of the tool call so allow skipping the corresponding response
|
||||
self.prev_assistant_message_id = function_call.id
|
||||
@ -1018,8 +1018,6 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
||||
# new_message = {"function_return": msg, "status": "success"}
|
||||
assert msg_obj.tool_call_id is not None
|
||||
|
||||
print(f"YYY printing the function call - {msg_obj.tool_call_id} == {self.prev_assistant_message_id} ???")
|
||||
|
||||
# Skip this is use_assistant_message is on
|
||||
if self.use_assistant_message and msg_obj.tool_call_id == self.prev_assistant_message_id:
|
||||
# Wipe the cache
|
||||
|
@ -43,7 +43,8 @@ def list_agents(
|
||||
),
|
||||
server: "SyncServer" = Depends(get_letta_server),
|
||||
user_id: Optional[str] = Header(None, alias="user_id"),
|
||||
cursor: Optional[str] = Query(None, description="Cursor for pagination"),
|
||||
before: Optional[str] = Query(None, description="Cursor for pagination"),
|
||||
after: Optional[str] = Query(None, description="Cursor for pagination"),
|
||||
limit: Optional[int] = Query(None, description="Limit for pagination"),
|
||||
query_text: Optional[str] = Query(None, description="Search agents by name"),
|
||||
):
|
||||
@ -66,7 +67,7 @@ def list_agents(
|
||||
}
|
||||
|
||||
# Call list_agents with the dynamic kwargs
|
||||
agents = server.agent_manager.list_agents(actor=actor, cursor=cursor, limit=limit, **kwargs)
|
||||
agents = server.agent_manager.list_agents(actor=actor, before=before, after=after, limit=limit, **kwargs)
|
||||
return agents
|
||||
|
||||
|
||||
@ -347,14 +348,11 @@ def list_archival_memory(
|
||||
"""
|
||||
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
||||
|
||||
# TODO need to add support for non-postgres here
|
||||
# chroma will throw:
|
||||
# raise ValueError("Cannot run get_all_cursor with chroma")
|
||||
|
||||
return server.get_agent_archival_cursor(
|
||||
return server.get_agent_archival(
|
||||
user_id=actor.id,
|
||||
agent_id=agent_id,
|
||||
cursor=after, # TODO: deleting before, after. is this expected?
|
||||
after=after,
|
||||
before=before,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
@ -429,7 +427,7 @@ def list_messages(
|
||||
"""
|
||||
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
||||
|
||||
return server.get_agent_recall_cursor(
|
||||
return server.get_agent_recall(
|
||||
user_id=actor.id,
|
||||
agent_id=agent_id,
|
||||
before=before,
|
||||
@ -560,9 +558,6 @@ async def process_message_background(
|
||||
)
|
||||
server.job_manager.update_job_by_id(job_id=job_id, job_update=job_update, actor=actor)
|
||||
|
||||
# Add job usage statistics
|
||||
server.job_manager.add_job_usage(job_id=job_id, usage=result.usage, actor=actor)
|
||||
|
||||
except Exception as e:
|
||||
# Update job status to failed
|
||||
job_update = JobUpdate(
|
||||
|
@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, List, Optional
|
||||
from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query
|
||||
|
||||
from letta.orm.errors import NoResultFound
|
||||
from letta.schemas.agent import AgentState
|
||||
from letta.schemas.block import Block, BlockUpdate, CreateBlock
|
||||
from letta.server.rest_api.utils import get_letta_server
|
||||
from letta.server.server import SyncServer
|
||||
@ -73,3 +74,21 @@ def retrieve_block(
|
||||
return block
|
||||
except NoResultFound:
|
||||
raise HTTPException(status_code=404, detail="Block not found")
|
||||
|
||||
|
||||
@router.get("/{block_id}/agents", response_model=List[AgentState], operation_id="list_agents_for_block")
|
||||
def list_agents_for_block(
|
||||
block_id: str,
|
||||
server: SyncServer = Depends(get_letta_server),
|
||||
user_id: Optional[str] = Header(None, alias="user_id"),
|
||||
):
|
||||
"""
|
||||
Retrieves all agents associated with the specified block.
|
||||
Raises a 404 if the block does not exist.
|
||||
"""
|
||||
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
||||
try:
|
||||
agents = server.block_manager.get_agents_for_block(block_id=block_id, actor=actor)
|
||||
return agents
|
||||
except NoResultFound:
|
||||
raise HTTPException(status_code=404, detail=f"Block with id={block_id} not found")
|
||||
|
@ -14,7 +14,7 @@ router = APIRouter(prefix="/orgs", tags=["organization", "admin"])
|
||||
|
||||
@router.get("/", tags=["admin"], response_model=List[Organization], operation_id="list_orgs")
|
||||
def get_all_orgs(
|
||||
cursor: Optional[str] = Query(None),
|
||||
after: Optional[str] = Query(None),
|
||||
limit: Optional[int] = Query(50),
|
||||
server: "SyncServer" = Depends(get_letta_server),
|
||||
):
|
||||
@ -22,7 +22,7 @@ def get_all_orgs(
|
||||
Get a list of all orgs in the database
|
||||
"""
|
||||
try:
|
||||
orgs = server.organization_manager.list_organizations(cursor=cursor, limit=limit)
|
||||
orgs = server.organization_manager.list_organizations(after=after, limit=limit)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
@ -13,7 +13,7 @@ router = APIRouter(prefix="/providers", tags=["providers"])
|
||||
|
||||
@router.get("/", tags=["providers"], response_model=List[Provider], operation_id="list_providers")
|
||||
def list_providers(
|
||||
cursor: Optional[str] = Query(None),
|
||||
after: Optional[str] = Query(None),
|
||||
limit: Optional[int] = Query(50),
|
||||
server: "SyncServer" = Depends(get_letta_server),
|
||||
):
|
||||
@ -21,7 +21,7 @@ def list_providers(
|
||||
Get a list of all custom providers in the database
|
||||
"""
|
||||
try:
|
||||
providers = server.provider_manager.list_providers(cursor=cursor, limit=limit)
|
||||
providers = server.provider_manager.list_providers(after=after, limit=limit)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
@ -75,9 +75,12 @@ async def list_run_messages(
|
||||
run_id: str,
|
||||
server: "SyncServer" = Depends(get_letta_server),
|
||||
user_id: Optional[str] = Header(None, alias="user_id"),
|
||||
cursor: Optional[str] = Query(None, description="Cursor for pagination"),
|
||||
before: Optional[str] = Query(None, description="Cursor for pagination"),
|
||||
after: Optional[str] = Query(None, description="Cursor for pagination"),
|
||||
limit: Optional[int] = Query(100, description="Maximum number of messages to return"),
|
||||
ascending: bool = Query(True, description="Sort order by creation time"),
|
||||
order: str = Query(
|
||||
"desc", description="Sort order by the created_at timestamp of the objects. asc for ascending order and desc for descending order."
|
||||
),
|
||||
role: Optional[MessageRole] = Query(None, description="Filter by role"),
|
||||
):
|
||||
"""
|
||||
@ -85,9 +88,10 @@ async def list_run_messages(
|
||||
|
||||
Args:
|
||||
run_id: ID of the run
|
||||
cursor: Cursor for pagination
|
||||
before: A cursor for use in pagination. `before` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, starting with obj_foo, your subsequent call can include before=obj_foo in order to fetch the previous page of the list.
|
||||
after: A cursor for use in pagination. `after` is an object ID that defines your place in the list. For instance, if you make a list request and receive 100 objects, ending with obj_foo, your subsequent call can include after=obj_foo in order to fetch the next page of the list.
|
||||
limit: Maximum number of messages to return
|
||||
ascending: Sort order by creation time
|
||||
order: Sort order by the created_at timestamp of the objects. asc for ascending order and desc for descending order.
|
||||
role: Filter by role (user/assistant/system/tool)
|
||||
return_message_object: Whether to return Message objects or LettaMessage objects
|
||||
user_id: ID of the user making the request
|
||||
@ -95,15 +99,19 @@ async def list_run_messages(
|
||||
Returns:
|
||||
A list of messages associated with the run. Default is List[LettaMessage].
|
||||
"""
|
||||
if order not in ["asc", "desc"]:
|
||||
raise HTTPException(status_code=400, detail="Order must be 'asc' or 'desc'")
|
||||
|
||||
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
||||
|
||||
try:
|
||||
messages = server.job_manager.get_run_messages_cursor(
|
||||
messages = server.job_manager.get_run_messages(
|
||||
run_id=run_id,
|
||||
actor=actor,
|
||||
limit=limit,
|
||||
cursor=cursor,
|
||||
ascending=ascending,
|
||||
before=before,
|
||||
after=after,
|
||||
ascending=(order == "asc"),
|
||||
role=role,
|
||||
)
|
||||
return messages
|
||||
|
@ -68,13 +68,13 @@ def delete_sandbox_config(
|
||||
@router.get("/", response_model=List[PydanticSandboxConfig])
|
||||
def list_sandbox_configs(
|
||||
limit: int = Query(1000, description="Number of results to return"),
|
||||
cursor: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
|
||||
after: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
|
||||
sandbox_type: Optional[SandboxType] = Query(None, description="Filter for this specific sandbox type"),
|
||||
server: SyncServer = Depends(get_letta_server),
|
||||
user_id: str = Depends(get_user_id),
|
||||
):
|
||||
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
||||
return server.sandbox_config_manager.list_sandbox_configs(actor, limit=limit, cursor=cursor, sandbox_type=sandbox_type)
|
||||
return server.sandbox_config_manager.list_sandbox_configs(actor, limit=limit, after=after, sandbox_type=sandbox_type)
|
||||
|
||||
|
||||
### Sandbox Environment Variable Routes
|
||||
@ -116,9 +116,9 @@ def delete_sandbox_env_var(
|
||||
def list_sandbox_env_vars(
|
||||
sandbox_config_id: str,
|
||||
limit: int = Query(1000, description="Number of results to return"),
|
||||
cursor: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
|
||||
after: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
|
||||
server: SyncServer = Depends(get_letta_server),
|
||||
user_id: str = Depends(get_user_id),
|
||||
):
|
||||
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
||||
return server.sandbox_config_manager.list_sandbox_env_vars(sandbox_config_id, actor, limit=limit, cursor=cursor)
|
||||
return server.sandbox_config_manager.list_sandbox_env_vars(sandbox_config_id, actor, limit=limit, after=after)
|
||||
|
@ -165,7 +165,7 @@ def list_source_passages(
|
||||
def list_source_files(
|
||||
source_id: str,
|
||||
limit: int = Query(1000, description="Number of files to return"),
|
||||
cursor: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
|
||||
after: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
|
||||
server: "SyncServer" = Depends(get_letta_server),
|
||||
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
||||
):
|
||||
@ -173,7 +173,7 @@ def list_source_files(
|
||||
List paginated files associated with a data source.
|
||||
"""
|
||||
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
||||
return server.source_manager.list_files(source_id=source_id, limit=limit, cursor=cursor, actor=actor)
|
||||
return server.source_manager.list_files(source_id=source_id, limit=limit, after=after, actor=actor)
|
||||
|
||||
|
||||
# it's redundant to include /delete in the URL path. The HTTP verb DELETE already implies that action.
|
||||
|
@ -13,7 +13,7 @@ router = APIRouter(prefix="/tags", tags=["tag", "admin"])
|
||||
|
||||
@router.get("/", tags=["admin"], response_model=List[str], operation_id="list_tags")
|
||||
def list_tags(
|
||||
cursor: Optional[str] = Query(None),
|
||||
after: Optional[str] = Query(None),
|
||||
limit: Optional[int] = Query(50),
|
||||
server: "SyncServer" = Depends(get_letta_server),
|
||||
query_text: Optional[str] = Query(None),
|
||||
@ -23,5 +23,5 @@ def list_tags(
|
||||
Get a list of all tags in the database
|
||||
"""
|
||||
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
||||
tags = server.agent_manager.list_tags(actor=actor, cursor=cursor, limit=limit, query_text=query_text)
|
||||
tags = server.agent_manager.list_tags(actor=actor, after=after, limit=limit, query_text=query_text)
|
||||
return tags
|
||||
|
@ -50,7 +50,7 @@ def retrieve_tool(
|
||||
|
||||
@router.get("/", response_model=List[Tool], operation_id="list_tools")
|
||||
def list_tools(
|
||||
cursor: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
limit: Optional[int] = 50,
|
||||
server: SyncServer = Depends(get_letta_server),
|
||||
user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
||||
@ -60,7 +60,7 @@ def list_tools(
|
||||
"""
|
||||
try:
|
||||
actor = server.user_manager.get_user_or_default(user_id=user_id)
|
||||
return server.tool_manager.list_tools(actor=actor, cursor=cursor, limit=limit)
|
||||
return server.tool_manager.list_tools(actor=actor, after=after, limit=limit)
|
||||
except Exception as e:
|
||||
# Log or print the full exception here for debugging
|
||||
print(f"Error occurred: {e}")
|
||||
|
@ -15,7 +15,7 @@ router = APIRouter(prefix="/users", tags=["users", "admin"])
|
||||
|
||||
@router.get("/", tags=["admin"], response_model=List[User], operation_id="list_users")
|
||||
def list_users(
|
||||
cursor: Optional[str] = Query(None),
|
||||
after: Optional[str] = Query(None),
|
||||
limit: Optional[int] = Query(50),
|
||||
server: "SyncServer" = Depends(get_letta_server),
|
||||
):
|
||||
@ -23,7 +23,7 @@ def list_users(
|
||||
Get a list of all users in the database
|
||||
"""
|
||||
try:
|
||||
next_cursor, users = server.user_manager.list_users(cursor=cursor, limit=limit)
|
||||
users = server.user_manager.list_users(after=after, limit=limit)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
@ -1,5 +1,6 @@
|
||||
# inspecting tools
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
import warnings
|
||||
@ -38,7 +39,7 @@ from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage, ToolRe
|
||||
from letta.schemas.letta_response import LettaResponse
|
||||
from letta.schemas.llm_config import LLMConfig
|
||||
from letta.schemas.memory import ArchivalMemorySummary, ContextWindowOverview, Memory, RecallMemorySummary
|
||||
from letta.schemas.message import Message, MessageCreate, MessageRole, MessageUpdate
|
||||
from letta.schemas.message import Message, MessageCreate, MessageRole, MessageUpdate, TextContent
|
||||
from letta.schemas.organization import Organization
|
||||
from letta.schemas.passage import Passage
|
||||
from letta.schemas.providers import (
|
||||
@ -616,14 +617,14 @@ class SyncServer(Server):
|
||||
message = Message(
|
||||
agent_id=agent_id,
|
||||
role="user",
|
||||
text=packaged_user_message,
|
||||
content=[TextContent(text=packaged_user_message)],
|
||||
created_at=timestamp,
|
||||
)
|
||||
else:
|
||||
message = Message(
|
||||
agent_id=agent_id,
|
||||
role="user",
|
||||
text=packaged_user_message,
|
||||
content=[TextContent(text=packaged_user_message)],
|
||||
)
|
||||
|
||||
# Run the agent state forward
|
||||
@ -666,14 +667,14 @@ class SyncServer(Server):
|
||||
message = Message(
|
||||
agent_id=agent_id,
|
||||
role="system",
|
||||
text=packaged_system_message,
|
||||
content=[TextContent(text=packaged_system_message)],
|
||||
created_at=timestamp,
|
||||
)
|
||||
else:
|
||||
message = Message(
|
||||
agent_id=agent_id,
|
||||
role="system",
|
||||
text=packaged_system_message,
|
||||
content=[TextContent(text=packaged_system_message)],
|
||||
)
|
||||
|
||||
if isinstance(message, Message):
|
||||
@ -720,9 +721,9 @@ class SyncServer(Server):
|
||||
|
||||
# If wrapping is eanbled, wrap with metadata before placing content inside the Message object
|
||||
if message.role == MessageRole.user and wrap_user_message:
|
||||
message.text = system.package_user_message(user_message=message.text)
|
||||
message.content = system.package_user_message(user_message=message.content)
|
||||
elif message.role == MessageRole.system and wrap_system_message:
|
||||
message.text = system.package_system_message(system_message=message.text)
|
||||
message.content = system.package_system_message(system_message=message.content)
|
||||
else:
|
||||
raise ValueError(f"Invalid message role: {message.role}")
|
||||
|
||||
@ -731,7 +732,7 @@ class SyncServer(Server):
|
||||
Message(
|
||||
agent_id=agent_id,
|
||||
role=message.role,
|
||||
text=message.text,
|
||||
content=[TextContent(text=message.content)],
|
||||
name=message.name,
|
||||
# assigned later?
|
||||
model=None,
|
||||
@ -804,20 +805,12 @@ class SyncServer(Server):
|
||||
def get_recall_memory_summary(self, agent_id: str, actor: User) -> RecallMemorySummary:
|
||||
return RecallMemorySummary(size=self.message_manager.size(actor=actor, agent_id=agent_id))
|
||||
|
||||
def get_agent_archival(self, user_id: str, agent_id: str, cursor: Optional[str] = None, limit: int = 50) -> List[Passage]:
|
||||
"""Paginated query of all messages in agent archival memory"""
|
||||
# TODO: Thread actor directly through this function, since the top level caller most likely already retrieved the user
|
||||
actor = self.user_manager.get_user_or_default(user_id=user_id)
|
||||
|
||||
passages = self.agent_manager.list_passages(agent_id=agent_id, actor=actor)
|
||||
|
||||
return passages
|
||||
|
||||
def get_agent_archival_cursor(
|
||||
def get_agent_archival(
|
||||
self,
|
||||
user_id: str,
|
||||
agent_id: str,
|
||||
cursor: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
before: Optional[str] = None,
|
||||
limit: Optional[int] = 100,
|
||||
order_by: Optional[str] = "created_at",
|
||||
reverse: Optional[bool] = False,
|
||||
@ -829,7 +822,8 @@ class SyncServer(Server):
|
||||
records = self.agent_manager.list_passages(
|
||||
actor=actor,
|
||||
agent_id=agent_id,
|
||||
cursor=cursor,
|
||||
after=after,
|
||||
before=before,
|
||||
limit=limit,
|
||||
ascending=not reverse,
|
||||
)
|
||||
@ -851,7 +845,7 @@ class SyncServer(Server):
|
||||
|
||||
# TODO: return archival memory
|
||||
|
||||
def get_agent_recall_cursor(
|
||||
def get_agent_recall(
|
||||
self,
|
||||
user_id: str,
|
||||
agent_id: str,
|
||||
@ -1047,13 +1041,14 @@ class SyncServer(Server):
|
||||
|
||||
def list_llm_models(self) -> List[LLMConfig]:
|
||||
"""List available models"""
|
||||
|
||||
llm_models = []
|
||||
for provider in self.get_enabled_providers():
|
||||
try:
|
||||
llm_models.extend(provider.list_llm_models())
|
||||
except Exception as e:
|
||||
warnings.warn(f"An error occurred while listing LLM models for provider {provider}: {e}")
|
||||
|
||||
llm_models.extend(self.get_local_llm_configs())
|
||||
return llm_models
|
||||
|
||||
def list_embedding_models(self) -> List[EmbeddingConfig]:
|
||||
@ -1073,12 +1068,22 @@ class SyncServer(Server):
|
||||
return {**providers_from_env, **providers_from_db}.values()
|
||||
|
||||
def get_llm_config_from_handle(self, handle: str, context_window_limit: Optional[int] = None) -> LLMConfig:
|
||||
provider_name, model_name = handle.split("/", 1)
|
||||
provider = self.get_provider_from_name(provider_name)
|
||||
try:
|
||||
provider_name, model_name = handle.split("/", 1)
|
||||
provider = self.get_provider_from_name(provider_name)
|
||||
|
||||
llm_configs = [config for config in provider.list_llm_models() if config.model == model_name]
|
||||
if not llm_configs:
|
||||
raise ValueError(f"LLM model {model_name} is not supported by {provider_name}")
|
||||
llm_configs = [config for config in provider.list_llm_models() if config.handle == handle]
|
||||
if not llm_configs:
|
||||
llm_configs = [config for config in provider.list_llm_models() if config.model == model_name]
|
||||
if not llm_configs:
|
||||
raise ValueError(f"LLM model {model_name} is not supported by {provider_name}")
|
||||
except ValueError as e:
|
||||
llm_configs = [config for config in self.get_local_llm_configs() if config.handle == handle]
|
||||
if not llm_configs:
|
||||
raise e
|
||||
|
||||
if len(llm_configs) == 1:
|
||||
llm_config = llm_configs[0]
|
||||
elif len(llm_configs) > 1:
|
||||
raise ValueError(f"Multiple LLM models with name {model_name} supported by {provider_name}")
|
||||
else:
|
||||
@ -1097,13 +1102,17 @@ class SyncServer(Server):
|
||||
provider_name, model_name = handle.split("/", 1)
|
||||
provider = self.get_provider_from_name(provider_name)
|
||||
|
||||
embedding_configs = [config for config in provider.list_embedding_models() if config.embedding_model == model_name]
|
||||
if not embedding_configs:
|
||||
raise ValueError(f"Embedding model {model_name} is not supported by {provider_name}")
|
||||
elif len(embedding_configs) > 1:
|
||||
raise ValueError(f"Multiple embedding models with name {model_name} supported by {provider_name}")
|
||||
else:
|
||||
embedding_configs = [config for config in provider.list_embedding_models() if config.handle == handle]
|
||||
if len(embedding_configs) == 1:
|
||||
embedding_config = embedding_configs[0]
|
||||
else:
|
||||
embedding_configs = [config for config in provider.list_embedding_models() if config.embedding_model == model_name]
|
||||
if not embedding_configs:
|
||||
raise ValueError(f"Embedding model {model_name} is not supported by {provider_name}")
|
||||
elif len(embedding_configs) > 1:
|
||||
raise ValueError(f"Multiple embedding models with name {model_name} supported by {provider_name}")
|
||||
else:
|
||||
embedding_config = embedding_configs[0]
|
||||
|
||||
if embedding_chunk_size:
|
||||
embedding_config.embedding_chunk_size = embedding_chunk_size
|
||||
@ -1121,6 +1130,25 @@ class SyncServer(Server):
|
||||
|
||||
return provider
|
||||
|
||||
def get_local_llm_configs(self):
|
||||
llm_models = []
|
||||
try:
|
||||
llm_configs_dir = os.path.expanduser("~/.letta/llm_configs")
|
||||
if os.path.exists(llm_configs_dir):
|
||||
for filename in os.listdir(llm_configs_dir):
|
||||
if filename.endswith(".json"):
|
||||
filepath = os.path.join(llm_configs_dir, filename)
|
||||
try:
|
||||
with open(filepath, "r") as f:
|
||||
config_data = json.load(f)
|
||||
llm_config = LLMConfig(**config_data)
|
||||
llm_models.append(llm_config)
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
warnings.warn(f"Error parsing LLM config file {filename}: {e}")
|
||||
except Exception as e:
|
||||
warnings.warn(f"Error reading LLM configs directory: {e}")
|
||||
return llm_models
|
||||
|
||||
def add_llm_model(self, request: LLMConfig) -> LLMConfig:
|
||||
"""Add a new LLM model"""
|
||||
|
||||
|
@ -2,7 +2,7 @@ from datetime import datetime
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import numpy as np
|
||||
from sqlalchemy import Select, func, literal, select, union_all
|
||||
from sqlalchemy import Select, and_, func, literal, or_, select, union_all
|
||||
|
||||
from letta.constants import BASE_MEMORY_TOOLS, BASE_TOOLS, MAX_EMBEDDING_DIM, MULTI_AGENT_TOOLS
|
||||
from letta.embeddings import embedding_model
|
||||
@ -271,10 +271,11 @@ class AgentManager:
|
||||
def list_agents(
|
||||
self,
|
||||
actor: PydanticUser,
|
||||
before: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
limit: Optional[int] = 50,
|
||||
tags: Optional[List[str]] = None,
|
||||
match_all_tags: bool = False,
|
||||
cursor: Optional[str] = None,
|
||||
limit: Optional[int] = 50,
|
||||
query_text: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> List[PydanticAgentState]:
|
||||
@ -284,10 +285,11 @@ class AgentManager:
|
||||
with self.session_maker() as session:
|
||||
agents = AgentModel.list(
|
||||
db_session=session,
|
||||
before=before,
|
||||
after=after,
|
||||
limit=limit,
|
||||
tags=tags,
|
||||
match_all_tags=match_all_tags,
|
||||
cursor=cursor,
|
||||
limit=limit,
|
||||
organization_id=actor.organization_id if actor else None,
|
||||
query_text=query_text,
|
||||
**kwargs,
|
||||
@ -723,7 +725,8 @@ class AgentManager:
|
||||
query_text: Optional[str] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
cursor: Optional[str] = None,
|
||||
before: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
source_id: Optional[str] = None,
|
||||
embed_query: bool = False,
|
||||
ascending: bool = True,
|
||||
@ -731,6 +734,7 @@ class AgentManager:
|
||||
agent_only: bool = False,
|
||||
) -> Select:
|
||||
"""Helper function to build the base passage query with all filters applied.
|
||||
Supports both before and after pagination across merged source and agent passages.
|
||||
|
||||
Returns the query before any limit or count operations are applied.
|
||||
"""
|
||||
@ -818,30 +822,69 @@ class AgentManager:
|
||||
else:
|
||||
# SQLite with custom vector type
|
||||
query_embedding_binary = adapt_array(embedded_text)
|
||||
if ascending:
|
||||
main_query = main_query.order_by(
|
||||
func.cosine_distance(combined_query.c.embedding, query_embedding_binary).asc(),
|
||||
combined_query.c.created_at.asc(),
|
||||
combined_query.c.id.asc(),
|
||||
)
|
||||
else:
|
||||
main_query = main_query.order_by(
|
||||
func.cosine_distance(combined_query.c.embedding, query_embedding_binary).asc(),
|
||||
combined_query.c.created_at.desc(),
|
||||
combined_query.c.id.asc(),
|
||||
)
|
||||
main_query = main_query.order_by(
|
||||
func.cosine_distance(combined_query.c.embedding, query_embedding_binary).asc(),
|
||||
combined_query.c.created_at.asc() if ascending else combined_query.c.created_at.desc(),
|
||||
combined_query.c.id.asc(),
|
||||
)
|
||||
else:
|
||||
if query_text:
|
||||
main_query = main_query.where(func.lower(combined_query.c.text).contains(func.lower(query_text)))
|
||||
|
||||
# Handle cursor-based pagination
|
||||
if cursor:
|
||||
cursor_query = select(combined_query.c.created_at).where(combined_query.c.id == cursor).scalar_subquery()
|
||||
# Handle pagination
|
||||
if before or after:
|
||||
# Create reference CTEs
|
||||
if before:
|
||||
before_ref = (
|
||||
select(combined_query.c.created_at, combined_query.c.id).where(combined_query.c.id == before).cte("before_ref")
|
||||
)
|
||||
if after:
|
||||
after_ref = (
|
||||
select(combined_query.c.created_at, combined_query.c.id).where(combined_query.c.id == after).cte("after_ref")
|
||||
)
|
||||
|
||||
if ascending:
|
||||
main_query = main_query.where(combined_query.c.created_at > cursor_query)
|
||||
if before and after:
|
||||
# Window-based query (get records between before and after)
|
||||
main_query = main_query.where(
|
||||
or_(
|
||||
combined_query.c.created_at < select(before_ref.c.created_at).scalar_subquery(),
|
||||
and_(
|
||||
combined_query.c.created_at == select(before_ref.c.created_at).scalar_subquery(),
|
||||
combined_query.c.id < select(before_ref.c.id).scalar_subquery(),
|
||||
),
|
||||
)
|
||||
)
|
||||
main_query = main_query.where(
|
||||
or_(
|
||||
combined_query.c.created_at > select(after_ref.c.created_at).scalar_subquery(),
|
||||
and_(
|
||||
combined_query.c.created_at == select(after_ref.c.created_at).scalar_subquery(),
|
||||
combined_query.c.id > select(after_ref.c.id).scalar_subquery(),
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
main_query = main_query.where(combined_query.c.created_at < cursor_query)
|
||||
# Pure pagination (only before or only after)
|
||||
if before:
|
||||
main_query = main_query.where(
|
||||
or_(
|
||||
combined_query.c.created_at < select(before_ref.c.created_at).scalar_subquery(),
|
||||
and_(
|
||||
combined_query.c.created_at == select(before_ref.c.created_at).scalar_subquery(),
|
||||
combined_query.c.id < select(before_ref.c.id).scalar_subquery(),
|
||||
),
|
||||
)
|
||||
)
|
||||
if after:
|
||||
main_query = main_query.where(
|
||||
or_(
|
||||
combined_query.c.created_at > select(after_ref.c.created_at).scalar_subquery(),
|
||||
and_(
|
||||
combined_query.c.created_at == select(after_ref.c.created_at).scalar_subquery(),
|
||||
combined_query.c.id > select(after_ref.c.id).scalar_subquery(),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# Add ordering if not already ordered by similarity
|
||||
if not embed_query:
|
||||
@ -856,7 +899,7 @@ class AgentManager:
|
||||
combined_query.c.id.asc(),
|
||||
)
|
||||
|
||||
return main_query
|
||||
return main_query
|
||||
|
||||
@enforce_types
|
||||
def list_passages(
|
||||
@ -868,7 +911,8 @@ class AgentManager:
|
||||
query_text: Optional[str] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
cursor: Optional[str] = None,
|
||||
before: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
source_id: Optional[str] = None,
|
||||
embed_query: bool = False,
|
||||
ascending: bool = True,
|
||||
@ -884,7 +928,8 @@ class AgentManager:
|
||||
query_text=query_text,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
cursor=cursor,
|
||||
before=before,
|
||||
after=after,
|
||||
source_id=source_id,
|
||||
embed_query=embed_query,
|
||||
ascending=ascending,
|
||||
@ -924,7 +969,8 @@ class AgentManager:
|
||||
query_text: Optional[str] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
cursor: Optional[str] = None,
|
||||
before: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
source_id: Optional[str] = None,
|
||||
embed_query: bool = False,
|
||||
ascending: bool = True,
|
||||
@ -940,7 +986,8 @@ class AgentManager:
|
||||
query_text=query_text,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
cursor=cursor,
|
||||
before=before,
|
||||
after=after,
|
||||
source_id=source_id,
|
||||
embed_query=embed_query,
|
||||
ascending=ascending,
|
||||
@ -1044,14 +1091,14 @@ class AgentManager:
|
||||
# ======================================================================================================================
|
||||
@enforce_types
|
||||
def list_tags(
|
||||
self, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50, query_text: Optional[str] = None
|
||||
self, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50, query_text: Optional[str] = None
|
||||
) -> List[str]:
|
||||
"""
|
||||
Get all tags a user has created, ordered alphabetically.
|
||||
|
||||
Args:
|
||||
actor: User performing the action.
|
||||
cursor: Cursor for pagination.
|
||||
after: Cursor for forward pagination.
|
||||
limit: Maximum number of tags to return.
|
||||
query_text: Query text to filter tags by.
|
||||
|
||||
@ -1069,8 +1116,8 @@ class AgentManager:
|
||||
if query_text:
|
||||
query = query.filter(AgentsTags.tag.ilike(f"%{query_text}%"))
|
||||
|
||||
if cursor:
|
||||
query = query.filter(AgentsTags.tag > cursor)
|
||||
if after:
|
||||
query = query.filter(AgentsTags.tag > after)
|
||||
|
||||
query = query.order_by(AgentsTags.tag).limit(limit)
|
||||
results = [tag[0] for tag in query.all()]
|
||||
|
@ -3,6 +3,7 @@ from typing import List, Optional
|
||||
|
||||
from letta.orm.block import Block as BlockModel
|
||||
from letta.orm.errors import NoResultFound
|
||||
from letta.schemas.agent import AgentState as PydanticAgentState
|
||||
from letta.schemas.block import Block
|
||||
from letta.schemas.block import Block as PydanticBlock
|
||||
from letta.schemas.block import BlockUpdate, Human, Persona
|
||||
@ -64,7 +65,7 @@ class BlockManager:
|
||||
is_template: Optional[bool] = None,
|
||||
template_name: Optional[str] = None,
|
||||
id: Optional[str] = None,
|
||||
cursor: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
limit: Optional[int] = 50,
|
||||
) -> List[PydanticBlock]:
|
||||
"""Retrieve blocks based on various optional filters."""
|
||||
@ -80,7 +81,7 @@ class BlockManager:
|
||||
if id:
|
||||
filters["id"] = id
|
||||
|
||||
blocks = BlockModel.list(db_session=session, cursor=cursor, limit=limit, **filters)
|
||||
blocks = BlockModel.list(db_session=session, after=after, limit=limit, **filters)
|
||||
|
||||
return [block.to_pydantic() for block in blocks]
|
||||
|
||||
@ -114,3 +115,15 @@ class BlockManager:
|
||||
text = open(human_file, "r", encoding="utf-8").read()
|
||||
name = os.path.basename(human_file).replace(".txt", "")
|
||||
self.create_or_update_block(Human(template_name=name, value=text, is_template=True), actor=actor)
|
||||
|
||||
@enforce_types
|
||||
def get_agents_for_block(self, block_id: str, actor: PydanticUser) -> List[PydanticAgentState]:
|
||||
"""
|
||||
Retrieve all agents associated with a given block.
|
||||
"""
|
||||
with self.session_maker() as session:
|
||||
block = BlockModel.read(db_session=session, identifier=block_id, actor=actor)
|
||||
agents_orm = block.agents
|
||||
agents_pydantic = [agent.to_pydantic() for agent in agents_orm]
|
||||
|
||||
return agents_pydantic
|
||||
|
@ -11,7 +11,7 @@ from letta.prompts import gpt_system
|
||||
from letta.schemas.agent import AgentState, AgentType
|
||||
from letta.schemas.enums import MessageRole
|
||||
from letta.schemas.memory import Memory
|
||||
from letta.schemas.message import Message, MessageCreate
|
||||
from letta.schemas.message import Message, MessageCreate, TextContent
|
||||
from letta.schemas.tool_rule import ToolRule
|
||||
from letta.schemas.user import User
|
||||
from letta.system import get_initial_boot_messages, get_login_event
|
||||
@ -234,17 +234,24 @@ def package_initial_message_sequence(
|
||||
|
||||
if message_create.role == MessageRole.user:
|
||||
packed_message = system.package_user_message(
|
||||
user_message=message_create.text,
|
||||
user_message=message_create.content,
|
||||
)
|
||||
elif message_create.role == MessageRole.system:
|
||||
packed_message = system.package_system_message(
|
||||
system_message=message_create.text,
|
||||
system_message=message_create.content,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid message role: {message_create.role}")
|
||||
|
||||
init_messages.append(
|
||||
Message(role=message_create.role, text=packed_message, organization_id=actor.organization_id, agent_id=agent_id, model=model)
|
||||
Message(
|
||||
role=message_create.role,
|
||||
content=[TextContent(text=packed_message)],
|
||||
name=message_create.name,
|
||||
organization_id=actor.organization_id,
|
||||
agent_id=agent_id,
|
||||
model=model,
|
||||
)
|
||||
)
|
||||
return init_messages
|
||||
|
||||
|
@ -78,10 +78,12 @@ class JobManager:
|
||||
def list_jobs(
|
||||
self,
|
||||
actor: PydanticUser,
|
||||
cursor: Optional[str] = None,
|
||||
before: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
limit: Optional[int] = 50,
|
||||
statuses: Optional[List[JobStatus]] = None,
|
||||
job_type: JobType = JobType.JOB,
|
||||
ascending: bool = True,
|
||||
) -> List[PydanticJob]:
|
||||
"""List all jobs with optional pagination and status filter."""
|
||||
with self.session_maker() as session:
|
||||
@ -93,8 +95,10 @@ class JobManager:
|
||||
|
||||
jobs = JobModel.list(
|
||||
db_session=session,
|
||||
cursor=cursor,
|
||||
before=before,
|
||||
after=after,
|
||||
limit=limit,
|
||||
ascending=ascending,
|
||||
**filter_kwargs,
|
||||
)
|
||||
return [job.to_pydantic() for job in jobs]
|
||||
@ -112,7 +116,8 @@ class JobManager:
|
||||
self,
|
||||
job_id: str,
|
||||
actor: PydanticUser,
|
||||
cursor: Optional[str] = None,
|
||||
before: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
limit: Optional[int] = 100,
|
||||
role: Optional[MessageRole] = None,
|
||||
ascending: bool = True,
|
||||
@ -123,7 +128,8 @@ class JobManager:
|
||||
Args:
|
||||
job_id: The ID of the job to get messages for
|
||||
actor: The user making the request
|
||||
cursor: Cursor for pagination
|
||||
before: Cursor for pagination
|
||||
after: Cursor for pagination
|
||||
limit: Maximum number of messages to return
|
||||
role: Optional filter for message role
|
||||
ascending: Optional flag to sort in ascending order
|
||||
@ -143,7 +149,8 @@ class JobManager:
|
||||
# Get messages
|
||||
messages = MessageModel.list(
|
||||
db_session=session,
|
||||
cursor=cursor,
|
||||
before=before,
|
||||
after=after,
|
||||
ascending=ascending,
|
||||
limit=limit,
|
||||
actor=actor,
|
||||
@ -255,11 +262,12 @@ class JobManager:
|
||||
session.commit()
|
||||
|
||||
@enforce_types
|
||||
def get_run_messages_cursor(
|
||||
def get_run_messages(
|
||||
self,
|
||||
run_id: str,
|
||||
actor: PydanticUser,
|
||||
cursor: Optional[str] = None,
|
||||
before: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
limit: Optional[int] = 100,
|
||||
role: Optional[MessageRole] = None,
|
||||
ascending: bool = True,
|
||||
@ -271,7 +279,8 @@ class JobManager:
|
||||
Args:
|
||||
job_id: The ID of the job to get messages for
|
||||
actor: The user making the request
|
||||
cursor: Message ID to get messages after or before
|
||||
before: Message ID to get messages after
|
||||
after: Message ID to get messages before
|
||||
limit: Maximum number of messages to return
|
||||
ascending: Whether to return messages in ascending order
|
||||
role: Optional role filter
|
||||
@ -285,7 +294,8 @@ class JobManager:
|
||||
messages = self.get_job_messages(
|
||||
job_id=run_id,
|
||||
actor=actor,
|
||||
cursor=cursor,
|
||||
before=before,
|
||||
after=after,
|
||||
limit=limit,
|
||||
role=role,
|
||||
ascending=ascending,
|
||||
|
@ -49,7 +49,7 @@ class MessageManager:
|
||||
with self.session_maker() as session:
|
||||
# Set the organization id of the Pydantic message
|
||||
pydantic_msg.organization_id = actor.organization_id
|
||||
msg_data = pydantic_msg.model_dump()
|
||||
msg_data = pydantic_msg.model_dump(to_orm=True)
|
||||
msg = MessageModel(**msg_data)
|
||||
msg.create(session, actor=actor) # Persist to database
|
||||
return msg.to_pydantic()
|
||||
@ -83,7 +83,7 @@ class MessageManager:
|
||||
)
|
||||
|
||||
# get update dictionary
|
||||
update_data = message_update.model_dump(exclude_unset=True, exclude_none=True)
|
||||
update_data = message_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
|
||||
# Remove redundant update fields
|
||||
update_data = {key: value for key, value in update_data.items() if getattr(message, key) != value}
|
||||
|
||||
@ -128,7 +128,8 @@ class MessageManager:
|
||||
self,
|
||||
agent_id: str,
|
||||
actor: Optional[PydanticUser] = None,
|
||||
cursor: Optional[str] = None,
|
||||
before: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
limit: Optional[int] = 50,
|
||||
@ -139,7 +140,8 @@ class MessageManager:
|
||||
"""List user messages with flexible filtering and pagination options.
|
||||
|
||||
Args:
|
||||
cursor: Cursor-based pagination - return records after this ID (exclusive)
|
||||
before: Cursor-based pagination - return records before this ID (exclusive)
|
||||
after: Cursor-based pagination - return records after this ID (exclusive)
|
||||
start_date: Filter records created after this date
|
||||
end_date: Filter records created before this date
|
||||
limit: Maximum number of records to return
|
||||
@ -156,7 +158,8 @@ class MessageManager:
|
||||
return self.list_messages_for_agent(
|
||||
agent_id=agent_id,
|
||||
actor=actor,
|
||||
cursor=cursor,
|
||||
before=before,
|
||||
after=after,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
limit=limit,
|
||||
@ -170,7 +173,8 @@ class MessageManager:
|
||||
self,
|
||||
agent_id: str,
|
||||
actor: Optional[PydanticUser] = None,
|
||||
cursor: Optional[str] = None,
|
||||
before: Optional[str] = None,
|
||||
after: Optional[str] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
limit: Optional[int] = 50,
|
||||
@ -181,7 +185,8 @@ class MessageManager:
|
||||
"""List messages with flexible filtering and pagination options.
|
||||
|
||||
Args:
|
||||
cursor: Cursor-based pagination - return records after this ID (exclusive)
|
||||
before: Cursor-based pagination - return records before this ID (exclusive)
|
||||
after: Cursor-based pagination - return records after this ID (exclusive)
|
||||
start_date: Filter records created after this date
|
||||
end_date: Filter records created before this date
|
||||
limit: Maximum number of records to return
|
||||
@ -201,7 +206,8 @@ class MessageManager:
|
||||
|
||||
results = MessageModel.list(
|
||||
db_session=session,
|
||||
cursor=cursor,
|
||||
before=before,
|
||||
after=after,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
limit=limit,
|
||||
|
@ -71,8 +71,12 @@ class OrganizationManager:
|
||||
organization.hard_delete(session)
|
||||
|
||||
@enforce_types
|
||||
def list_organizations(self, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticOrganization]:
|
||||
"""List organizations with pagination based on cursor (org_id) and limit."""
|
||||
def list_organizations(self, after: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticOrganization]:
|
||||
"""List all organizations with optional pagination."""
|
||||
with self.session_maker() as session:
|
||||
results = OrganizationModel.list(db_session=session, cursor=cursor, limit=limit)
|
||||
return [org.to_pydantic() for org in results]
|
||||
organizations = OrganizationModel.list(
|
||||
db_session=session,
|
||||
after=after,
|
||||
limit=limit,
|
||||
)
|
||||
return [org.to_pydantic() for org in organizations]
|
||||
|
@ -59,11 +59,15 @@ class ProviderManager:
|
||||
session.commit()
|
||||
|
||||
@enforce_types
|
||||
def list_providers(self, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticProvider]:
|
||||
"""List providers with pagination using cursor (id) and limit."""
|
||||
def list_providers(self, after: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticProvider]:
|
||||
"""List all providers with optional pagination."""
|
||||
with self.session_maker() as session:
|
||||
results = ProviderModel.list(db_session=session, cursor=cursor, limit=limit)
|
||||
return [provider.to_pydantic() for provider in results]
|
||||
providers = ProviderModel.list(
|
||||
db_session=session,
|
||||
after=after,
|
||||
limit=limit,
|
||||
)
|
||||
return [provider.to_pydantic() for provider in providers]
|
||||
|
||||
@enforce_types
|
||||
def get_anthropic_override_provider_id(self) -> Optional[str]:
|
||||
|
@ -111,7 +111,11 @@ class SandboxConfigManager:
|
||||
|
||||
@enforce_types
|
||||
def list_sandbox_configs(
|
||||
self, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50, sandbox_type: Optional[SandboxType] = None
|
||||
self,
|
||||
actor: PydanticUser,
|
||||
after: Optional[str] = None,
|
||||
limit: Optional[int] = 50,
|
||||
sandbox_type: Optional[SandboxType] = None,
|
||||
) -> List[PydanticSandboxConfig]:
|
||||
"""List all sandbox configurations with optional pagination."""
|
||||
kwargs = {"organization_id": actor.organization_id}
|
||||
@ -119,7 +123,7 @@ class SandboxConfigManager:
|
||||
kwargs.update({"type": sandbox_type})
|
||||
|
||||
with self.session_maker() as session:
|
||||
sandboxes = SandboxConfigModel.list(db_session=session, cursor=cursor, limit=limit, **kwargs)
|
||||
sandboxes = SandboxConfigModel.list(db_session=session, after=after, limit=limit, **kwargs)
|
||||
return [sandbox.to_pydantic() for sandbox in sandboxes]
|
||||
|
||||
@enforce_types
|
||||
@ -207,13 +211,17 @@ class SandboxConfigManager:
|
||||
|
||||
@enforce_types
|
||||
def list_sandbox_env_vars(
|
||||
self, sandbox_config_id: str, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50
|
||||
self,
|
||||
sandbox_config_id: str,
|
||||
actor: PydanticUser,
|
||||
after: Optional[str] = None,
|
||||
limit: Optional[int] = 50,
|
||||
) -> List[PydanticEnvVar]:
|
||||
"""List all sandbox environment variables with optional pagination."""
|
||||
with self.session_maker() as session:
|
||||
env_vars = SandboxEnvVarModel.list(
|
||||
db_session=session,
|
||||
cursor=cursor,
|
||||
after=after,
|
||||
limit=limit,
|
||||
organization_id=actor.organization_id,
|
||||
sandbox_config_id=sandbox_config_id,
|
||||
@ -222,13 +230,13 @@ class SandboxConfigManager:
|
||||
|
||||
@enforce_types
|
||||
def list_sandbox_env_vars_by_key(
|
||||
self, key: str, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50
|
||||
self, key: str, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50
|
||||
) -> List[PydanticEnvVar]:
|
||||
"""List all sandbox environment variables with optional pagination."""
|
||||
with self.session_maker() as session:
|
||||
env_vars = SandboxEnvVarModel.list(
|
||||
db_session=session,
|
||||
cursor=cursor,
|
||||
after=after,
|
||||
limit=limit,
|
||||
organization_id=actor.organization_id,
|
||||
key=key,
|
||||
@ -237,9 +245,9 @@ class SandboxConfigManager:
|
||||
|
||||
@enforce_types
|
||||
def get_sandbox_env_vars_as_dict(
|
||||
self, sandbox_config_id: str, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50
|
||||
self, sandbox_config_id: str, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50
|
||||
) -> Dict[str, str]:
|
||||
env_vars = self.list_sandbox_env_vars(sandbox_config_id, actor, cursor, limit)
|
||||
env_vars = self.list_sandbox_env_vars(sandbox_config_id, actor, after, limit)
|
||||
result = {}
|
||||
for env_var in env_vars:
|
||||
result[env_var.key] = env_var.value
|
||||
|
@ -65,12 +65,12 @@ class SourceManager:
|
||||
return source.to_pydantic()
|
||||
|
||||
@enforce_types
|
||||
def list_sources(self, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50, **kwargs) -> List[PydanticSource]:
|
||||
def list_sources(self, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50, **kwargs) -> List[PydanticSource]:
|
||||
"""List all sources with optional pagination."""
|
||||
with self.session_maker() as session:
|
||||
sources = SourceModel.list(
|
||||
db_session=session,
|
||||
cursor=cursor,
|
||||
after=after,
|
||||
limit=limit,
|
||||
organization_id=actor.organization_id,
|
||||
**kwargs,
|
||||
@ -149,12 +149,12 @@ class SourceManager:
|
||||
|
||||
@enforce_types
|
||||
def list_files(
|
||||
self, source_id: str, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50
|
||||
self, source_id: str, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50
|
||||
) -> List[PydanticFileMetadata]:
|
||||
"""List all files with optional pagination."""
|
||||
with self.session_maker() as session:
|
||||
files = FileMetadataModel.list(
|
||||
db_session=session, cursor=cursor, limit=limit, organization_id=actor.organization_id, source_id=source_id
|
||||
db_session=session, after=after, limit=limit, organization_id=actor.organization_id, source_id=source_id
|
||||
)
|
||||
return [file.to_pydantic() for file in files]
|
||||
|
||||
|
@ -93,12 +93,12 @@ class ToolManager:
|
||||
return None
|
||||
|
||||
@enforce_types
|
||||
def list_tools(self, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticTool]:
|
||||
"""List all tools with optional pagination using cursor and limit."""
|
||||
def list_tools(self, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticTool]:
|
||||
"""List all tools with optional pagination."""
|
||||
with self.session_maker() as session:
|
||||
tools = ToolModel.list(
|
||||
db_session=session,
|
||||
cursor=cursor,
|
||||
after=after,
|
||||
limit=limit,
|
||||
organization_id=actor.organization_id,
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import List, Optional
|
||||
|
||||
from letta.orm.errors import NoResultFound
|
||||
from letta.orm.organization import Organization as OrganizationModel
|
||||
@ -99,8 +99,12 @@ class UserManager:
|
||||
return self.get_default_user()
|
||||
|
||||
@enforce_types
|
||||
def list_users(self, cursor: Optional[str] = None, limit: Optional[int] = 50) -> Tuple[Optional[str], List[PydanticUser]]:
|
||||
"""List users with pagination using cursor (id) and limit."""
|
||||
def list_users(self, after: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticUser]:
|
||||
"""List all users with optional pagination."""
|
||||
with self.session_maker() as session:
|
||||
results = UserModel.list(db_session=session, cursor=cursor, limit=limit)
|
||||
return [user.to_pydantic() for user in results]
|
||||
users = UserModel.list(
|
||||
db_session=session,
|
||||
after=after,
|
||||
limit=limit,
|
||||
)
|
||||
return [user.to_pydantic() for user in users]
|
||||
|
@ -55,7 +55,7 @@ class LettaUser(HttpUser):
|
||||
|
||||
@task(1)
|
||||
def send_message(self):
|
||||
messages = [MessageCreate(role=MessageRole("user"), text="hello")]
|
||||
messages = [MessageCreate(role=MessageRole("user"), content="hello")]
|
||||
request = LettaRequest(messages=messages)
|
||||
|
||||
with self.client.post(
|
||||
@ -70,7 +70,7 @@ class LettaUser(HttpUser):
|
||||
# @task(1)
|
||||
# def send_message_stream(self):
|
||||
|
||||
# messages = [MessageCreate(role=MessageRole("user"), text="hello")]
|
||||
# messages = [MessageCreate(role=MessageRole("user"), content="hello")]
|
||||
# request = LettaRequest(messages=messages, stream_steps=True, stream_tokens=True, return_message_object=True)
|
||||
# if stream_tokens or stream_steps:
|
||||
# from letta.client.streaming import _sse_post
|
||||
|
264
poetry.lock
generated
264
poetry.lock
generated
@ -1671,137 +1671,137 @@ test = ["objgraph", "psutil"]
|
||||
|
||||
[[package]]
|
||||
name = "grpcio"
|
||||
version = "1.69.0"
|
||||
version = "1.70.0"
|
||||
description = "HTTP/2-based RPC framework"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "grpcio-1.69.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:2060ca95a8db295ae828d0fc1c7f38fb26ccd5edf9aa51a0f44251f5da332e97"},
|
||||
{file = "grpcio-1.69.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:2e52e107261fd8fa8fa457fe44bfadb904ae869d87c1280bf60f93ecd3e79278"},
|
||||
{file = "grpcio-1.69.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:316463c0832d5fcdb5e35ff2826d9aa3f26758d29cdfb59a368c1d6c39615a11"},
|
||||
{file = "grpcio-1.69.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26c9a9c4ac917efab4704b18eed9082ed3b6ad19595f047e8173b5182fec0d5e"},
|
||||
{file = "grpcio-1.69.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90b3646ced2eae3a0599658eeccc5ba7f303bf51b82514c50715bdd2b109e5ec"},
|
||||
{file = "grpcio-1.69.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3b75aea7c6cb91b341c85e7c1d9db1e09e1dd630b0717f836be94971e015031e"},
|
||||
{file = "grpcio-1.69.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5cfd14175f9db33d4b74d63de87c64bb0ee29ce475ce3c00c01ad2a3dc2a9e51"},
|
||||
{file = "grpcio-1.69.0-cp310-cp310-win32.whl", hash = "sha256:9031069d36cb949205293cf0e243abd5e64d6c93e01b078c37921493a41b72dc"},
|
||||
{file = "grpcio-1.69.0-cp310-cp310-win_amd64.whl", hash = "sha256:cc89b6c29f3dccbe12d7a3b3f1b3999db4882ae076c1c1f6df231d55dbd767a5"},
|
||||
{file = "grpcio-1.69.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:8de1b192c29b8ce45ee26a700044717bcbbd21c697fa1124d440548964328561"},
|
||||
{file = "grpcio-1.69.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:7e76accf38808f5c5c752b0ab3fd919eb14ff8fafb8db520ad1cc12afff74de6"},
|
||||
{file = "grpcio-1.69.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:d5658c3c2660417d82db51e168b277e0ff036d0b0f859fa7576c0ffd2aec1442"},
|
||||
{file = "grpcio-1.69.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5494d0e52bf77a2f7eb17c6da662886ca0a731e56c1c85b93505bece8dc6cf4c"},
|
||||
{file = "grpcio-1.69.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ed866f9edb574fd9be71bf64c954ce1b88fc93b2a4cbf94af221e9426eb14d6"},
|
||||
{file = "grpcio-1.69.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c5ba38aeac7a2fe353615c6b4213d1fbb3a3c34f86b4aaa8be08baaaee8cc56d"},
|
||||
{file = "grpcio-1.69.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f79e05f5bbf551c4057c227d1b041ace0e78462ac8128e2ad39ec58a382536d2"},
|
||||
{file = "grpcio-1.69.0-cp311-cp311-win32.whl", hash = "sha256:bf1f8be0da3fcdb2c1e9f374f3c2d043d606d69f425cd685110dd6d0d2d61258"},
|
||||
{file = "grpcio-1.69.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb9302afc3a0e4ba0b225cd651ef8e478bf0070cf11a529175caecd5ea2474e7"},
|
||||
{file = "grpcio-1.69.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:fc18a4de8c33491ad6f70022af5c460b39611e39578a4d84de0fe92f12d5d47b"},
|
||||
{file = "grpcio-1.69.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:0f0270bd9ffbff6961fe1da487bdcd594407ad390cc7960e738725d4807b18c4"},
|
||||
{file = "grpcio-1.69.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:dc48f99cc05e0698e689b51a05933253c69a8c8559a47f605cff83801b03af0e"},
|
||||
{file = "grpcio-1.69.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e925954b18d41aeb5ae250262116d0970893b38232689c4240024e4333ac084"},
|
||||
{file = "grpcio-1.69.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87d222569273720366f68a99cb62e6194681eb763ee1d3b1005840678d4884f9"},
|
||||
{file = "grpcio-1.69.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b62b0f41e6e01a3e5082000b612064c87c93a49b05f7602fe1b7aa9fd5171a1d"},
|
||||
{file = "grpcio-1.69.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:db6f9fd2578dbe37db4b2994c94a1d9c93552ed77dca80e1657bb8a05b898b55"},
|
||||
{file = "grpcio-1.69.0-cp312-cp312-win32.whl", hash = "sha256:b192b81076073ed46f4b4dd612b8897d9a1e39d4eabd822e5da7b38497ed77e1"},
|
||||
{file = "grpcio-1.69.0-cp312-cp312-win_amd64.whl", hash = "sha256:1227ff7836f7b3a4ab04e5754f1d001fa52a730685d3dc894ed8bc262cc96c01"},
|
||||
{file = "grpcio-1.69.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:a78a06911d4081a24a1761d16215a08e9b6d4d29cdbb7e427e6c7e17b06bcc5d"},
|
||||
{file = "grpcio-1.69.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:dc5a351927d605b2721cbb46158e431dd49ce66ffbacb03e709dc07a491dde35"},
|
||||
{file = "grpcio-1.69.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:3629d8a8185f5139869a6a17865d03113a260e311e78fbe313f1a71603617589"},
|
||||
{file = "grpcio-1.69.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9a281878feeb9ae26db0622a19add03922a028d4db684658f16d546601a4870"},
|
||||
{file = "grpcio-1.69.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc614e895177ab7e4b70f154d1a7c97e152577ea101d76026d132b7aaba003b"},
|
||||
{file = "grpcio-1.69.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1ee76cd7e2e49cf9264f6812d8c9ac1b85dda0eaea063af07292400f9191750e"},
|
||||
{file = "grpcio-1.69.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0470fa911c503af59ec8bc4c82b371ee4303ececbbdc055f55ce48e38b20fd67"},
|
||||
{file = "grpcio-1.69.0-cp313-cp313-win32.whl", hash = "sha256:b650f34aceac8b2d08a4c8d7dc3e8a593f4d9e26d86751ebf74ebf5107d927de"},
|
||||
{file = "grpcio-1.69.0-cp313-cp313-win_amd64.whl", hash = "sha256:028337786f11fecb5d7b7fa660475a06aabf7e5e52b5ac2df47414878c0ce7ea"},
|
||||
{file = "grpcio-1.69.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:b7f693db593d6bf285e015d5538bf1c86cf9c60ed30b6f7da04a00ed052fe2f3"},
|
||||
{file = "grpcio-1.69.0-cp38-cp38-macosx_10_14_universal2.whl", hash = "sha256:8b94e83f66dbf6fd642415faca0608590bc5e8d30e2c012b31d7d1b91b1de2fd"},
|
||||
{file = "grpcio-1.69.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:b634851b92c090763dde61df0868c730376cdb73a91bcc821af56ae043b09596"},
|
||||
{file = "grpcio-1.69.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf5f680d3ed08c15330d7830d06bc65f58ca40c9999309517fd62880d70cb06e"},
|
||||
{file = "grpcio-1.69.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:200e48a6e7b00f804cf00a1c26292a5baa96507c7749e70a3ec10ca1a288936e"},
|
||||
{file = "grpcio-1.69.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:45a4704339b6e5b24b0e136dea9ad3815a94f30eb4f1e1d44c4ac484ef11d8dd"},
|
||||
{file = "grpcio-1.69.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85d347cb8237751b23539981dbd2d9d8f6e9ff90082b427b13022b948eb6347a"},
|
||||
{file = "grpcio-1.69.0-cp38-cp38-win32.whl", hash = "sha256:60e5de105dc02832dc8f120056306d0ef80932bcf1c0e2b4ca3b676de6dc6505"},
|
||||
{file = "grpcio-1.69.0-cp38-cp38-win_amd64.whl", hash = "sha256:282f47d0928e40f25d007f24eb8fa051cb22551e3c74b8248bc9f9bea9c35fe0"},
|
||||
{file = "grpcio-1.69.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:dd034d68a2905464c49479b0c209c773737a4245d616234c79c975c7c90eca03"},
|
||||
{file = "grpcio-1.69.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:01f834732c22a130bdf3dc154d1053bdbc887eb3ccb7f3e6285cfbfc33d9d5cc"},
|
||||
{file = "grpcio-1.69.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:a7f4ed0dcf202a70fe661329f8874bc3775c14bb3911d020d07c82c766ce0eb1"},
|
||||
{file = "grpcio-1.69.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd7ea241b10bc5f0bb0f82c0d7896822b7ed122b3ab35c9851b440c1ccf81588"},
|
||||
{file = "grpcio-1.69.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f03dc9b4da4c0dc8a1db7a5420f575251d7319b7a839004d8916257ddbe4816"},
|
||||
{file = "grpcio-1.69.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ca71d73a270dff052fe4edf74fef142d6ddd1f84175d9ac4a14b7280572ac519"},
|
||||
{file = "grpcio-1.69.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ccbed100dc43704e94ccff9e07680b540d64e4cc89213ab2832b51b4f68a520"},
|
||||
{file = "grpcio-1.69.0-cp39-cp39-win32.whl", hash = "sha256:1514341def9c6ec4b7f0b9628be95f620f9d4b99331b7ef0a1845fd33d9b579c"},
|
||||
{file = "grpcio-1.69.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1fea55d26d647346acb0069b08dca70984101f2dc95066e003019207212e303"},
|
||||
{file = "grpcio-1.69.0.tar.gz", hash = "sha256:936fa44241b5379c5afc344e1260d467bee495747eaf478de825bab2791da6f5"},
|
||||
{file = "grpcio-1.70.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:95469d1977429f45fe7df441f586521361e235982a0b39e33841549143ae2851"},
|
||||
{file = "grpcio-1.70.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:ed9718f17fbdb472e33b869c77a16d0b55e166b100ec57b016dc7de9c8d236bf"},
|
||||
{file = "grpcio-1.70.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:374d014f29f9dfdb40510b041792e0e2828a1389281eb590df066e1cc2b404e5"},
|
||||
{file = "grpcio-1.70.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2af68a6f5c8f78d56c145161544ad0febbd7479524a59c16b3e25053f39c87f"},
|
||||
{file = "grpcio-1.70.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce7df14b2dcd1102a2ec32f621cc9fab6695effef516efbc6b063ad749867295"},
|
||||
{file = "grpcio-1.70.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c78b339869f4dbf89881e0b6fbf376313e4f845a42840a7bdf42ee6caed4b11f"},
|
||||
{file = "grpcio-1.70.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58ad9ba575b39edef71f4798fdb5c7b6d02ad36d47949cd381d4392a5c9cbcd3"},
|
||||
{file = "grpcio-1.70.0-cp310-cp310-win32.whl", hash = "sha256:2b0d02e4b25a5c1f9b6c7745d4fa06efc9fd6a611af0fb38d3ba956786b95199"},
|
||||
{file = "grpcio-1.70.0-cp310-cp310-win_amd64.whl", hash = "sha256:0de706c0a5bb9d841e353f6343a9defc9fc35ec61d6eb6111802f3aa9fef29e1"},
|
||||
{file = "grpcio-1.70.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:17325b0be0c068f35770f944124e8839ea3185d6d54862800fc28cc2ffad205a"},
|
||||
{file = "grpcio-1.70.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:dbe41ad140df911e796d4463168e33ef80a24f5d21ef4d1e310553fcd2c4a386"},
|
||||
{file = "grpcio-1.70.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:5ea67c72101d687d44d9c56068328da39c9ccba634cabb336075fae2eab0d04b"},
|
||||
{file = "grpcio-1.70.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb5277db254ab7586769e490b7b22f4ddab3876c490da0a1a9d7c695ccf0bf77"},
|
||||
{file = "grpcio-1.70.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7831a0fc1beeeb7759f737f5acd9fdcda520e955049512d68fda03d91186eea"},
|
||||
{file = "grpcio-1.70.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:27cc75e22c5dba1fbaf5a66c778e36ca9b8ce850bf58a9db887754593080d839"},
|
||||
{file = "grpcio-1.70.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d63764963412e22f0491d0d32833d71087288f4e24cbcddbae82476bfa1d81fd"},
|
||||
{file = "grpcio-1.70.0-cp311-cp311-win32.whl", hash = "sha256:bb491125103c800ec209d84c9b51f1c60ea456038e4734688004f377cfacc113"},
|
||||
{file = "grpcio-1.70.0-cp311-cp311-win_amd64.whl", hash = "sha256:d24035d49e026353eb042bf7b058fb831db3e06d52bee75c5f2f3ab453e71aca"},
|
||||
{file = "grpcio-1.70.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:ef4c14508299b1406c32bdbb9fb7b47612ab979b04cf2b27686ea31882387cff"},
|
||||
{file = "grpcio-1.70.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:aa47688a65643afd8b166928a1da6247d3f46a2784d301e48ca1cc394d2ffb40"},
|
||||
{file = "grpcio-1.70.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:880bfb43b1bb8905701b926274eafce5c70a105bc6b99e25f62e98ad59cb278e"},
|
||||
{file = "grpcio-1.70.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e654c4b17d07eab259d392e12b149c3a134ec52b11ecdc6a515b39aceeec898"},
|
||||
{file = "grpcio-1.70.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2394e3381071045a706ee2eeb6e08962dd87e8999b90ac15c55f56fa5a8c9597"},
|
||||
{file = "grpcio-1.70.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b3c76701428d2df01964bc6479422f20e62fcbc0a37d82ebd58050b86926ef8c"},
|
||||
{file = "grpcio-1.70.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ac073fe1c4cd856ebcf49e9ed6240f4f84d7a4e6ee95baa5d66ea05d3dd0df7f"},
|
||||
{file = "grpcio-1.70.0-cp312-cp312-win32.whl", hash = "sha256:cd24d2d9d380fbbee7a5ac86afe9787813f285e684b0271599f95a51bce33528"},
|
||||
{file = "grpcio-1.70.0-cp312-cp312-win_amd64.whl", hash = "sha256:0495c86a55a04a874c7627fd33e5beaee771917d92c0e6d9d797628ac40e7655"},
|
||||
{file = "grpcio-1.70.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:aa573896aeb7d7ce10b1fa425ba263e8dddd83d71530d1322fd3a16f31257b4a"},
|
||||
{file = "grpcio-1.70.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:d405b005018fd516c9ac529f4b4122342f60ec1cee181788249372524e6db429"},
|
||||
{file = "grpcio-1.70.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f32090238b720eb585248654db8e3afc87b48d26ac423c8dde8334a232ff53c9"},
|
||||
{file = "grpcio-1.70.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfa089a734f24ee5f6880c83d043e4f46bf812fcea5181dcb3a572db1e79e01c"},
|
||||
{file = "grpcio-1.70.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f19375f0300b96c0117aca118d400e76fede6db6e91f3c34b7b035822e06c35f"},
|
||||
{file = "grpcio-1.70.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:7c73c42102e4a5ec76608d9b60227d917cea46dff4d11d372f64cbeb56d259d0"},
|
||||
{file = "grpcio-1.70.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:0a5c78d5198a1f0aa60006cd6eb1c912b4a1520b6a3968e677dbcba215fabb40"},
|
||||
{file = "grpcio-1.70.0-cp313-cp313-win32.whl", hash = "sha256:fe9dbd916df3b60e865258a8c72ac98f3ac9e2a9542dcb72b7a34d236242a5ce"},
|
||||
{file = "grpcio-1.70.0-cp313-cp313-win_amd64.whl", hash = "sha256:4119fed8abb7ff6c32e3d2255301e59c316c22d31ab812b3fbcbaf3d0d87cc68"},
|
||||
{file = "grpcio-1.70.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:8058667a755f97407fca257c844018b80004ae8035565ebc2812cc550110718d"},
|
||||
{file = "grpcio-1.70.0-cp38-cp38-macosx_10_14_universal2.whl", hash = "sha256:879a61bf52ff8ccacbedf534665bb5478ec8e86ad483e76fe4f729aaef867cab"},
|
||||
{file = "grpcio-1.70.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:0ba0a173f4feacf90ee618fbc1a27956bfd21260cd31ced9bc707ef551ff7dc7"},
|
||||
{file = "grpcio-1.70.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558c386ecb0148f4f99b1a65160f9d4b790ed3163e8610d11db47838d452512d"},
|
||||
{file = "grpcio-1.70.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:412faabcc787bbc826f51be261ae5fa996b21263de5368a55dc2cf824dc5090e"},
|
||||
{file = "grpcio-1.70.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3b0f01f6ed9994d7a0b27eeddea43ceac1b7e6f3f9d86aeec0f0064b8cf50fdb"},
|
||||
{file = "grpcio-1.70.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7385b1cb064734005204bc8994eed7dcb801ed6c2eda283f613ad8c6c75cf873"},
|
||||
{file = "grpcio-1.70.0-cp38-cp38-win32.whl", hash = "sha256:07269ff4940f6fb6710951116a04cd70284da86d0a4368fd5a3b552744511f5a"},
|
||||
{file = "grpcio-1.70.0-cp38-cp38-win_amd64.whl", hash = "sha256:aba19419aef9b254e15011b230a180e26e0f6864c90406fdbc255f01d83bc83c"},
|
||||
{file = "grpcio-1.70.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:4f1937f47c77392ccd555728f564a49128b6a197a05a5cd527b796d36f3387d0"},
|
||||
{file = "grpcio-1.70.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:0cd430b9215a15c10b0e7d78f51e8a39d6cf2ea819fd635a7214fae600b1da27"},
|
||||
{file = "grpcio-1.70.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:e27585831aa6b57b9250abaf147003e126cd3a6c6ca0c531a01996f31709bed1"},
|
||||
{file = "grpcio-1.70.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1af8e15b0f0fe0eac75195992a63df17579553b0c4af9f8362cc7cc99ccddf4"},
|
||||
{file = "grpcio-1.70.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbce24409beaee911c574a3d75d12ffb8c3e3dd1b813321b1d7a96bbcac46bf4"},
|
||||
{file = "grpcio-1.70.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ff4a8112a79464919bb21c18e956c54add43ec9a4850e3949da54f61c241a4a6"},
|
||||
{file = "grpcio-1.70.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5413549fdf0b14046c545e19cfc4eb1e37e9e1ebba0ca390a8d4e9963cab44d2"},
|
||||
{file = "grpcio-1.70.0-cp39-cp39-win32.whl", hash = "sha256:b745d2c41b27650095e81dea7091668c040457483c9bdb5d0d9de8f8eb25e59f"},
|
||||
{file = "grpcio-1.70.0-cp39-cp39-win_amd64.whl", hash = "sha256:a31d7e3b529c94e930a117b2175b2efd179d96eb3c7a21ccb0289a8ab05b645c"},
|
||||
{file = "grpcio-1.70.0.tar.gz", hash = "sha256:8d1584a68d5922330025881e63a6c1b54cc8117291d382e4fa69339b6d914c56"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
protobuf = ["grpcio-tools (>=1.69.0)"]
|
||||
protobuf = ["grpcio-tools (>=1.70.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "grpcio-tools"
|
||||
version = "1.69.0"
|
||||
version = "1.70.0"
|
||||
description = "Protobuf code generator for gRPC"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "grpcio_tools-1.69.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:8c210630faa581c3bd08953dac4ad21a7f49862f3b92d69686e9b436d2f1265d"},
|
||||
{file = "grpcio_tools-1.69.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:09b66ea279fcdaebae4ec34b1baf7577af3b14322738aa980c1c33cfea71f7d7"},
|
||||
{file = "grpcio_tools-1.69.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:be94a4bfa56d356aae242cc54072c9ccc2704b659eaae2fd599a94afebf791ce"},
|
||||
{file = "grpcio_tools-1.69.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28778debad73a8c8e0a0e07e6a2f76eecce43adbc205d17dd244d2d58bb0f0aa"},
|
||||
{file = "grpcio_tools-1.69.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:449308d93e4c97ae3a4503510c6d64978748ff5e21429c85da14fdc783c0f498"},
|
||||
{file = "grpcio_tools-1.69.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b9343651e73bc6e0df6bb518c2638bf9cc2194b50d060cdbcf1b2121cd4e4ae3"},
|
||||
{file = "grpcio_tools-1.69.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2f08b063612553e726e328aef3a27adfaea8d92712b229012afc54d59da88a02"},
|
||||
{file = "grpcio_tools-1.69.0-cp310-cp310-win32.whl", hash = "sha256:599ffd39525e7bbb6412a63e56a2e6c1af8f3493fe4305260efd4a11d064cce0"},
|
||||
{file = "grpcio_tools-1.69.0-cp310-cp310-win_amd64.whl", hash = "sha256:02f92e3c2bae67ece818787f8d3d89df0fa1e5e6bbb7c1493824fd5dfad886dd"},
|
||||
{file = "grpcio_tools-1.69.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:c18df5d1c8e163a29863583ec51237d08d7059ef8d4f7661ee6d6363d3e38fe3"},
|
||||
{file = "grpcio_tools-1.69.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:37876ae49235ef2e61e5059faf45dc5e7142ca54ae61aec378bb9483e0cd7e95"},
|
||||
{file = "grpcio_tools-1.69.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:33120920e29959eaa37a1268c6a22af243d086b1a5e5222b4203e29560ece9ce"},
|
||||
{file = "grpcio_tools-1.69.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:788bb3ecd1b44664d829d319b3c1ebc15c7d7b5e7d1f22706ab57d6acd2c6301"},
|
||||
{file = "grpcio_tools-1.69.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f453b11a112e3774c8957ec2570669f3da1f7fbc8ee242482c38981496e88da2"},
|
||||
{file = "grpcio_tools-1.69.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7e5c5dc2b656755cb58b11a7e87b65258a4a8eaff01b6c30ffcb230dd447c03d"},
|
||||
{file = "grpcio_tools-1.69.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8eabf0a7a98c14322bc74f9910c96f98feebe311e085624b2d022924d4f652ca"},
|
||||
{file = "grpcio_tools-1.69.0-cp311-cp311-win32.whl", hash = "sha256:ad567bea43d018c2215e1db10316eda94ca19229a834a3221c15d132d24c1b8a"},
|
||||
{file = "grpcio_tools-1.69.0-cp311-cp311-win_amd64.whl", hash = "sha256:3d64e801586dbea3530f245d48b9ed031738cc3eb099d5ce2fdb1b3dc2e1fb20"},
|
||||
{file = "grpcio_tools-1.69.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:8ef8efe8beac4cc1e30d41893e4096ca2601da61001897bd17441645de2d4d3c"},
|
||||
{file = "grpcio_tools-1.69.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:a00e87a0c5a294028115a098819899b08dd18449df5b2aac4a2b87ba865e8681"},
|
||||
{file = "grpcio_tools-1.69.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:7722700346d5b223159532e046e51f2ff743ed4342e5fe3e0457120a4199015e"},
|
||||
{file = "grpcio_tools-1.69.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a934116fdf202cb675246056ee54645c743e2240632f86a37e52f91a405c7143"},
|
||||
{file = "grpcio_tools-1.69.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e6a6d44359ca836acfbc58103daf94b3bb8ac919d659bb348dcd7fbecedc293"},
|
||||
{file = "grpcio_tools-1.69.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e27662c0597fd1ab5399a583d358b5203edcb6fc2b29d6245099dfacd51a6ddc"},
|
||||
{file = "grpcio_tools-1.69.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7bbb2b2fb81d95bcdd1d8331defb5f5dc256dbe423bb98b682cf129cdd432366"},
|
||||
{file = "grpcio_tools-1.69.0-cp312-cp312-win32.whl", hash = "sha256:e11accd10cf4af5031ac86c45f1a13fb08f55e005cea070917c12e78fe6d2aa2"},
|
||||
{file = "grpcio_tools-1.69.0-cp312-cp312-win_amd64.whl", hash = "sha256:6df4c6ac109af338a8ccde29d184e0b0bdab13d78490cb360ff9b192a1aec7e2"},
|
||||
{file = "grpcio_tools-1.69.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:8c320c4faa1431f2e1252ef2325a970ac23b2fd04ffef6c12f96dd4552c3445c"},
|
||||
{file = "grpcio_tools-1.69.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:5f1224596ad74dd14444b20c37122b361c5d203b67e14e018b995f3c5d76eede"},
|
||||
{file = "grpcio_tools-1.69.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:965a0cf656a113bc32d15ac92ca51ed702a75d5370ae0afbdd36f818533a708a"},
|
||||
{file = "grpcio_tools-1.69.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:978835768c11a7f28778b3b7c40f839d8a57f765c315e80c4246c23900d56149"},
|
||||
{file = "grpcio_tools-1.69.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:094c7cec9bd271a32dfb7c620d4a558c63fcb0122fd1651b9ed73d6afd4ae6fe"},
|
||||
{file = "grpcio_tools-1.69.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:b51bf4981b3d7e47c2569efadff08284787124eb3dea0f63f491d39703231d3c"},
|
||||
{file = "grpcio_tools-1.69.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea7aaf0dc1a828e2133357a9e9553fd1bb4e766890d52a506cc132e40632acdc"},
|
||||
{file = "grpcio_tools-1.69.0-cp313-cp313-win32.whl", hash = "sha256:4320f11b79d3a148cc23bad1b81719ce1197808dc2406caa8a8ba0a5cfb0260d"},
|
||||
{file = "grpcio_tools-1.69.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9bae733654e0eb8ca83aa1d0d6b6c2f4a3525ce70d5ffc07df68d28f6520137"},
|
||||
{file = "grpcio_tools-1.69.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:c78d3a7d9ba4292ba7abcc43430df426fc805e79a1dcd147509af0668332885b"},
|
||||
{file = "grpcio_tools-1.69.0-cp38-cp38-macosx_10_14_universal2.whl", hash = "sha256:497bdaa996a4de70f643c008a08813b4d20e114de50a384ae5e29d849c24c9c8"},
|
||||
{file = "grpcio_tools-1.69.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:aea33dd5a07a3b250b02a1b3f435e86d4abc94936b3ce634a2d70bc224189495"},
|
||||
{file = "grpcio_tools-1.69.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d3101c8d6f890f9d978e400843cc29992c5e03ae74f359e73dade09f2469a08"},
|
||||
{file = "grpcio_tools-1.69.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1163ba3f829141206dce1ceb67cfca73b57d279cd7183f188276443700a4980e"},
|
||||
{file = "grpcio_tools-1.69.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a85785058c31bac3d0b26c158b576eed536e4ce1af72c1d05a3518e745d44aac"},
|
||||
{file = "grpcio_tools-1.69.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ee934bbe8aa8035eea2711c12a6e537ab4c4a35a6d742ccf34bfa3a0492f412"},
|
||||
{file = "grpcio_tools-1.69.0-cp38-cp38-win32.whl", hash = "sha256:808d1b963bda8ca3c9f55cb8aa051ed2f2c98cc1fb89f79b4f67e8218580f8f3"},
|
||||
{file = "grpcio_tools-1.69.0-cp38-cp38-win_amd64.whl", hash = "sha256:afa8cd6b93e4f607c3750a976a96f874830ec7dc5f408e0fac270d0464147024"},
|
||||
{file = "grpcio_tools-1.69.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:01121b6570932bfb7d8b2ce2c0055dba902a415477079e249d85fe4494f72db2"},
|
||||
{file = "grpcio_tools-1.69.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:9861e282aa7b3656c67e84d0c25ee0e9210b955e0ec2c64699b8f80483f90853"},
|
||||
{file = "grpcio_tools-1.69.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:00adf628259e8c314a02ca1580d6a8b14eeef266f5dd5e15bf92c1efbbcf63c0"},
|
||||
{file = "grpcio_tools-1.69.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:371d03ac31b76ba77d44bdba6a8560f344c6d1ed558babab64760da085e392b7"},
|
||||
{file = "grpcio_tools-1.69.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6730414c01fe9027ba12538fd6e192e1bea94d5b819a1e03d15e89aab1b4573"},
|
||||
{file = "grpcio_tools-1.69.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5562a1b1b67deffd04fbb1bcf8f1634580538ce35895b77cdfaec1fb115efd95"},
|
||||
{file = "grpcio_tools-1.69.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f8996efddc867134f22bbf8a368b1b2a018d0a9b0ac9d3185cfd81d1abd8066"},
|
||||
{file = "grpcio_tools-1.69.0-cp39-cp39-win32.whl", hash = "sha256:8f5959d8a453d613e7137831f6885b43b5c378ec317943b4ec599046baa97bfc"},
|
||||
{file = "grpcio_tools-1.69.0-cp39-cp39-win_amd64.whl", hash = "sha256:5d47abf7e0662dd5dbb9cc252c3616e5fbc5f71d34e3f6332cd24bcdf2940abd"},
|
||||
{file = "grpcio_tools-1.69.0.tar.gz", hash = "sha256:3e1a98f4d9decb84979e1ddd3deb09c0a33a84b6e3c0776d5bde4097e3ab66dd"},
|
||||
{file = "grpcio_tools-1.70.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:4d456521290e25b1091975af71604facc5c7db162abdca67e12a0207b8bbacbe"},
|
||||
{file = "grpcio_tools-1.70.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:d50080bca84f53f3a05452e06e6251cbb4887f5a1d1321d1989e26d6e0dc398d"},
|
||||
{file = "grpcio_tools-1.70.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:02e3bf55fb569fe21b54a32925979156e320f9249bb247094c4cbaa60c23a80d"},
|
||||
{file = "grpcio_tools-1.70.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88a3ec6fa2381f616d567f996503e12ca353777941b61030fd9733fd5772860e"},
|
||||
{file = "grpcio_tools-1.70.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6034a0579fab2aed8685fa1a558de084668b1e9b01a82a4ca7458b9bedf4654c"},
|
||||
{file = "grpcio_tools-1.70.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:701bbb1ff406a21a771f5b1df6be516c0a59236774b6836eaad7696b1d128ea8"},
|
||||
{file = "grpcio_tools-1.70.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eeb86864e1432fc1ab61e03395a2a4c04e9dd9c89db07e6fe68c7c2ac8ec24f"},
|
||||
{file = "grpcio_tools-1.70.0-cp310-cp310-win32.whl", hash = "sha256:d53c8c45e843b5836781ad6b82a607c72c2f9a3f556e23d703a0e099222421fa"},
|
||||
{file = "grpcio_tools-1.70.0-cp310-cp310-win_amd64.whl", hash = "sha256:22024caee36ab65c2489594d718921dcbb5bd18d61c5417a9ede94fd8dc8a589"},
|
||||
{file = "grpcio_tools-1.70.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:5f5aba12d98d25c7ab2dd983939e2c21556a7d15f903b286f24d88d2c6e30c0a"},
|
||||
{file = "grpcio_tools-1.70.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:d47a6c6cfc526b290b7b53a37dd7e6932983f7a168b56aab760b4b597c47f30f"},
|
||||
{file = "grpcio_tools-1.70.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:b5a9beadd1e24772ffa2c70f07d72f73330d356b78b246e424f4f2ed6c6713f3"},
|
||||
{file = "grpcio_tools-1.70.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb8135eef160a62505f074bf7a3d62f3b13911c3c14037c5392bf877114213b5"},
|
||||
{file = "grpcio_tools-1.70.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7ac9b3e13ace8467a586c53580ee22f9732c355583f3c344ef8c6c0666219cc"},
|
||||
{file = "grpcio_tools-1.70.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:63f367363a4a1489a0046b19f9d561216ea0d206c40a6f1bf07a58ccfb7be480"},
|
||||
{file = "grpcio_tools-1.70.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54ceffef59a059d2c7304554a8bbb20eedb05a3f937159ab1c332c1b28e12c9f"},
|
||||
{file = "grpcio_tools-1.70.0-cp311-cp311-win32.whl", hash = "sha256:7a90a66a46821140a2a2b0be787dfabe42e22e9a5ba9cc70726b3e5c71a3b785"},
|
||||
{file = "grpcio_tools-1.70.0-cp311-cp311-win_amd64.whl", hash = "sha256:4ebf09733545a69c166b02caa14c34451e38855544820dab7fdde5c28e2dbffe"},
|
||||
{file = "grpcio_tools-1.70.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:ec5d6932c3173d7618267b3b3fd77b9243949c5ec04302b7338386d4f8544e0b"},
|
||||
{file = "grpcio_tools-1.70.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:f22852da12f53b02a3bdb29d0c32fcabab9c7c8f901389acffec8461083f110d"},
|
||||
{file = "grpcio_tools-1.70.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:7d45067e6efd20881e98a0e1d7edd7f207b1625ad7113321becbfe0a6ebee46c"},
|
||||
{file = "grpcio_tools-1.70.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3020c97f03b30eee3c26aa2a55fbe003f1729c6f879a378507c2c78524db7c12"},
|
||||
{file = "grpcio_tools-1.70.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7fd472fce3b33bdf7fbc24d40da7ab10d7a088bcaf59c37433c2c57330fbcb6"},
|
||||
{file = "grpcio_tools-1.70.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3875543d74ce1a698a11f498f83795216ce929cb29afa5fac15672c7ba1d6dd2"},
|
||||
{file = "grpcio_tools-1.70.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a130c24d617a3a57369da784080dfa8848444d41b7ae1250abc06e72e706a8d9"},
|
||||
{file = "grpcio_tools-1.70.0-cp312-cp312-win32.whl", hash = "sha256:8eae17c920d14e2e451dbb18f5d8148f884e10228061941b33faa8fceee86e73"},
|
||||
{file = "grpcio_tools-1.70.0-cp312-cp312-win_amd64.whl", hash = "sha256:99caa530242a0a832d8b6a6ab94b190c9b449d3e237f953911b4d56207569436"},
|
||||
{file = "grpcio_tools-1.70.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:f024688d04e7a9429489ed695b85628075c3c6d655198ba3c6ccbd1d8b7c333b"},
|
||||
{file = "grpcio_tools-1.70.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:1fa9a81621d7178498dedcf94eb8f276a7594327faf3dd5fd1935ce2819a2bdb"},
|
||||
{file = "grpcio_tools-1.70.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:c6da2585c0950cdb650df1ff6d85b3fe31e22f8370b9ee11f8fe641d5b4bf096"},
|
||||
{file = "grpcio_tools-1.70.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70234b592af17050ec30cf35894790cef52aeae87639efe6db854a7fa783cc8c"},
|
||||
{file = "grpcio_tools-1.70.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c021b040d0a9f5bb96a725c4d2b95008aad127d6bed124a7bbe854973014f5b"},
|
||||
{file = "grpcio_tools-1.70.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:114a42e566e5b16a47e98f7910a6c0074b37e2d1faacaae13222e463d0d0d43c"},
|
||||
{file = "grpcio_tools-1.70.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:4cae365d7e3ba297256216a9a256458b286f75c64603f017972b3ad1ee374437"},
|
||||
{file = "grpcio_tools-1.70.0-cp313-cp313-win32.whl", hash = "sha256:ae139a8d3ddd8353f62af3af018e99ebcd2f4a237bd319cb4b6f58dd608aaa54"},
|
||||
{file = "grpcio_tools-1.70.0-cp313-cp313-win_amd64.whl", hash = "sha256:04bf30c0eb2741defe3ab6e0a6102b022d69cfd39d68fab9b954993ceca8d346"},
|
||||
{file = "grpcio_tools-1.70.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:076f71c6d5adcf237ebca63f1ed51098293261dab9f301e3dfd180e896e5fa89"},
|
||||
{file = "grpcio_tools-1.70.0-cp38-cp38-macosx_10_14_universal2.whl", hash = "sha256:d1fc2112e9c40167086e2e6a929b253e5281bffd070fab7cd1ae019317ffc11d"},
|
||||
{file = "grpcio_tools-1.70.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:904f13d2d04f88178b09d8ef89549b90cbf8792b684a7c72540fc1a9887697e2"},
|
||||
{file = "grpcio_tools-1.70.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1de6c71833d36fb8cc8ac10539681756dc2c5c67e5d4aa4d05adb91ecbdd8474"},
|
||||
{file = "grpcio_tools-1.70.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ab788afced2d2c59bef86479967ce0b28485789a9f2cc43793bb7aa67f9528b"},
|
||||
{file = "grpcio_tools-1.70.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:836293dcbb1e59fa52aa8aa890bd7a32a8eea7651cd614e96d86de4f3032fe73"},
|
||||
{file = "grpcio_tools-1.70.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:740b3741d124c5f390dd50ad1c42c11788882baf3c202cd3e69adee0e3dde559"},
|
||||
{file = "grpcio_tools-1.70.0-cp38-cp38-win32.whl", hash = "sha256:b9e4a12b862ba5e42d8028da311e8d4a2c307362659b2f4141d0f940f8c12b49"},
|
||||
{file = "grpcio_tools-1.70.0-cp38-cp38-win_amd64.whl", hash = "sha256:fd04c93af460b1456cd12f8f85502503e1db6c4adc1b7d4bd775b12c1fd94fee"},
|
||||
{file = "grpcio_tools-1.70.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:52d7e7ef11867fe7de577076b1f2ac6bf106b2325130e3de66f8c364c96ff332"},
|
||||
{file = "grpcio_tools-1.70.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:0f7ed0372afd9f5eb938334e84681396257015ab92e03de009aa3170e64b24d0"},
|
||||
{file = "grpcio_tools-1.70.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:24a5b0328ffcfe0c4a9024f302545abdb8d6f24921409a5839f2879555b96fea"},
|
||||
{file = "grpcio_tools-1.70.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9387b30f3b2f46942fb5718624d7421875a6ce458620d6e15817172d78db1e1a"},
|
||||
{file = "grpcio_tools-1.70.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4545264e06e1cd7fb21b9447bb5126330bececb4bc626c98f793fda2fd910bf8"},
|
||||
{file = "grpcio_tools-1.70.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79b723ce30416e8e1d7ff271f97ade79aaf30309a595d80c377105c07f5b20fd"},
|
||||
{file = "grpcio_tools-1.70.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1c0917dce12af04529606d437def83962d51c59dcde905746134222e94a2ab1b"},
|
||||
{file = "grpcio_tools-1.70.0-cp39-cp39-win32.whl", hash = "sha256:5cb0baa52d4d44690fac6b1040197c694776a291a90e2d3c369064b4d5bc6642"},
|
||||
{file = "grpcio_tools-1.70.0-cp39-cp39-win_amd64.whl", hash = "sha256:840ec536ab933db2ef8d5acaa6b712d0e9e8f397f62907c852ec50a3f69cdb78"},
|
||||
{file = "grpcio_tools-1.70.0.tar.gz", hash = "sha256:e578fee7c1c213c8e471750d92631d00f178a15479fb2cb3b939a07fc125ccd3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
grpcio = ">=1.69.0"
|
||||
grpcio = ">=1.70.0"
|
||||
protobuf = ">=5.26.1,<6.0dev"
|
||||
setuptools = "*"
|
||||
|
||||
@ -2465,17 +2465,17 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langchain-openai"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
description = "An integration package connecting OpenAI and LangChain"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "langchain_openai-0.3.1-py3-none-any.whl", hash = "sha256:5cf2a1e115b12570158d89c22832fa381803c3e1e11d1eb781195c8d9e454bd5"},
|
||||
{file = "langchain_openai-0.3.1.tar.gz", hash = "sha256:cce314f1437b2cad73e0ed2b55e74dc399bc1bbc43594c4448912fb51c5e4447"},
|
||||
{file = "langchain_openai-0.3.2-py3-none-any.whl", hash = "sha256:8674183805e26d3ae3f78cc44f79fe0b2066f61e2de0e7e18be3b86f0d3b2759"},
|
||||
{file = "langchain_openai-0.3.2.tar.gz", hash = "sha256:c2c80ac0208eb7cefdef96f6353b00fa217979ffe83f0a21cc8666001df828c1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
langchain-core = ">=0.3.30,<0.4.0"
|
||||
langchain-core = ">=0.3.31,<0.4.0"
|
||||
openai = ">=1.58.1,<2.0.0"
|
||||
tiktoken = ">=0.7,<1"
|
||||
|
||||
@ -2537,13 +2537,13 @@ pytest = ["pytest (>=7.0.0)", "rich (>=13.9.4,<14.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "letta-client"
|
||||
version = "0.1.17"
|
||||
version = "0.1.19"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.8"
|
||||
files = [
|
||||
{file = "letta_client-0.1.17-py3-none-any.whl", hash = "sha256:b60996bb64c574ec0352a5256e5ce8c16bf72d462c244cf867afb5da2c49151f"},
|
||||
{file = "letta_client-0.1.17.tar.gz", hash = "sha256:5172af77d5f6997b641219dabc68c925130372e47ca6718430e423a457ba2e8b"},
|
||||
{file = "letta_client-0.1.19-py3-none-any.whl", hash = "sha256:45cdfff85a19c7cab676f27a99fd268ec535d79adde0ea828dce9d305fac2e55"},
|
||||
{file = "letta_client-0.1.19.tar.gz", hash = "sha256:053fda535063ede4a74c2eff2af635524a19e10f4fca48b2649e85e3a6d19393"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -2571,19 +2571,19 @@ pydantic = ">=1.10"
|
||||
|
||||
[[package]]
|
||||
name = "llama-index"
|
||||
version = "0.12.12"
|
||||
version = "0.12.13"
|
||||
description = "Interface between LLMs and your data"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "llama_index-0.12.12-py3-none-any.whl", hash = "sha256:208f77dba5fd8268cacd3d56ec3ee33b0001d5b6ec623c5b91c755af7b08cfae"},
|
||||
{file = "llama_index-0.12.12.tar.gz", hash = "sha256:d4e475726e342b1178736ae3ed93336fe114605e86431b6dfcb454a9e1f26e72"},
|
||||
{file = "llama_index-0.12.13-py3-none-any.whl", hash = "sha256:0b285aa451ced6bd8da40df99068ac96badf8b5725c4edc29f2bce4da2ffd8bc"},
|
||||
{file = "llama_index-0.12.13.tar.gz", hash = "sha256:1e39a397dcc51dabe280c121fd8d5451a6a84595233a8b26caa54d9b7ecf9ffc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
llama-index-agent-openai = ">=0.4.0,<0.5.0"
|
||||
llama-index-cli = ">=0.4.0,<0.5.0"
|
||||
llama-index-core = ">=0.12.12,<0.13.0"
|
||||
llama-index-core = ">=0.12.13,<0.13.0"
|
||||
llama-index-embeddings-openai = ">=0.3.0,<0.4.0"
|
||||
llama-index-indices-managed-llama-cloud = ">=0.4.0"
|
||||
llama-index-llms-openai = ">=0.3.0,<0.4.0"
|
||||
@ -2628,13 +2628,13 @@ llama-index-llms-openai = ">=0.3.0,<0.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "llama-index-core"
|
||||
version = "0.12.12"
|
||||
version = "0.12.13"
|
||||
description = "Interface between LLMs and your data"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "llama_index_core-0.12.12-py3-none-any.whl", hash = "sha256:cea491e87f65e6b775b5aef95720de302b85af1bdc67d779c4b09170a30e5b98"},
|
||||
{file = "llama_index_core-0.12.12.tar.gz", hash = "sha256:068b755bbc681731336e822f5977d7608585e8f759c6293ebd812e2659316a37"},
|
||||
{file = "llama_index_core-0.12.13-py3-none-any.whl", hash = "sha256:9708bb594bbddffd6ff0767242e49d8978d1ba60a2e62e071d9d123ad2f17e6f"},
|
||||
{file = "llama_index_core-0.12.13.tar.gz", hash = "sha256:77af0161246ce1de38efc17cb6438dfff9e9558af00bcfac7dd4d0b7325efa4b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -2755,13 +2755,13 @@ llama-index-program-openai = ">=0.3.0,<0.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "llama-index-readers-file"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
description = "llama-index readers file integration"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "llama_index_readers_file-0.4.3-py3-none-any.whl", hash = "sha256:c669da967ea534e3af3660f9fd730c71c725288f5c57906bcce338414ebeee5c"},
|
||||
{file = "llama_index_readers_file-0.4.3.tar.gz", hash = "sha256:07514bebed7ce431c1b3ef9279d09aa3d1bba8e342d661860a033355b98fb33a"},
|
||||
{file = "llama_index_readers_file-0.4.4-py3-none-any.whl", hash = "sha256:01589a4895e2d4abad30294c9b0d2813520ee1f5164922ad92f11e64a1d65d6c"},
|
||||
{file = "llama_index_readers_file-0.4.4.tar.gz", hash = "sha256:e076b3fa1e68eea1594d47cec1f64b384fb6067f2697ca8aae22b4a21ad27ca7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -6441,4 +6441,4 @@ tests = ["wikipedia"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "<3.14,>=3.10"
|
||||
content-hash = "effb82094dcdc8c73c1c3e4277a7d3012f33ff7d8b4cf114f90b55df7f663587"
|
||||
content-hash = "58f66b702bd791fcf73f48fa59a1bb0930370832427c1660ebbc81b9c58d1123"
|
||||
|
@ -78,6 +78,7 @@ llama-index-embeddings-openai = "^0.3.1"
|
||||
e2b-code-interpreter = {version = "^1.0.3", optional = true}
|
||||
anthropic = "^0.43.0"
|
||||
letta_client = "^0.1.16"
|
||||
openai = "^1.60.0"
|
||||
colorama = "^0.4.6"
|
||||
|
||||
|
||||
|
@ -75,8 +75,10 @@ def test_ripple_edit(client, mock_e2b_api_key_none):
|
||||
# limit=2000,
|
||||
# )
|
||||
# new_memory = Block(name="rethink_memory_block", label="rethink_memory_block", value="[empty]", limit=2000)
|
||||
conversation_memory = BasicBlockMemory(blocks=[conversation_persona_block, conversation_human_block, fact_block, new_memory])
|
||||
offline_memory = BasicBlockMemory(blocks=[offline_persona_block, offline_human_block, fact_block, new_memory])
|
||||
# conversation_memory = BasicBlockMemory(blocks=[conversation_persona_block, conversation_human_block, fact_block, new_memory])
|
||||
conversation_memory = BasicBlockMemory(blocks=[conversation_persona_block, conversation_human_block, fact_block])
|
||||
# offline_memory = BasicBlockMemory(blocks=[offline_persona_block, offline_human_block, fact_block, new_memory])
|
||||
offline_memory = BasicBlockMemory(blocks=[offline_persona_block, offline_human_block, fact_block])
|
||||
|
||||
conversation_agent = client.create_agent(
|
||||
name="conversation_agent",
|
||||
@ -86,6 +88,7 @@ def test_ripple_edit(client, mock_e2b_api_key_none):
|
||||
embedding_config=EmbeddingConfig.default_config("text-embedding-ada-002"),
|
||||
tool_ids=[send_message.id, trigger_rethink_memory_tool.id],
|
||||
memory=conversation_memory,
|
||||
block_ids=[new_memory.id],
|
||||
include_base_tools=False,
|
||||
)
|
||||
assert conversation_agent is not None
|
||||
@ -103,6 +106,7 @@ def test_ripple_edit(client, mock_e2b_api_key_none):
|
||||
embedding_config=EmbeddingConfig.default_config("text-embedding-ada-002"),
|
||||
tool_ids=[rethink_memory_tool.id, finish_rethinking_memory_tool.id],
|
||||
tool_rules=[TerminalToolRule(tool_name=finish_rethinking_memory_tool.name)],
|
||||
block_ids=[new_memory.id],
|
||||
include_base_tools=False,
|
||||
)
|
||||
assert offline_memory_agent is not None
|
||||
|
@ -14,7 +14,7 @@ from letta.llm_api.helpers import calculate_summarizer_cutoff
|
||||
from letta.schemas.embedding_config import EmbeddingConfig
|
||||
from letta.schemas.enums import MessageRole
|
||||
from letta.schemas.llm_config import LLMConfig
|
||||
from letta.schemas.message import Message
|
||||
from letta.schemas.message import Message, TextContent
|
||||
from letta.settings import summarizer_settings
|
||||
from letta.streaming_interface import StreamingRefreshCLIInterface
|
||||
from tests.helpers.endpoints_helper import EMBEDDING_CONFIG_PATH
|
||||
@ -55,7 +55,7 @@ def generate_message(role: str, text: str = None, tool_calls: List = None) -> Me
|
||||
return Message(
|
||||
id="message-" + str(uuid.uuid4()),
|
||||
role=MessageRole(role),
|
||||
text=text or f"{role} message text",
|
||||
content=[TextContent(text=text or f"{role} message text")],
|
||||
created_at=datetime.utcnow(),
|
||||
tool_calls=tool_calls or [],
|
||||
)
|
||||
|
@ -249,7 +249,7 @@ def test_agent_tags(client: Union[LocalClient, RESTClient]):
|
||||
assert paginated_tags[1] == "agent2"
|
||||
|
||||
# Test pagination with cursor
|
||||
next_page_tags = client.get_tags(cursor="agent2", limit=2)
|
||||
next_page_tags = client.get_tags(after="agent2", limit=2)
|
||||
assert len(next_page_tags) == 2
|
||||
assert next_page_tags[0] == "agent3"
|
||||
assert next_page_tags[1] == "development"
|
||||
@ -654,7 +654,7 @@ def test_agent_listing(client: Union[LocalClient, RESTClient], agent, search_age
|
||||
assert len(first_page) == 1
|
||||
first_agent = first_page[0]
|
||||
|
||||
second_page = client.list_agents(query_text="search agent", cursor=first_agent.id, limit=1) # Use agent ID as cursor
|
||||
second_page = client.list_agents(query_text="search agent", after=first_agent.id, limit=1) # Use agent ID as cursor
|
||||
assert len(second_page) == 1
|
||||
assert second_page[0].id != first_agent.id
|
||||
|
||||
|
@ -369,7 +369,7 @@ def test_list_files_pagination(client: Union[LocalClient, RESTClient], agent: Ag
|
||||
assert files_a[0].source_id == source.id
|
||||
|
||||
# Use the cursor from response_a to get the remaining file
|
||||
files_b = client.list_files_from_source(source.id, limit=1, cursor=files_a[-1].id)
|
||||
files_b = client.list_files_from_source(source.id, limit=1, after=files_a[-1].id)
|
||||
assert len(files_b) == 1
|
||||
assert files_b[0].source_id == source.id
|
||||
|
||||
@ -377,7 +377,7 @@ def test_list_files_pagination(client: Union[LocalClient, RESTClient], agent: Ag
|
||||
assert files_a[0].file_name != files_b[0].file_name
|
||||
|
||||
# Use the cursor from response_b to list files, should be empty
|
||||
files = client.list_files_from_source(source.id, limit=1, cursor=files_b[-1].id)
|
||||
files = client.list_files_from_source(source.id, limit=1, after=files_b[-1].id)
|
||||
assert len(files) == 0 # Should be empty
|
||||
|
||||
|
||||
@ -628,7 +628,7 @@ def test_initial_message_sequence(client: Union[LocalClient, RESTClient], agent:
|
||||
empty_agent_state = client.create_agent(name="test-empty-message-sequence", initial_message_sequence=[])
|
||||
cleanup_agents.append(empty_agent_state.id)
|
||||
|
||||
custom_sequence = [MessageCreate(**{"text": "Hello, how are you?", "role": MessageRole.user})]
|
||||
custom_sequence = [MessageCreate(**{"content": "Hello, how are you?", "role": MessageRole.user})]
|
||||
custom_agent_state = client.create_agent(name="test-custom-message-sequence", initial_message_sequence=custom_sequence)
|
||||
cleanup_agents.append(custom_agent_state.id)
|
||||
assert custom_agent_state.message_ids is not None
|
||||
@ -637,7 +637,7 @@ def test_initial_message_sequence(client: Union[LocalClient, RESTClient], agent:
|
||||
), f"Expected {len(custom_sequence) + 1} messages, got {len(custom_agent_state.message_ids)}"
|
||||
# assert custom_agent_state.message_ids[1:] == [msg.id for msg in custom_sequence]
|
||||
# shoule be contained in second message (after system message)
|
||||
assert custom_sequence[0].text in client.get_in_context_messages(custom_agent_state.id)[1].text
|
||||
assert custom_sequence[0].content in client.get_in_context_messages(custom_agent_state.id)[1].text
|
||||
|
||||
|
||||
def test_add_and_manage_tags_for_agent(client: Union[LocalClient, RESTClient], agent: AgentState):
|
||||
|
@ -447,7 +447,7 @@ def comprehensive_test_agent_fixture(server: SyncServer, default_user, print_too
|
||||
description="test_description",
|
||||
metadata={"test_key": "test_value"},
|
||||
tool_rules=[InitToolRule(tool_name=print_tool.name)],
|
||||
initial_message_sequence=[MessageCreate(role=MessageRole.user, text="hello world")],
|
||||
initial_message_sequence=[MessageCreate(role=MessageRole.user, content="hello world")],
|
||||
tool_exec_environment_variables={"test_env_var_key_a": "test_env_var_value_a", "test_env_var_key_b": "test_env_var_value_b"},
|
||||
)
|
||||
created_agent = server.agent_manager.create_agent(
|
||||
@ -549,7 +549,7 @@ def test_create_agent_passed_in_initial_messages(server: SyncServer, default_use
|
||||
block_ids=[default_block.id],
|
||||
tags=["a", "b"],
|
||||
description="test_description",
|
||||
initial_message_sequence=[MessageCreate(role=MessageRole.user, text="hello world")],
|
||||
initial_message_sequence=[MessageCreate(role=MessageRole.user, content="hello world")],
|
||||
)
|
||||
agent_state = server.agent_manager.create_agent(
|
||||
create_agent_request,
|
||||
@ -562,7 +562,7 @@ def test_create_agent_passed_in_initial_messages(server: SyncServer, default_use
|
||||
assert create_agent_request.memory_blocks[0].value in init_messages[0].text
|
||||
# Check that the second message is the passed in initial message seq
|
||||
assert create_agent_request.initial_message_sequence[0].role == init_messages[1].role
|
||||
assert create_agent_request.initial_message_sequence[0].text in init_messages[1].text
|
||||
assert create_agent_request.initial_message_sequence[0].content in init_messages[1].text
|
||||
|
||||
|
||||
def test_create_agent_default_initial_message(server: SyncServer, default_user, default_block):
|
||||
@ -914,11 +914,18 @@ def test_list_agents_by_tags_pagination(server: SyncServer, default_user, defaul
|
||||
|
||||
# Get second page using cursor
|
||||
second_page = server.agent_manager.list_agents(
|
||||
tags=["pagination_test"], match_all_tags=True, actor=default_user, cursor=first_agent_id, limit=1
|
||||
tags=["pagination_test"], match_all_tags=True, actor=default_user, after=first_agent_id, limit=1
|
||||
)
|
||||
assert len(second_page) == 1
|
||||
assert second_page[0].id != first_agent_id
|
||||
|
||||
# Get previous page using before
|
||||
prev_page = server.agent_manager.list_agents(
|
||||
tags=["pagination_test"], match_all_tags=True, actor=default_user, before=second_page[0].id, limit=1
|
||||
)
|
||||
assert len(prev_page) == 1
|
||||
assert prev_page[0].id == first_agent_id
|
||||
|
||||
# Verify we got both agents with no duplicates
|
||||
all_ids = {first_page[0].id, second_page[0].id}
|
||||
assert len(all_ids) == 2
|
||||
@ -980,10 +987,20 @@ def test_list_agents_query_text_pagination(server: SyncServer, default_user, def
|
||||
first_agent_id = first_page[0].id
|
||||
|
||||
# Get second page using cursor
|
||||
second_page = server.agent_manager.list_agents(actor=default_user, query_text="search agent", cursor=first_agent_id, limit=1)
|
||||
second_page = server.agent_manager.list_agents(actor=default_user, query_text="search agent", after=first_agent_id, limit=1)
|
||||
assert len(second_page) == 1
|
||||
assert second_page[0].id != first_agent_id
|
||||
|
||||
# Test before and after
|
||||
all_agents = server.agent_manager.list_agents(actor=default_user, query_text="agent")
|
||||
assert len(all_agents) == 3
|
||||
first_agent, second_agent, third_agent = all_agents
|
||||
middle_agent = server.agent_manager.list_agents(
|
||||
actor=default_user, query_text="search agent", before=third_agent.id, after=first_agent.id
|
||||
)
|
||||
assert len(middle_agent) == 1
|
||||
assert middle_agent[0].id == second_agent.id
|
||||
|
||||
# Verify we got both search agents with no duplicates
|
||||
all_ids = {first_page[0].id, second_page[0].id}
|
||||
assert len(all_ids) == 2
|
||||
@ -1139,6 +1156,10 @@ def test_detach_block(server: SyncServer, sarah_agent, default_block, default_us
|
||||
agent = server.agent_manager.get_agent_by_id(sarah_agent.id, actor=default_user)
|
||||
assert len(agent.memory.blocks) == 0
|
||||
|
||||
# Check that block still exists
|
||||
block = server.block_manager.get_block_by_id(block_id=default_block.id, actor=default_user)
|
||||
assert block
|
||||
|
||||
|
||||
def test_detach_nonexistent_block(server: SyncServer, sarah_agent, default_user):
|
||||
"""Test detaching a block that isn't attached."""
|
||||
@ -1232,12 +1253,33 @@ def test_agent_list_passages_pagination(server, default_user, sarah_agent, agent
|
||||
assert len(first_page) == 2
|
||||
|
||||
second_page = server.agent_manager.list_passages(
|
||||
actor=default_user, agent_id=sarah_agent.id, cursor=first_page[-1].id, limit=2, ascending=True
|
||||
actor=default_user, agent_id=sarah_agent.id, after=first_page[-1].id, limit=2, ascending=True
|
||||
)
|
||||
assert len(second_page) == 2
|
||||
assert first_page[-1].id != second_page[0].id
|
||||
assert first_page[-1].created_at <= second_page[0].created_at
|
||||
|
||||
"""
|
||||
[1] [2]
|
||||
* * | * *
|
||||
|
||||
[mid]
|
||||
* | * * | *
|
||||
"""
|
||||
middle_page = server.agent_manager.list_passages(
|
||||
actor=default_user, agent_id=sarah_agent.id, before=second_page[-1].id, after=first_page[0].id, ascending=True
|
||||
)
|
||||
assert len(middle_page) == 2
|
||||
assert middle_page[0].id == first_page[-1].id
|
||||
assert middle_page[1].id == second_page[0].id
|
||||
|
||||
middle_page_desc = server.agent_manager.list_passages(
|
||||
actor=default_user, agent_id=sarah_agent.id, before=second_page[-1].id, after=first_page[0].id, ascending=False
|
||||
)
|
||||
assert len(middle_page_desc) == 2
|
||||
assert middle_page_desc[0].id == second_page[0].id
|
||||
assert middle_page_desc[1].id == first_page[-1].id
|
||||
|
||||
|
||||
def test_agent_list_passages_text_search(server, default_user, sarah_agent, agent_passages_setup):
|
||||
"""Test text search functionality of agent passages"""
|
||||
@ -1402,11 +1444,11 @@ def test_list_organizations_pagination(server: SyncServer):
|
||||
orgs_x = server.organization_manager.list_organizations(limit=1)
|
||||
assert len(orgs_x) == 1
|
||||
|
||||
orgs_y = server.organization_manager.list_organizations(cursor=orgs_x[0].id, limit=1)
|
||||
orgs_y = server.organization_manager.list_organizations(after=orgs_x[0].id, limit=1)
|
||||
assert len(orgs_y) == 1
|
||||
assert orgs_y[0].name != orgs_x[0].name
|
||||
|
||||
orgs = server.organization_manager.list_organizations(cursor=orgs_y[0].id, limit=1)
|
||||
orgs = server.organization_manager.list_organizations(after=orgs_y[0].id, limit=1)
|
||||
assert len(orgs) == 0
|
||||
|
||||
|
||||
@ -1789,7 +1831,7 @@ def test_message_get_by_id(server: SyncServer, hello_world_message_fixture, defa
|
||||
def test_message_update(server: SyncServer, hello_world_message_fixture, default_user, other_user):
|
||||
"""Test updating a message"""
|
||||
new_text = "Updated text"
|
||||
updated = server.message_manager.update_message_by_id(hello_world_message_fixture.id, MessageUpdate(text=new_text), actor=other_user)
|
||||
updated = server.message_manager.update_message_by_id(hello_world_message_fixture.id, MessageUpdate(content=new_text), actor=other_user)
|
||||
assert updated is not None
|
||||
assert updated.text == new_text
|
||||
retrieved = server.message_manager.get_message_by_id(hello_world_message_fixture.id, actor=default_user)
|
||||
@ -1871,7 +1913,7 @@ def test_message_listing_cursor(server: SyncServer, hello_world_message_fixture,
|
||||
"""Test cursor-based pagination functionality"""
|
||||
create_test_messages(server, hello_world_message_fixture, default_user)
|
||||
|
||||
# Make sure there are 5 messages
|
||||
# Make sure there are 6 messages
|
||||
assert server.message_manager.size(actor=default_user, role=MessageRole.user) == 6
|
||||
|
||||
# Get first page
|
||||
@ -1882,11 +1924,28 @@ def test_message_listing_cursor(server: SyncServer, hello_world_message_fixture,
|
||||
|
||||
# Get second page
|
||||
second_page = server.message_manager.list_user_messages_for_agent(
|
||||
agent_id=sarah_agent.id, actor=default_user, cursor=last_id_on_first_page, limit=3
|
||||
agent_id=sarah_agent.id, actor=default_user, after=last_id_on_first_page, limit=3
|
||||
)
|
||||
assert len(second_page) == 3 # Should have 2 remaining messages
|
||||
assert len(second_page) == 3 # Should have 3 remaining messages
|
||||
assert all(r1.id != r2.id for r1 in first_page for r2 in second_page)
|
||||
|
||||
# Get the middle
|
||||
middle_page = server.message_manager.list_user_messages_for_agent(
|
||||
agent_id=sarah_agent.id, actor=default_user, before=second_page[1].id, after=first_page[0].id
|
||||
)
|
||||
assert len(middle_page) == 3
|
||||
assert middle_page[0].id == first_page[1].id
|
||||
assert middle_page[1].id == first_page[-1].id
|
||||
assert middle_page[-1].id == second_page[0].id
|
||||
|
||||
middle_page_desc = server.message_manager.list_user_messages_for_agent(
|
||||
agent_id=sarah_agent.id, actor=default_user, before=second_page[1].id, after=first_page[0].id, ascending=False
|
||||
)
|
||||
assert len(middle_page_desc) == 3
|
||||
assert middle_page_desc[0].id == second_page[0].id
|
||||
assert middle_page_desc[1].id == first_page[-1].id
|
||||
assert middle_page_desc[-1].id == first_page[1].id
|
||||
|
||||
|
||||
def test_message_listing_filtering(server: SyncServer, hello_world_message_fixture, default_user, sarah_agent):
|
||||
"""Test filtering messages by agent ID"""
|
||||
@ -2026,6 +2085,46 @@ def test_delete_block(server: SyncServer, default_user):
|
||||
assert len(blocks) == 0
|
||||
|
||||
|
||||
def test_delete_block_detaches_from_agent(server: SyncServer, sarah_agent, default_user):
|
||||
# Create and delete a block
|
||||
block = server.block_manager.create_or_update_block(PydanticBlock(label="human", value="Sample content"), actor=default_user)
|
||||
agent_state = server.agent_manager.attach_block(agent_id=sarah_agent.id, block_id=block.id, actor=default_user)
|
||||
|
||||
# Check that block has been attached
|
||||
assert block.id in [b.id for b in agent_state.memory.blocks]
|
||||
|
||||
# Now attempt to delete the block
|
||||
server.block_manager.delete_block(block_id=block.id, actor=default_user)
|
||||
|
||||
# Verify that the block was deleted
|
||||
blocks = server.block_manager.get_blocks(actor=default_user)
|
||||
assert len(blocks) == 0
|
||||
|
||||
# Check that block has been detached too
|
||||
agent_state = server.agent_manager.get_agent_by_id(agent_id=sarah_agent.id, actor=default_user)
|
||||
assert not (block.id in [b.id for b in agent_state.memory.blocks])
|
||||
|
||||
|
||||
def test_get_agents_for_block(server: SyncServer, sarah_agent, charles_agent, default_user):
|
||||
# Create and delete a block
|
||||
block = server.block_manager.create_or_update_block(PydanticBlock(label="alien", value="Sample content"), actor=default_user)
|
||||
sarah_agent = server.agent_manager.attach_block(agent_id=sarah_agent.id, block_id=block.id, actor=default_user)
|
||||
charles_agent = server.agent_manager.attach_block(agent_id=charles_agent.id, block_id=block.id, actor=default_user)
|
||||
|
||||
# Check that block has been attached to both
|
||||
assert block.id in [b.id for b in sarah_agent.memory.blocks]
|
||||
assert block.id in [b.id for b in charles_agent.memory.blocks]
|
||||
|
||||
# Get the agents for that block
|
||||
agent_states = server.block_manager.get_agents_for_block(block_id=block.id, actor=default_user)
|
||||
assert len(agent_states) == 2
|
||||
|
||||
# Check both agents are in the list
|
||||
agent_state_ids = [a.id for a in agent_states]
|
||||
assert sarah_agent.id in agent_state_ids
|
||||
assert charles_agent.id in agent_state_ids
|
||||
|
||||
|
||||
# ======================================================================================================================
|
||||
# SourceManager Tests - Sources
|
||||
# ======================================================================================================================
|
||||
@ -2118,7 +2217,7 @@ def test_list_sources(server: SyncServer, default_user):
|
||||
assert len(paginated_sources) == 1
|
||||
|
||||
# Ensure cursor-based pagination works
|
||||
next_page = server.source_manager.list_sources(actor=default_user, cursor=paginated_sources[-1].id, limit=1)
|
||||
next_page = server.source_manager.list_sources(actor=default_user, after=paginated_sources[-1].id, limit=1)
|
||||
assert len(next_page) == 1
|
||||
assert next_page[0].name != paginated_sources[0].name
|
||||
|
||||
@ -2218,7 +2317,7 @@ def test_list_files(server: SyncServer, default_user, default_source):
|
||||
assert len(paginated_files) == 1
|
||||
|
||||
# Ensure cursor-based pagination works
|
||||
next_page = server.source_manager.list_files(source_id=default_source.id, actor=default_user, cursor=paginated_files[-1].id, limit=1)
|
||||
next_page = server.source_manager.list_files(source_id=default_source.id, actor=default_user, after=paginated_files[-1].id, limit=1)
|
||||
assert len(next_page) == 1
|
||||
assert next_page[0].file_name != paginated_files[0].file_name
|
||||
|
||||
@ -2316,7 +2415,7 @@ def test_list_sandbox_configs(server: SyncServer, default_user):
|
||||
paginated_configs = server.sandbox_config_manager.list_sandbox_configs(actor=default_user, limit=1)
|
||||
assert len(paginated_configs) == 1
|
||||
|
||||
next_page = server.sandbox_config_manager.list_sandbox_configs(actor=default_user, cursor=paginated_configs[-1].id, limit=1)
|
||||
next_page = server.sandbox_config_manager.list_sandbox_configs(actor=default_user, after=paginated_configs[-1].id, limit=1)
|
||||
assert len(next_page) == 1
|
||||
assert next_page[0].id != paginated_configs[0].id
|
||||
|
||||
@ -2387,7 +2486,7 @@ def test_list_sandbox_env_vars(server: SyncServer, sandbox_config_fixture, defau
|
||||
assert len(paginated_env_vars) == 1
|
||||
|
||||
next_page = server.sandbox_config_manager.list_sandbox_env_vars(
|
||||
sandbox_config_id=sandbox_config_fixture.id, actor=default_user, cursor=paginated_env_vars[-1].id, limit=1
|
||||
sandbox_config_id=sandbox_config_fixture.id, actor=default_user, after=paginated_env_vars[-1].id, limit=1
|
||||
)
|
||||
assert len(next_page) == 1
|
||||
assert next_page[0].id != paginated_env_vars[0].id
|
||||
@ -2541,11 +2640,46 @@ def test_list_jobs_pagination(server: SyncServer, default_user):
|
||||
|
||||
# List jobs with a limit
|
||||
jobs = server.job_manager.list_jobs(actor=default_user, limit=5)
|
||||
|
||||
# Assertions to check pagination
|
||||
assert len(jobs) == 5
|
||||
assert all(job.user_id == default_user.id for job in jobs)
|
||||
|
||||
# Test cursor-based pagination
|
||||
first_page = server.job_manager.list_jobs(actor=default_user, limit=3, ascending=True) # [J0, J1, J2]
|
||||
assert len(first_page) == 3
|
||||
assert first_page[0].created_at <= first_page[1].created_at <= first_page[2].created_at
|
||||
|
||||
last_page = server.job_manager.list_jobs(actor=default_user, limit=3, ascending=False) # [J9, J8, J7]
|
||||
assert len(last_page) == 3
|
||||
assert last_page[0].created_at >= last_page[1].created_at >= last_page[2].created_at
|
||||
first_page_ids = set(job.id for job in first_page)
|
||||
last_page_ids = set(job.id for job in last_page)
|
||||
assert first_page_ids.isdisjoint(last_page_ids)
|
||||
|
||||
# Test middle page using both before and after
|
||||
middle_page = server.job_manager.list_jobs(
|
||||
actor=default_user, before=last_page[-1].id, after=first_page[-1].id, ascending=True
|
||||
) # [J3, J4, J5, J6]
|
||||
assert len(middle_page) == 4 # Should include jobs between first and second page
|
||||
head_tail_jobs = first_page_ids.union(last_page_ids)
|
||||
assert all(job.id not in head_tail_jobs for job in middle_page)
|
||||
|
||||
# Test descending order
|
||||
middle_page_desc = server.job_manager.list_jobs(
|
||||
actor=default_user, before=last_page[-1].id, after=first_page[-1].id, ascending=False
|
||||
) # [J6, J5, J4, J3]
|
||||
assert len(middle_page_desc) == 4
|
||||
assert middle_page_desc[0].id == middle_page[-1].id
|
||||
assert middle_page_desc[1].id == middle_page[-2].id
|
||||
assert middle_page_desc[2].id == middle_page[-3].id
|
||||
assert middle_page_desc[3].id == middle_page[-4].id
|
||||
|
||||
# BONUS
|
||||
job_7 = last_page[-1].id
|
||||
earliest_jobs = server.job_manager.list_jobs(actor=default_user, ascending=False, before=job_7)
|
||||
assert len(earliest_jobs) == 7
|
||||
assert all(j.id not in last_page_ids for j in earliest_jobs)
|
||||
assert all(earliest_jobs[i].created_at >= earliest_jobs[i + 1].created_at for i in range(len(earliest_jobs) - 1))
|
||||
|
||||
|
||||
def test_list_jobs_by_status(server: SyncServer, default_user):
|
||||
"""Test listing jobs filtered by status."""
|
||||
@ -2660,15 +2794,81 @@ def test_job_messages_pagination(server: SyncServer, default_run, default_user,
|
||||
assert messages[1].id == message_ids[1]
|
||||
|
||||
# Test pagination with cursor
|
||||
messages = server.job_manager.get_job_messages(
|
||||
first_page = server.job_manager.get_job_messages(
|
||||
job_id=default_run.id,
|
||||
actor=default_user,
|
||||
cursor=message_ids[1],
|
||||
limit=2,
|
||||
ascending=True, # [M0, M1]
|
||||
)
|
||||
assert len(messages) == 2
|
||||
assert messages[0].id == message_ids[2]
|
||||
assert messages[1].id == message_ids[3]
|
||||
assert len(first_page) == 2
|
||||
assert first_page[0].id == message_ids[0]
|
||||
assert first_page[1].id == message_ids[1]
|
||||
assert first_page[0].created_at <= first_page[1].created_at
|
||||
|
||||
last_page = server.job_manager.get_job_messages(
|
||||
job_id=default_run.id,
|
||||
actor=default_user,
|
||||
limit=2,
|
||||
ascending=False, # [M4, M3]
|
||||
)
|
||||
assert len(last_page) == 2
|
||||
assert last_page[0].id == message_ids[4]
|
||||
assert last_page[1].id == message_ids[3]
|
||||
assert last_page[0].created_at >= last_page[1].created_at
|
||||
|
||||
first_page_ids = set(msg.id for msg in first_page)
|
||||
last_page_ids = set(msg.id for msg in last_page)
|
||||
assert first_page_ids.isdisjoint(last_page_ids)
|
||||
|
||||
# Test middle page using both before and after
|
||||
middle_page = server.job_manager.get_job_messages(
|
||||
job_id=default_run.id,
|
||||
actor=default_user,
|
||||
before=last_page[-1].id, # M3
|
||||
after=first_page[0].id, # M0
|
||||
ascending=True, # [M1, M2]
|
||||
)
|
||||
assert len(middle_page) == 2 # Should include message between first and last pages
|
||||
assert middle_page[0].id == message_ids[1]
|
||||
assert middle_page[1].id == message_ids[2]
|
||||
head_tail_msgs = first_page_ids.union(last_page_ids)
|
||||
assert middle_page[1].id not in head_tail_msgs
|
||||
assert middle_page[0].id in first_page_ids
|
||||
|
||||
# Test descending order for middle page
|
||||
middle_page = server.job_manager.get_job_messages(
|
||||
job_id=default_run.id,
|
||||
actor=default_user,
|
||||
before=last_page[-1].id, # M3
|
||||
after=first_page[0].id, # M0
|
||||
ascending=False, # [M2, M1]
|
||||
)
|
||||
assert len(middle_page) == 2 # Should include message between first and last pages
|
||||
assert middle_page[0].id == message_ids[2]
|
||||
assert middle_page[1].id == message_ids[1]
|
||||
|
||||
# Test getting earliest messages
|
||||
msg_3 = last_page[-1].id
|
||||
earliest_msgs = server.job_manager.get_job_messages(
|
||||
job_id=default_run.id,
|
||||
actor=default_user,
|
||||
ascending=False,
|
||||
before=msg_3, # Get messages after M3 in descending order
|
||||
)
|
||||
assert len(earliest_msgs) == 3 # Should get M2, M1, M0
|
||||
assert all(m.id not in last_page_ids for m in earliest_msgs)
|
||||
assert earliest_msgs[0].created_at > earliest_msgs[1].created_at > earliest_msgs[2].created_at
|
||||
|
||||
# Test getting earliest messages with ascending order
|
||||
earliest_msgs_ascending = server.job_manager.get_job_messages(
|
||||
job_id=default_run.id,
|
||||
actor=default_user,
|
||||
ascending=True,
|
||||
before=msg_3, # Get messages before M3 in ascending order
|
||||
)
|
||||
assert len(earliest_msgs_ascending) == 3 # Should get M0, M1, M2
|
||||
assert all(m.id not in last_page_ids for m in earliest_msgs_ascending)
|
||||
assert earliest_msgs_ascending[0].created_at < earliest_msgs_ascending[1].created_at < earliest_msgs_ascending[2].created_at
|
||||
|
||||
|
||||
def test_job_messages_ordering(server: SyncServer, default_run, default_user, sarah_agent):
|
||||
@ -2800,7 +3000,7 @@ def test_job_messages_filter(server: SyncServer, default_run, default_user, sara
|
||||
assert len(limited_messages) == 2
|
||||
|
||||
|
||||
def test_get_run_messages_cursor(server: SyncServer, default_user: PydanticUser, sarah_agent):
|
||||
def test_get_run_messages(server: SyncServer, default_user: PydanticUser, sarah_agent):
|
||||
"""Test getting messages for a run with request config."""
|
||||
# Create a run with custom request config
|
||||
run = server.job_manager.create_job(
|
||||
@ -2835,7 +3035,7 @@ def test_get_run_messages_cursor(server: SyncServer, default_user: PydanticUser,
|
||||
server.job_manager.add_message_to_job(job_id=run.id, message_id=created_msg.id, actor=default_user)
|
||||
|
||||
# Get messages and verify they're converted correctly
|
||||
result = server.job_manager.get_run_messages_cursor(run_id=run.id, actor=default_user)
|
||||
result = server.job_manager.get_run_messages(run_id=run.id, actor=default_user)
|
||||
|
||||
# Verify correct number of messages. Assistant messages should be parsed
|
||||
assert len(result) == 6
|
||||
@ -2995,7 +3195,7 @@ def test_list_tags(server: SyncServer, default_user, default_organization):
|
||||
assert limited_tags == tags[:2] # Should return first 2 tags
|
||||
|
||||
# Test pagination with cursor
|
||||
cursor_tags = server.agent_manager.list_tags(actor=default_user, cursor="beta")
|
||||
cursor_tags = server.agent_manager.list_tags(actor=default_user, after="beta")
|
||||
assert cursor_tags == ["delta", "epsilon", "gamma"] # Tags after "beta"
|
||||
|
||||
# Test text search
|
||||
|
@ -96,7 +96,7 @@ def test_shared_blocks(client):
|
||||
messages=[
|
||||
MessageCreate(
|
||||
role="user",
|
||||
text="my name is actually charles",
|
||||
content="my name is actually charles",
|
||||
)
|
||||
],
|
||||
)
|
||||
@ -109,7 +109,7 @@ def test_shared_blocks(client):
|
||||
messages=[
|
||||
MessageCreate(
|
||||
role="user",
|
||||
text="whats my name?",
|
||||
content="whats my name?",
|
||||
)
|
||||
],
|
||||
)
|
||||
@ -338,7 +338,7 @@ def test_messages(client, agent):
|
||||
messages=[
|
||||
MessageCreate(
|
||||
role="user",
|
||||
text="Test message",
|
||||
content="Test message",
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -358,7 +358,7 @@ def test_send_system_message(client, agent):
|
||||
messages=[
|
||||
MessageCreate(
|
||||
role="system",
|
||||
text="Event occurred: The user just logged off.",
|
||||
content="Event occurred: The user just logged off.",
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -387,7 +387,7 @@ def test_function_return_limit(client, agent):
|
||||
messages=[
|
||||
MessageCreate(
|
||||
role="user",
|
||||
text="call the big_return function",
|
||||
content="call the big_return function",
|
||||
),
|
||||
],
|
||||
config=LettaRequestConfig(use_assistant_message=False),
|
||||
@ -423,7 +423,7 @@ def test_function_always_error(client, agent):
|
||||
messages=[
|
||||
MessageCreate(
|
||||
role="user",
|
||||
text="call the always_error function",
|
||||
content="call the always_error function",
|
||||
),
|
||||
],
|
||||
config=LettaRequestConfig(use_assistant_message=False),
|
||||
@ -454,7 +454,7 @@ async def test_send_message_parallel(client, agent):
|
||||
messages=[
|
||||
MessageCreate(
|
||||
role="user",
|
||||
text=message,
|
||||
content=message,
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -489,7 +489,7 @@ def test_send_message_async(client, agent):
|
||||
messages=[
|
||||
MessageCreate(
|
||||
role="user",
|
||||
text=test_message,
|
||||
content=test_message,
|
||||
),
|
||||
],
|
||||
config=LettaRequestConfig(use_assistant_message=False),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
import warnings
|
||||
from typing import List, Tuple
|
||||
@ -13,6 +14,7 @@ from letta.orm import Provider, Step
|
||||
from letta.schemas.block import CreateBlock
|
||||
from letta.schemas.enums import MessageRole
|
||||
from letta.schemas.letta_message import LettaMessage, ReasoningMessage, SystemMessage, ToolCallMessage, ToolReturnMessage, UserMessage
|
||||
from letta.schemas.llm_config import LLMConfig
|
||||
from letta.schemas.providers import Provider as PydanticProvider
|
||||
from letta.schemas.user import User
|
||||
|
||||
@ -330,7 +332,7 @@ def agent_id(server, user_id, base_tools):
|
||||
name="test_agent",
|
||||
tool_ids=[t.id for t in base_tools],
|
||||
memory_blocks=[],
|
||||
model="openai/gpt-4",
|
||||
model="openai/gpt-4o",
|
||||
embedding="openai/text-embedding-ada-002",
|
||||
),
|
||||
actor=actor,
|
||||
@ -351,7 +353,7 @@ def other_agent_id(server, user_id, base_tools):
|
||||
name="test_agent_other",
|
||||
tool_ids=[t.id for t in base_tools],
|
||||
memory_blocks=[],
|
||||
model="openai/gpt-4",
|
||||
model="openai/gpt-4o",
|
||||
embedding="openai/text-embedding-ada-002",
|
||||
),
|
||||
actor=actor,
|
||||
@ -392,7 +394,7 @@ def test_user_message_memory(server, user, agent_id):
|
||||
@pytest.mark.order(3)
|
||||
def test_load_data(server, user, agent_id):
|
||||
# create source
|
||||
passages_before = server.agent_manager.list_passages(actor=user, agent_id=agent_id, cursor=None, limit=10000)
|
||||
passages_before = server.agent_manager.list_passages(actor=user, agent_id=agent_id, after=None, limit=10000)
|
||||
assert len(passages_before) == 0
|
||||
|
||||
source = server.source_manager.create_source(
|
||||
@ -414,7 +416,7 @@ def test_load_data(server, user, agent_id):
|
||||
server.agent_manager.attach_source(agent_id=agent_id, source_id=source.id, actor=user)
|
||||
|
||||
# check archival memory size
|
||||
passages_after = server.agent_manager.list_passages(actor=user, agent_id=agent_id, cursor=None, limit=10000)
|
||||
passages_after = server.agent_manager.list_passages(actor=user, agent_id=agent_id, after=None, limit=10000)
|
||||
assert len(passages_after) == 5
|
||||
|
||||
|
||||
@ -426,25 +428,25 @@ def test_save_archival_memory(server, user_id, agent_id):
|
||||
@pytest.mark.order(4)
|
||||
def test_user_message(server, user, agent_id):
|
||||
# add data into recall memory
|
||||
server.user_message(user_id=user.id, agent_id=agent_id, message="Hello?")
|
||||
# server.user_message(user_id=user_id, agent_id=agent_id, message="Hello?")
|
||||
# server.user_message(user_id=user_id, agent_id=agent_id, message="Hello?")
|
||||
# server.user_message(user_id=user_id, agent_id=agent_id, message="Hello?")
|
||||
# server.user_message(user_id=user_id, agent_id=agent_id, message="Hello?")
|
||||
response = server.user_message(user_id=user.id, agent_id=agent_id, message="What's up?")
|
||||
assert response.step_count == 1
|
||||
assert response.completion_tokens > 0
|
||||
assert response.prompt_tokens > 0
|
||||
assert response.total_tokens > 0
|
||||
|
||||
|
||||
@pytest.mark.order(5)
|
||||
def test_get_recall_memory(server, org_id, user, agent_id):
|
||||
# test recall memory cursor pagination
|
||||
actor = user
|
||||
messages_1 = server.get_agent_recall_cursor(user_id=user.id, agent_id=agent_id, limit=2)
|
||||
messages_1 = server.get_agent_recall(user_id=user.id, agent_id=agent_id, limit=2)
|
||||
cursor1 = messages_1[-1].id
|
||||
messages_2 = server.get_agent_recall_cursor(user_id=user.id, agent_id=agent_id, after=cursor1, limit=1000)
|
||||
messages_3 = server.get_agent_recall_cursor(user_id=user.id, agent_id=agent_id, limit=1000)
|
||||
messages_2 = server.get_agent_recall(user_id=user.id, agent_id=agent_id, after=cursor1, limit=1000)
|
||||
messages_3 = server.get_agent_recall(user_id=user.id, agent_id=agent_id, limit=1000)
|
||||
messages_3[-1].id
|
||||
assert messages_3[-1].created_at >= messages_3[0].created_at
|
||||
assert len(messages_3) == len(messages_1) + len(messages_2)
|
||||
messages_4 = server.get_agent_recall_cursor(user_id=user.id, agent_id=agent_id, reverse=True, before=cursor1)
|
||||
messages_4 = server.get_agent_recall(user_id=user.id, agent_id=agent_id, reverse=True, before=cursor1)
|
||||
assert len(messages_4) == 1
|
||||
|
||||
# test in-context message ids
|
||||
@ -475,7 +477,7 @@ def test_get_archival_memory(server, user, agent_id):
|
||||
actor=actor,
|
||||
agent_id=agent_id,
|
||||
ascending=False,
|
||||
cursor=cursor1,
|
||||
before=cursor1,
|
||||
)
|
||||
|
||||
# List all 5
|
||||
@ -497,11 +499,11 @@ def test_get_archival_memory(server, user, agent_id):
|
||||
passage_1 = server.agent_manager.list_passages(actor=actor, agent_id=agent_id, limit=1, ascending=True)
|
||||
assert len(passage_1) == 1
|
||||
assert passage_1[0].text == "alpha"
|
||||
passage_2 = server.agent_manager.list_passages(actor=actor, agent_id=agent_id, cursor=earliest.id, limit=1000, ascending=True)
|
||||
passage_2 = server.agent_manager.list_passages(actor=actor, agent_id=agent_id, after=earliest.id, limit=1000, ascending=True)
|
||||
assert len(passage_2) in [4, 5] # NOTE: exact size seems non-deterministic, so loosen test
|
||||
assert all("alpha" not in passage.text for passage in passage_2)
|
||||
# test safe empty return
|
||||
passage_none = server.agent_manager.list_passages(actor=actor, agent_id=agent_id, cursor=latest.id, limit=1000, ascending=True)
|
||||
passage_none = server.agent_manager.list_passages(actor=actor, agent_id=agent_id, after=latest.id, limit=1000, ascending=True)
|
||||
assert len(passage_none) == 0
|
||||
|
||||
|
||||
@ -550,7 +552,7 @@ def test_delete_agent_same_org(server: SyncServer, org_id: str, user: User):
|
||||
request=CreateAgent(
|
||||
name="nonexistent_tools_agent",
|
||||
memory_blocks=[],
|
||||
model="openai/gpt-4",
|
||||
model="openai/gpt-4o",
|
||||
embedding="openai/text-embedding-ada-002",
|
||||
),
|
||||
actor=user,
|
||||
@ -563,6 +565,63 @@ def test_delete_agent_same_org(server: SyncServer, org_id: str, user: User):
|
||||
server.agent_manager.delete_agent(agent_state.id, actor=another_user)
|
||||
|
||||
|
||||
def test_read_local_llm_configs(server: SyncServer, user: User):
|
||||
configs_base_dir = os.path.join(os.path.expanduser("~"), ".letta", "llm_configs")
|
||||
clean_up_dir = False
|
||||
if not os.path.exists(configs_base_dir):
|
||||
os.makedirs(configs_base_dir)
|
||||
clean_up_dir = True
|
||||
|
||||
try:
|
||||
sample_config = LLMConfig(
|
||||
model="my-custom-model",
|
||||
model_endpoint_type="openai",
|
||||
model_endpoint="https://api.openai.com/v1",
|
||||
context_window=8192,
|
||||
handle="caren/my-custom-model",
|
||||
)
|
||||
|
||||
config_filename = f"custom_llm_config_{uuid.uuid4().hex}.json"
|
||||
config_filepath = os.path.join(configs_base_dir, config_filename)
|
||||
with open(config_filepath, "w") as f:
|
||||
json.dump(sample_config.model_dump(), f)
|
||||
|
||||
# Call list_llm_models
|
||||
assert os.path.exists(configs_base_dir)
|
||||
llm_models = server.list_llm_models()
|
||||
|
||||
# Assert that the config is in the returned models
|
||||
assert any(
|
||||
model.model == "my-custom-model"
|
||||
and model.model_endpoint_type == "openai"
|
||||
and model.model_endpoint == "https://api.openai.com/v1"
|
||||
and model.context_window == 8192
|
||||
and model.handle == "caren/my-custom-model"
|
||||
for model in llm_models
|
||||
), "Custom LLM config not found in list_llm_models result"
|
||||
|
||||
# Try to use in agent creation
|
||||
context_window_override = 4000
|
||||
agent = server.create_agent(
|
||||
request=CreateAgent(
|
||||
model="caren/my-custom-model",
|
||||
context_window_limit=context_window_override,
|
||||
embedding="openai/text-embedding-ada-002",
|
||||
),
|
||||
actor=user,
|
||||
)
|
||||
assert agent.llm_config.model == sample_config.model
|
||||
assert agent.llm_config.model_endpoint == sample_config.model_endpoint
|
||||
assert agent.llm_config.model_endpoint_type == sample_config.model_endpoint_type
|
||||
assert agent.llm_config.context_window == context_window_override
|
||||
assert agent.llm_config.handle == sample_config.handle
|
||||
|
||||
finally:
|
||||
os.remove(config_filepath)
|
||||
if clean_up_dir:
|
||||
shutil.rmtree(configs_base_dir)
|
||||
|
||||
|
||||
def _test_get_messages_letta_format(
|
||||
server,
|
||||
user,
|
||||
@ -571,7 +630,7 @@ def _test_get_messages_letta_format(
|
||||
):
|
||||
"""Test mapping between messages and letta_messages with reverse=False."""
|
||||
|
||||
messages = server.get_agent_recall_cursor(
|
||||
messages = server.get_agent_recall(
|
||||
user_id=user.id,
|
||||
agent_id=agent_id,
|
||||
limit=1000,
|
||||
@ -580,7 +639,7 @@ def _test_get_messages_letta_format(
|
||||
)
|
||||
assert all(isinstance(m, Message) for m in messages)
|
||||
|
||||
letta_messages = server.get_agent_recall_cursor(
|
||||
letta_messages = server.get_agent_recall(
|
||||
user_id=user.id,
|
||||
agent_id=agent_id,
|
||||
limit=1000,
|
||||
@ -652,12 +711,12 @@ def _test_get_messages_letta_format(
|
||||
|
||||
elif message.role == MessageRole.user:
|
||||
assert isinstance(letta_message, UserMessage)
|
||||
assert message.text == letta_message.message
|
||||
assert message.text == letta_message.content
|
||||
letta_message_index += 1
|
||||
|
||||
elif message.role == MessageRole.system:
|
||||
assert isinstance(letta_message, SystemMessage)
|
||||
assert message.text == letta_message.message
|
||||
assert message.text == letta_message.content
|
||||
letta_message_index += 1
|
||||
|
||||
elif message.role == MessageRole.tool:
|
||||
@ -861,7 +920,7 @@ def test_memory_rebuild_count(server, user, mock_e2b_api_key_none, base_tools, b
|
||||
CreateBlock(label="human", value="The human's name is Bob."),
|
||||
CreateBlock(label="persona", value="My name is Alice."),
|
||||
],
|
||||
model="openai/gpt-4",
|
||||
model="openai/gpt-4o",
|
||||
embedding="openai/text-embedding-ada-002",
|
||||
),
|
||||
actor=actor,
|
||||
@ -871,7 +930,7 @@ def test_memory_rebuild_count(server, user, mock_e2b_api_key_none, base_tools, b
|
||||
def count_system_messages_in_recall() -> Tuple[int, List[LettaMessage]]:
|
||||
|
||||
# At this stage, there should only be 1 system message inside of recall storage
|
||||
letta_messages = server.get_agent_recall_cursor(
|
||||
letta_messages = server.get_agent_recall(
|
||||
user_id=user.id,
|
||||
agent_id=agent_state.id,
|
||||
limit=1000,
|
||||
@ -1049,7 +1108,7 @@ def test_add_remove_tools_update_agent(server: SyncServer, user_id: str, base_to
|
||||
CreateBlock(label="human", value="The human's name is Bob."),
|
||||
CreateBlock(label="persona", value="My name is Alice."),
|
||||
],
|
||||
model="openai/gpt-4",
|
||||
model="openai/gpt-4o",
|
||||
embedding="openai/text-embedding-ada-002",
|
||||
include_base_tools=False,
|
||||
),
|
||||
@ -1133,7 +1192,7 @@ def test_messages_with_provider_override(server: SyncServer, user_id: str):
|
||||
usage = server.user_message(user_id=actor.id, agent_id=agent.id, message="Test message")
|
||||
assert usage, "Sending message failed"
|
||||
|
||||
get_messages_response = server.message_manager.list_messages_for_agent(agent_id=agent.id, actor=actor, cursor=existing_messages[-1].id)
|
||||
get_messages_response = server.message_manager.list_messages_for_agent(agent_id=agent.id, actor=actor, after=existing_messages[-1].id)
|
||||
assert len(get_messages_response) > 0, "Retrieving messages failed"
|
||||
|
||||
step_ids = set([msg.step_id for msg in get_messages_response])
|
||||
@ -1160,7 +1219,7 @@ def test_messages_with_provider_override(server: SyncServer, user_id: str):
|
||||
usage = server.user_message(user_id=actor.id, agent_id=agent.id, message="Test message")
|
||||
assert usage, "Sending message failed"
|
||||
|
||||
get_messages_response = server.message_manager.list_messages_for_agent(agent_id=agent.id, actor=actor, cursor=existing_messages[-1].id)
|
||||
get_messages_response = server.message_manager.list_messages_for_agent(agent_id=agent.id, actor=actor, after=existing_messages[-1].id)
|
||||
assert len(get_messages_response) > 0, "Retrieving messages failed"
|
||||
|
||||
step_ids = set([msg.step_id for msg in get_messages_response])
|
||||
@ -1179,3 +1238,12 @@ def test_messages_with_provider_override(server: SyncServer, user_id: str):
|
||||
assert completion_tokens == usage.completion_tokens
|
||||
assert prompt_tokens == usage.prompt_tokens
|
||||
assert total_tokens == usage.total_tokens
|
||||
|
||||
|
||||
def test_unique_handles_for_provider_configs(server: SyncServer):
|
||||
models = server.list_llm_models()
|
||||
model_handles = [model.handle for model in models]
|
||||
assert sorted(model_handles) == sorted(list(set(model_handles))), "All models should have unique handles"
|
||||
embeddings = server.list_embedding_models()
|
||||
embedding_handles = [embedding.handle for embedding in embeddings]
|
||||
assert sorted(embedding_handles) == sorted(list(set(embedding_handles))), "All embeddings should have unique handles"
|
||||
|
@ -6,6 +6,7 @@ from composio.client.collections import ActionModel, ActionParametersModel, Acti
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from letta.orm.errors import NoResultFound
|
||||
from letta.schemas.block import Block, BlockUpdate, CreateBlock
|
||||
from letta.schemas.message import UserMessage
|
||||
from letta.schemas.tool import ToolCreate, ToolUpdate
|
||||
from letta.server.rest_api.app import app
|
||||
@ -323,14 +324,14 @@ def test_get_run_messages(client, mock_sync_server):
|
||||
UserMessage(
|
||||
id=f"message-{i:08x}",
|
||||
date=current_time,
|
||||
message=f"Test message {i}",
|
||||
content=f"Test message {i}",
|
||||
)
|
||||
for i in range(2)
|
||||
]
|
||||
|
||||
# Configure mock server responses
|
||||
mock_sync_server.user_manager.get_user_or_default.return_value = Mock(id="user-123")
|
||||
mock_sync_server.job_manager.get_run_messages_cursor.return_value = mock_messages
|
||||
mock_sync_server.job_manager.get_run_messages.return_value = mock_messages
|
||||
|
||||
# Test successful retrieval
|
||||
response = client.get(
|
||||
@ -338,9 +339,10 @@ def test_get_run_messages(client, mock_sync_server):
|
||||
headers={"user_id": "user-123"},
|
||||
params={
|
||||
"limit": 10,
|
||||
"cursor": mock_messages[0].id,
|
||||
"before": "message-1234",
|
||||
"after": "message-6789",
|
||||
"role": "user",
|
||||
"ascending": True,
|
||||
"order": "desc",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@ -350,12 +352,13 @@ def test_get_run_messages(client, mock_sync_server):
|
||||
|
||||
# Verify mock calls
|
||||
mock_sync_server.user_manager.get_user_or_default.assert_called_once_with(user_id="user-123")
|
||||
mock_sync_server.job_manager.get_run_messages_cursor.assert_called_once_with(
|
||||
mock_sync_server.job_manager.get_run_messages.assert_called_once_with(
|
||||
run_id="run-12345678",
|
||||
actor=mock_sync_server.user_manager.get_user_or_default.return_value,
|
||||
limit=10,
|
||||
cursor=mock_messages[0].id,
|
||||
ascending=True,
|
||||
before="message-1234",
|
||||
after="message-6789",
|
||||
ascending=False,
|
||||
role="user",
|
||||
)
|
||||
|
||||
@ -365,7 +368,7 @@ def test_get_run_messages_not_found(client, mock_sync_server):
|
||||
# Configure mock responses
|
||||
error_message = "Run 'run-nonexistent' not found"
|
||||
mock_sync_server.user_manager.get_user_or_default.return_value = Mock(id="user-123")
|
||||
mock_sync_server.job_manager.get_run_messages_cursor.side_effect = NoResultFound(error_message)
|
||||
mock_sync_server.job_manager.get_run_messages.side_effect = NoResultFound(error_message)
|
||||
|
||||
response = client.get("/v1/runs/run-nonexistent/messages", headers={"user_id": "user-123"})
|
||||
|
||||
@ -431,7 +434,7 @@ def test_get_tags(client, mock_sync_server):
|
||||
assert response.status_code == 200
|
||||
assert response.json() == ["tag1", "tag2"]
|
||||
mock_sync_server.agent_manager.list_tags.assert_called_once_with(
|
||||
actor=mock_sync_server.user_manager.get_user_or_default.return_value, cursor=None, limit=50, query_text=None
|
||||
actor=mock_sync_server.user_manager.get_user_or_default.return_value, after=None, limit=50, query_text=None
|
||||
)
|
||||
|
||||
|
||||
@ -439,12 +442,12 @@ def test_get_tags_with_pagination(client, mock_sync_server):
|
||||
"""Test tag listing with pagination parameters"""
|
||||
mock_sync_server.agent_manager.list_tags.return_value = ["tag3", "tag4"]
|
||||
|
||||
response = client.get("/v1/tags", params={"cursor": "tag2", "limit": 2}, headers={"user_id": "test_user"})
|
||||
response = client.get("/v1/tags", params={"after": "tag2", "limit": 2}, headers={"user_id": "test_user"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == ["tag3", "tag4"]
|
||||
mock_sync_server.agent_manager.list_tags.assert_called_once_with(
|
||||
actor=mock_sync_server.user_manager.get_user_or_default.return_value, cursor="tag2", limit=2, query_text=None
|
||||
actor=mock_sync_server.user_manager.get_user_or_default.return_value, after="tag2", limit=2, query_text=None
|
||||
)
|
||||
|
||||
|
||||
@ -457,5 +460,134 @@ def test_get_tags_with_search(client, mock_sync_server):
|
||||
assert response.status_code == 200
|
||||
assert response.json() == ["user_tag1", "user_tag2"]
|
||||
mock_sync_server.agent_manager.list_tags.assert_called_once_with(
|
||||
actor=mock_sync_server.user_manager.get_user_or_default.return_value, cursor=None, limit=50, query_text="user"
|
||||
actor=mock_sync_server.user_manager.get_user_or_default.return_value, after=None, limit=50, query_text="user"
|
||||
)
|
||||
|
||||
|
||||
# ======================================================================================================================
|
||||
# Blocks Routes Tests
|
||||
# ======================================================================================================================
|
||||
|
||||
|
||||
def test_list_blocks(client, mock_sync_server):
|
||||
"""
|
||||
Test the GET /v1/blocks endpoint to list blocks.
|
||||
"""
|
||||
# Arrange: mock return from block_manager
|
||||
mock_block = Block(label="human", value="Hi", is_template=True)
|
||||
mock_sync_server.block_manager.get_blocks.return_value = [mock_block]
|
||||
|
||||
# Act
|
||||
response = client.get("/v1/blocks", headers={"user_id": "test_user"})
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 1
|
||||
assert data[0]["id"] == mock_block.id
|
||||
mock_sync_server.block_manager.get_blocks.assert_called_once_with(
|
||||
actor=mock_sync_server.user_manager.get_user_or_default.return_value,
|
||||
label=None,
|
||||
is_template=True,
|
||||
template_name=None,
|
||||
)
|
||||
|
||||
|
||||
def test_create_block(client, mock_sync_server):
|
||||
"""
|
||||
Test the POST /v1/blocks endpoint to create a block.
|
||||
"""
|
||||
new_block = CreateBlock(label="system", value="Some system text")
|
||||
returned_block = Block(**new_block.model_dump())
|
||||
|
||||
mock_sync_server.block_manager.create_or_update_block.return_value = returned_block
|
||||
|
||||
response = client.post("/v1/blocks", json=new_block.model_dump(), headers={"user_id": "test_user"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == returned_block.id
|
||||
|
||||
mock_sync_server.block_manager.create_or_update_block.assert_called_once()
|
||||
|
||||
|
||||
def test_modify_block(client, mock_sync_server):
|
||||
"""
|
||||
Test the PATCH /v1/blocks/{block_id} endpoint to update a block.
|
||||
"""
|
||||
block_update = BlockUpdate(value="Updated text", description="New description")
|
||||
updated_block = Block(label="human", value="Updated text", description="New description")
|
||||
mock_sync_server.block_manager.update_block.return_value = updated_block
|
||||
|
||||
response = client.patch(f"/v1/blocks/{updated_block.id}", json=block_update.model_dump(), headers={"user_id": "test_user"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["value"] == "Updated text"
|
||||
assert data["description"] == "New description"
|
||||
|
||||
mock_sync_server.block_manager.update_block.assert_called_once_with(
|
||||
block_id=updated_block.id,
|
||||
block_update=block_update,
|
||||
actor=mock_sync_server.user_manager.get_user_or_default.return_value,
|
||||
)
|
||||
|
||||
|
||||
def test_delete_block(client, mock_sync_server):
|
||||
"""
|
||||
Test the DELETE /v1/blocks/{block_id} endpoint.
|
||||
"""
|
||||
deleted_block = Block(label="persona", value="Deleted text")
|
||||
mock_sync_server.block_manager.delete_block.return_value = deleted_block
|
||||
|
||||
response = client.delete(f"/v1/blocks/{deleted_block.id}", headers={"user_id": "test_user"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == deleted_block.id
|
||||
|
||||
mock_sync_server.block_manager.delete_block.assert_called_once_with(
|
||||
block_id=deleted_block.id, actor=mock_sync_server.user_manager.get_user_or_default.return_value
|
||||
)
|
||||
|
||||
|
||||
def test_retrieve_block(client, mock_sync_server):
|
||||
"""
|
||||
Test the GET /v1/blocks/{block_id} endpoint.
|
||||
"""
|
||||
existing_block = Block(label="human", value="Hello")
|
||||
mock_sync_server.block_manager.get_block_by_id.return_value = existing_block
|
||||
|
||||
response = client.get(f"/v1/blocks/{existing_block.id}", headers={"user_id": "test_user"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == existing_block.id
|
||||
|
||||
mock_sync_server.block_manager.get_block_by_id.assert_called_once_with(
|
||||
block_id=existing_block.id, actor=mock_sync_server.user_manager.get_user_or_default.return_value
|
||||
)
|
||||
|
||||
|
||||
def test_retrieve_block_404(client, mock_sync_server):
|
||||
"""
|
||||
Test that retrieving a non-existent block returns 404.
|
||||
"""
|
||||
mock_sync_server.block_manager.get_block_by_id.return_value = None
|
||||
|
||||
response = client.get("/v1/blocks/block-999", headers={"user_id": "test_user"})
|
||||
assert response.status_code == 404
|
||||
assert "Block not found" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_list_agents_for_block(client, mock_sync_server):
|
||||
"""
|
||||
Test the GET /v1/blocks/{block_id}/agents endpoint.
|
||||
"""
|
||||
mock_sync_server.block_manager.get_agents_for_block.return_value = []
|
||||
|
||||
response = client.get("/v1/blocks/block-abc/agents", headers={"user_id": "test_user"})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 0
|
||||
|
||||
mock_sync_server.block_manager.get_agents_for_block.assert_called_once_with(
|
||||
block_id="block-abc",
|
||||
actor=mock_sync_server.user_manager.get_user_or_default.return_value,
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user