letta-nightly 0.5.0.dev20241021104213__py3-none-any.whl → 0.5.0.dev20241023104105__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +7 -2
- letta/agent_store/db.py +4 -2
- letta/cli/cli_config.py +2 -2
- letta/client/client.py +13 -0
- letta/constants.py +4 -1
- letta/embeddings.py +34 -16
- letta/llm_api/azure_openai.py +44 -4
- letta/llm_api/helpers.py +45 -19
- letta/llm_api/openai.py +24 -5
- letta/metadata.py +1 -59
- letta/orm/__all__.py +0 -0
- letta/orm/__init__.py +0 -0
- letta/orm/base.py +75 -0
- letta/orm/enums.py +8 -0
- letta/orm/errors.py +2 -0
- letta/orm/mixins.py +40 -0
- letta/orm/organization.py +35 -0
- letta/orm/sqlalchemy_base.py +214 -0
- letta/schemas/organization.py +3 -3
- letta/server/rest_api/interface.py +245 -98
- letta/server/rest_api/routers/v1/agents.py +11 -3
- letta/server/rest_api/routers/v1/organizations.py +4 -5
- letta/server/server.py +10 -25
- letta/services/__init__.py +0 -0
- letta/services/organization_manager.py +66 -0
- letta/streaming_utils.py +270 -0
- {letta_nightly-0.5.0.dev20241021104213.dist-info → letta_nightly-0.5.0.dev20241023104105.dist-info}/METADATA +2 -1
- {letta_nightly-0.5.0.dev20241021104213.dist-info → letta_nightly-0.5.0.dev20241023104105.dist-info}/RECORD +31 -22
- letta/base.py +0 -3
- letta/client/admin.py +0 -171
- {letta_nightly-0.5.0.dev20241021104213.dist-info → letta_nightly-0.5.0.dev20241023104105.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.0.dev20241021104213.dist-info → letta_nightly-0.5.0.dev20241023104105.dist-info}/WHEEL +0 -0
- {letta_nightly-0.5.0.dev20241021104213.dist-info → letta_nightly-0.5.0.dev20241023104105.dist-info}/entry_points.txt +0 -0
letta/__init__.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
__version__ = "0.5.0"
|
|
2
2
|
|
|
3
3
|
# import clients
|
|
4
|
-
from letta.client.admin import Admin
|
|
5
4
|
from letta.client.client import LocalClient, RESTClient, create_client
|
|
6
5
|
|
|
7
6
|
# imports for easier access
|
|
@@ -13,7 +12,13 @@ from letta.schemas.file import FileMetadata
|
|
|
13
12
|
from letta.schemas.job import Job
|
|
14
13
|
from letta.schemas.letta_message import LettaMessage
|
|
15
14
|
from letta.schemas.llm_config import LLMConfig
|
|
16
|
-
from letta.schemas.memory import
|
|
15
|
+
from letta.schemas.memory import (
|
|
16
|
+
ArchivalMemorySummary,
|
|
17
|
+
BasicBlockMemory,
|
|
18
|
+
ChatMemory,
|
|
19
|
+
Memory,
|
|
20
|
+
RecallMemorySummary,
|
|
21
|
+
)
|
|
17
22
|
from letta.schemas.message import Message
|
|
18
23
|
from letta.schemas.openai.chat_completion_response import UsageStatistics
|
|
19
24
|
from letta.schemas.organization import Organization
|
letta/agent_store/db.py
CHANGED
|
@@ -25,10 +25,10 @@ from sqlalchemy_json import MutableJson
|
|
|
25
25
|
from tqdm import tqdm
|
|
26
26
|
|
|
27
27
|
from letta.agent_store.storage import StorageConnector, TableType
|
|
28
|
-
from letta.base import Base
|
|
29
28
|
from letta.config import LettaConfig
|
|
30
29
|
from letta.constants import MAX_EMBEDDING_DIM
|
|
31
30
|
from letta.metadata import EmbeddingConfigColumn, FileMetadataModel, ToolCallColumn
|
|
31
|
+
from letta.orm.base import Base
|
|
32
32
|
|
|
33
33
|
# from letta.schemas.message import Message, Passage, Record, RecordType, ToolCall
|
|
34
34
|
from letta.schemas.message import Message
|
|
@@ -509,8 +509,10 @@ class SQLLiteStorageConnector(SQLStorageConnector):
|
|
|
509
509
|
|
|
510
510
|
self.session_maker = db_context
|
|
511
511
|
|
|
512
|
+
# Need this in order to allow UUIDs to be stored successfully in the sqlite database
|
|
512
513
|
# import sqlite3
|
|
513
|
-
|
|
514
|
+
# import uuid
|
|
515
|
+
#
|
|
514
516
|
# sqlite3.register_adapter(uuid.UUID, lambda u: u.bytes_le)
|
|
515
517
|
# sqlite3.register_converter("UUID", lambda b: uuid.UUID(bytes_le=b))
|
|
516
518
|
|
letta/cli/cli_config.py
CHANGED
|
@@ -105,7 +105,7 @@ def add_tool(
|
|
|
105
105
|
"""Add or update a tool from a Python file."""
|
|
106
106
|
from letta.client.client import create_client
|
|
107
107
|
|
|
108
|
-
client = create_client(
|
|
108
|
+
client = create_client()
|
|
109
109
|
|
|
110
110
|
# 1. Parse the Python file
|
|
111
111
|
with open(filename, "r", encoding="utf-8") as file:
|
|
@@ -145,7 +145,7 @@ def list_tools():
|
|
|
145
145
|
"""List all available tools."""
|
|
146
146
|
from letta.client.client import create_client
|
|
147
147
|
|
|
148
|
-
client = create_client(
|
|
148
|
+
client = create_client()
|
|
149
149
|
|
|
150
150
|
tools = client.list_tools()
|
|
151
151
|
for tool in tools:
|
letta/client/client.py
CHANGED
|
@@ -1777,6 +1777,19 @@ class LocalClient(AbstractClient):
|
|
|
1777
1777
|
"""
|
|
1778
1778
|
self.server.delete_agent(user_id=self.user_id, agent_id=agent_id)
|
|
1779
1779
|
|
|
1780
|
+
def get_agent_by_name(self, agent_name: str, user_id: str) -> AgentState:
|
|
1781
|
+
"""
|
|
1782
|
+
Get an agent by its name
|
|
1783
|
+
|
|
1784
|
+
Args:
|
|
1785
|
+
agent_name (str): Name of the agent
|
|
1786
|
+
|
|
1787
|
+
Returns:
|
|
1788
|
+
agent_state (AgentState): State of the agent
|
|
1789
|
+
"""
|
|
1790
|
+
self.interface.clear()
|
|
1791
|
+
return self.server.get_agent(agent_name=agent_name, user_id=user_id, agent_id=None)
|
|
1792
|
+
|
|
1780
1793
|
def get_agent(self, agent_id: str) -> AgentState:
|
|
1781
1794
|
"""
|
|
1782
1795
|
Get an agent's state by its ID.
|
letta/constants.py
CHANGED
|
@@ -5,7 +5,10 @@ LETTA_DIR = os.path.join(os.path.expanduser("~"), ".letta")
|
|
|
5
5
|
|
|
6
6
|
# Defaults
|
|
7
7
|
DEFAULT_USER_ID = "user-00000000"
|
|
8
|
-
|
|
8
|
+
# This UUID follows the UUID4 rules:
|
|
9
|
+
# The 13th character (4) indicates it's version 4.
|
|
10
|
+
# The first character of the third segment (8) ensures the variant is correctly set.
|
|
11
|
+
DEFAULT_ORG_ID = "organization-00000000-0000-4000-8000-000000000000"
|
|
9
12
|
DEFAULT_USER_NAME = "default"
|
|
10
13
|
DEFAULT_ORG_NAME = "default"
|
|
11
14
|
|
letta/embeddings.py
CHANGED
|
@@ -21,7 +21,6 @@ from letta.constants import (
|
|
|
21
21
|
EMBEDDING_TO_TOKENIZER_MAP,
|
|
22
22
|
MAX_EMBEDDING_DIM,
|
|
23
23
|
)
|
|
24
|
-
from letta.credentials import LettaCredentials
|
|
25
24
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
26
25
|
from letta.utils import is_valid_url, printd
|
|
27
26
|
|
|
@@ -138,6 +137,18 @@ class EmbeddingEndpoint:
|
|
|
138
137
|
return self._call_api(text)
|
|
139
138
|
|
|
140
139
|
|
|
140
|
+
class AzureOpenAIEmbedding:
|
|
141
|
+
def __init__(self, api_endpoint: str, api_key: str, api_version: str, model: str):
|
|
142
|
+
from openai import AzureOpenAI
|
|
143
|
+
|
|
144
|
+
self.client = AzureOpenAI(api_key=api_key, api_version=api_version, azure_endpoint=api_endpoint)
|
|
145
|
+
self.model = model
|
|
146
|
+
|
|
147
|
+
def get_text_embedding(self, text: str):
|
|
148
|
+
embeddings = self.client.embeddings.create(input=[text], model=self.model).data[0].embedding
|
|
149
|
+
return embeddings
|
|
150
|
+
|
|
151
|
+
|
|
141
152
|
def default_embedding_model():
|
|
142
153
|
# default to hugging face model running local
|
|
143
154
|
# warning: this is a terrible model
|
|
@@ -161,8 +172,8 @@ def embedding_model(config: EmbeddingConfig, user_id: Optional[uuid.UUID] = None
|
|
|
161
172
|
|
|
162
173
|
endpoint_type = config.embedding_endpoint_type
|
|
163
174
|
|
|
164
|
-
# TODO refactor to pass
|
|
165
|
-
|
|
175
|
+
# TODO: refactor to pass in settings from server
|
|
176
|
+
from letta.settings import model_settings
|
|
166
177
|
|
|
167
178
|
if endpoint_type == "openai":
|
|
168
179
|
from llama_index.embeddings.openai import OpenAIEmbedding
|
|
@@ -170,7 +181,7 @@ def embedding_model(config: EmbeddingConfig, user_id: Optional[uuid.UUID] = None
|
|
|
170
181
|
additional_kwargs = {"user_id": user_id} if user_id else {}
|
|
171
182
|
model = OpenAIEmbedding(
|
|
172
183
|
api_base=config.embedding_endpoint,
|
|
173
|
-
api_key=
|
|
184
|
+
api_key=model_settings.openai_api_key,
|
|
174
185
|
additional_kwargs=additional_kwargs,
|
|
175
186
|
)
|
|
176
187
|
return model
|
|
@@ -178,22 +189,29 @@ def embedding_model(config: EmbeddingConfig, user_id: Optional[uuid.UUID] = None
|
|
|
178
189
|
elif endpoint_type == "azure":
|
|
179
190
|
assert all(
|
|
180
191
|
[
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
192
|
+
model_settings.azure_api_key is not None,
|
|
193
|
+
model_settings.azure_base_url is not None,
|
|
194
|
+
model_settings.azure_api_version is not None,
|
|
184
195
|
]
|
|
185
196
|
)
|
|
186
|
-
from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
|
|
197
|
+
# from llama_index.embeddings.azure_openai import AzureOpenAIEmbedding
|
|
198
|
+
|
|
199
|
+
## https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#embeddings
|
|
200
|
+
# model = "text-embedding-ada-002"
|
|
201
|
+
# deployment = credentials.azure_embedding_deployment if credentials.azure_embedding_deployment is not None else model
|
|
202
|
+
# return AzureOpenAIEmbedding(
|
|
203
|
+
# model=model,
|
|
204
|
+
# deployment_name=deployment,
|
|
205
|
+
# api_key=credentials.azure_key,
|
|
206
|
+
# azure_endpoint=credentials.azure_endpoint,
|
|
207
|
+
# api_version=credentials.azure_version,
|
|
208
|
+
# )
|
|
187
209
|
|
|
188
|
-
# https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#embeddings
|
|
189
|
-
model = "text-embedding-ada-002"
|
|
190
|
-
deployment = credentials.azure_embedding_deployment if credentials.azure_embedding_deployment is not None else model
|
|
191
210
|
return AzureOpenAIEmbedding(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
api_version=credentials.azure_version,
|
|
211
|
+
api_endpoint=model_settings.azure_base_url,
|
|
212
|
+
api_key=model_settings.azure_api_key,
|
|
213
|
+
api_version=model_settings.azure_api_version,
|
|
214
|
+
model=config.embedding_model,
|
|
197
215
|
)
|
|
198
216
|
|
|
199
217
|
elif endpoint_type == "hugging-face":
|
letta/llm_api/azure_openai.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
1
3
|
import requests
|
|
2
4
|
|
|
3
5
|
from letta.llm_api.helpers import make_post_request
|
|
@@ -20,7 +22,14 @@ def get_azure_model_list_endpoint(base_url: str, api_version: str):
|
|
|
20
22
|
return f"{base_url}/openai/models?api-version={api_version}"
|
|
21
23
|
|
|
22
24
|
|
|
23
|
-
def
|
|
25
|
+
def get_azure_deployment_list_endpoint(base_url: str):
|
|
26
|
+
# Please note that it has to be 2023-03-15-preview
|
|
27
|
+
# That's the only api version that works with this deployments endpoint
|
|
28
|
+
# TODO: Use the Azure Client library here instead
|
|
29
|
+
return f"{base_url}/openai/deployments?api-version=2023-03-15-preview"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def azure_openai_get_deployed_model_list(base_url: str, api_key: str, api_version: str) -> list:
|
|
24
33
|
"""https://learn.microsoft.com/en-us/rest/api/azureopenai/models/list?view=rest-azureopenai-2023-05-15&tabs=HTTP"""
|
|
25
34
|
|
|
26
35
|
# https://xxx.openai.azure.com/openai/models?api-version=xxx
|
|
@@ -28,18 +37,48 @@ def azure_openai_get_model_list(base_url: str, api_key: str, api_version: str) -
|
|
|
28
37
|
if api_key is not None:
|
|
29
38
|
headers["api-key"] = f"{api_key}"
|
|
30
39
|
|
|
40
|
+
# 1. Get all available models
|
|
31
41
|
url = get_azure_model_list_endpoint(base_url, api_version)
|
|
32
42
|
try:
|
|
33
43
|
response = requests.get(url, headers=headers)
|
|
34
44
|
response.raise_for_status()
|
|
35
45
|
except requests.RequestException as e:
|
|
36
46
|
raise RuntimeError(f"Failed to retrieve model list: {e}")
|
|
47
|
+
all_available_models = response.json().get("data", [])
|
|
37
48
|
|
|
38
|
-
|
|
49
|
+
# 2. Get all the deployed models
|
|
50
|
+
url = get_azure_deployment_list_endpoint(base_url)
|
|
51
|
+
try:
|
|
52
|
+
response = requests.get(url, headers=headers)
|
|
53
|
+
response.raise_for_status()
|
|
54
|
+
except requests.RequestException as e:
|
|
55
|
+
raise RuntimeError(f"Failed to retrieve model list: {e}")
|
|
56
|
+
|
|
57
|
+
deployed_models = response.json().get("data", [])
|
|
58
|
+
deployed_model_names = set([m["id"] for m in deployed_models])
|
|
59
|
+
|
|
60
|
+
# 3. Only return the models in available models if they have been deployed
|
|
61
|
+
deployed_models = [m for m in all_available_models if m["id"] in deployed_model_names]
|
|
62
|
+
|
|
63
|
+
# 4. Remove redundant deployments, only include the ones with the latest deployment
|
|
64
|
+
# Create a dictionary to store the latest model for each ID
|
|
65
|
+
latest_models = defaultdict()
|
|
66
|
+
|
|
67
|
+
# Iterate through the models and update the dictionary with the most recent model
|
|
68
|
+
for model in deployed_models:
|
|
69
|
+
model_id = model["id"]
|
|
70
|
+
updated_at = model["created_at"]
|
|
71
|
+
|
|
72
|
+
# If the model ID is new or the current model has a more recent created_at, update the dictionary
|
|
73
|
+
if model_id not in latest_models or updated_at > latest_models[model_id]["created_at"]:
|
|
74
|
+
latest_models[model_id] = model
|
|
75
|
+
|
|
76
|
+
# Extract the unique models
|
|
77
|
+
return list(latest_models.values())
|
|
39
78
|
|
|
40
79
|
|
|
41
80
|
def azure_openai_get_chat_completion_model_list(base_url: str, api_key: str, api_version: str) -> list:
|
|
42
|
-
model_list =
|
|
81
|
+
model_list = azure_openai_get_deployed_model_list(base_url, api_key, api_version)
|
|
43
82
|
# Extract models that support text generation
|
|
44
83
|
model_options = [m for m in model_list if m.get("capabilities").get("chat_completion") == True]
|
|
45
84
|
return model_options
|
|
@@ -53,10 +92,11 @@ def azure_openai_get_embeddings_model_list(base_url: str, api_key: str, api_vers
|
|
|
53
92
|
|
|
54
93
|
return m.get("capabilities").get("embeddings") == True and valid_name
|
|
55
94
|
|
|
56
|
-
model_list =
|
|
95
|
+
model_list = azure_openai_get_deployed_model_list(base_url, api_key, api_version)
|
|
57
96
|
# Extract models that support embeddings
|
|
58
97
|
|
|
59
98
|
model_options = [m for m in model_list if valid_embedding_model(m)]
|
|
99
|
+
|
|
60
100
|
return model_options
|
|
61
101
|
|
|
62
102
|
|
letta/llm_api/helpers.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import json
|
|
3
3
|
import warnings
|
|
4
|
+
from collections import OrderedDict
|
|
4
5
|
from typing import Any, List, Union
|
|
5
6
|
|
|
6
7
|
import requests
|
|
@@ -10,6 +11,30 @@ from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
|
|
|
10
11
|
from letta.utils import json_dumps, printd
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
def convert_to_structured_output(openai_function: dict) -> dict:
|
|
15
|
+
"""Convert function call objects to structured output objects
|
|
16
|
+
|
|
17
|
+
See: https://platform.openai.com/docs/guides/structured-outputs/supported-schemas
|
|
18
|
+
"""
|
|
19
|
+
structured_output = {
|
|
20
|
+
"name": openai_function["name"],
|
|
21
|
+
"description": openai_function["description"],
|
|
22
|
+
"strict": True,
|
|
23
|
+
"parameters": {"type": "object", "properties": {}, "additionalProperties": False, "required": []},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for param, details in openai_function["parameters"]["properties"].items():
|
|
27
|
+
structured_output["parameters"]["properties"][param] = {"type": details["type"], "description": details["description"]}
|
|
28
|
+
|
|
29
|
+
if "enum" in details:
|
|
30
|
+
structured_output["parameters"]["properties"][param]["enum"] = details["enum"]
|
|
31
|
+
|
|
32
|
+
# Add all properties to required list
|
|
33
|
+
structured_output["parameters"]["required"] = list(structured_output["parameters"]["properties"].keys())
|
|
34
|
+
|
|
35
|
+
return structured_output
|
|
36
|
+
|
|
37
|
+
|
|
13
38
|
def make_post_request(url: str, headers: dict[str, str], data: dict[str, Any]) -> dict[str, Any]:
|
|
14
39
|
printd(f"Sending request to {url}")
|
|
15
40
|
try:
|
|
@@ -78,33 +103,34 @@ def add_inner_thoughts_to_functions(
|
|
|
78
103
|
inner_thoughts_key: str,
|
|
79
104
|
inner_thoughts_description: str,
|
|
80
105
|
inner_thoughts_required: bool = True,
|
|
81
|
-
# inner_thoughts_to_front: bool = True, TODO support sorting somewhere, probably in the to_dict?
|
|
82
106
|
) -> List[dict]:
|
|
83
|
-
"""Add an inner_thoughts kwarg to every function in the provided list"""
|
|
84
|
-
# return copies
|
|
107
|
+
"""Add an inner_thoughts kwarg to every function in the provided list, ensuring it's the first parameter"""
|
|
85
108
|
new_functions = []
|
|
86
|
-
|
|
87
|
-
# functions is a list of dicts in the OpenAI schema (https://platform.openai.com/docs/api-reference/chat/create)
|
|
88
109
|
for function_object in functions:
|
|
89
|
-
|
|
90
|
-
required_params = list(function_object["parameters"]["required"])
|
|
110
|
+
new_function_object = copy.deepcopy(function_object)
|
|
91
111
|
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
# Create a new OrderedDict with inner_thoughts as the first item
|
|
113
|
+
new_properties = OrderedDict()
|
|
114
|
+
new_properties[inner_thoughts_key] = {
|
|
115
|
+
"type": "string",
|
|
116
|
+
"description": inner_thoughts_description,
|
|
117
|
+
}
|
|
98
118
|
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
119
|
+
# Add the rest of the properties
|
|
120
|
+
new_properties.update(function_object["parameters"]["properties"])
|
|
121
|
+
|
|
122
|
+
# Cast OrderedDict back to a regular dict
|
|
123
|
+
new_function_object["parameters"]["properties"] = dict(new_properties)
|
|
124
|
+
|
|
125
|
+
# Update required parameters if necessary
|
|
126
|
+
if inner_thoughts_required:
|
|
127
|
+
required_params = new_function_object["parameters"].get("required", [])
|
|
128
|
+
if inner_thoughts_key not in required_params:
|
|
129
|
+
required_params.insert(0, inner_thoughts_key)
|
|
130
|
+
new_function_object["parameters"]["required"] = required_params
|
|
104
131
|
|
|
105
132
|
new_functions.append(new_function_object)
|
|
106
133
|
|
|
107
|
-
# return a list of copies
|
|
108
134
|
return new_functions
|
|
109
135
|
|
|
110
136
|
|
letta/llm_api/openai.py
CHANGED
|
@@ -9,7 +9,11 @@ from httpx_sse._exceptions import SSEError
|
|
|
9
9
|
|
|
10
10
|
from letta.constants import OPENAI_CONTEXT_WINDOW_ERROR_SUBSTRING
|
|
11
11
|
from letta.errors import LLMError
|
|
12
|
-
from letta.llm_api.helpers import
|
|
12
|
+
from letta.llm_api.helpers import (
|
|
13
|
+
add_inner_thoughts_to_functions,
|
|
14
|
+
convert_to_structured_output,
|
|
15
|
+
make_post_request,
|
|
16
|
+
)
|
|
13
17
|
from letta.local_llm.constants import (
|
|
14
18
|
INNER_THOUGHTS_KWARG,
|
|
15
19
|
INNER_THOUGHTS_KWARG_DESCRIPTION,
|
|
@@ -112,7 +116,7 @@ def build_openai_chat_completions_request(
|
|
|
112
116
|
use_tool_naming: bool,
|
|
113
117
|
max_tokens: Optional[int],
|
|
114
118
|
) -> ChatCompletionRequest:
|
|
115
|
-
if llm_config.put_inner_thoughts_in_kwargs:
|
|
119
|
+
if functions and llm_config.put_inner_thoughts_in_kwargs:
|
|
116
120
|
functions = add_inner_thoughts_to_functions(
|
|
117
121
|
functions=functions,
|
|
118
122
|
inner_thoughts_key=INNER_THOUGHTS_KWARG,
|
|
@@ -154,8 +158,8 @@ def build_openai_chat_completions_request(
|
|
|
154
158
|
)
|
|
155
159
|
# https://platform.openai.com/docs/guides/text-generation/json-mode
|
|
156
160
|
# only supported by gpt-4o, gpt-4-turbo, or gpt-3.5-turbo
|
|
157
|
-
if "gpt-4o" in llm_config.model or "gpt-4-turbo" in llm_config.model or "gpt-3.5-turbo" in llm_config.model:
|
|
158
|
-
|
|
161
|
+
# if "gpt-4o" in llm_config.model or "gpt-4-turbo" in llm_config.model or "gpt-3.5-turbo" in llm_config.model:
|
|
162
|
+
# data.response_format = {"type": "json_object"}
|
|
159
163
|
|
|
160
164
|
if "inference.memgpt.ai" in llm_config.model_endpoint:
|
|
161
165
|
# override user id for inference.memgpt.ai
|
|
@@ -310,11 +314,17 @@ def openai_chat_completions_process_stream(
|
|
|
310
314
|
for _ in range(len(tool_calls_delta))
|
|
311
315
|
]
|
|
312
316
|
|
|
317
|
+
# There may be many tool calls in a tool calls delta (e.g. parallel tool calls)
|
|
313
318
|
for tool_call_delta in tool_calls_delta:
|
|
314
319
|
if tool_call_delta.id is not None:
|
|
315
320
|
# TODO assert that we're not overwriting?
|
|
316
321
|
# TODO += instead of =?
|
|
317
|
-
|
|
322
|
+
if tool_call_delta.index not in range(len(accum_message.tool_calls)):
|
|
323
|
+
warnings.warn(
|
|
324
|
+
f"Tool call index out of range ({tool_call_delta.index})\ncurrent tool calls: {accum_message.tool_calls}\ncurrent delta: {tool_call_delta}"
|
|
325
|
+
)
|
|
326
|
+
else:
|
|
327
|
+
accum_message.tool_calls[tool_call_delta.index].id = tool_call_delta.id
|
|
318
328
|
if tool_call_delta.function is not None:
|
|
319
329
|
if tool_call_delta.function.name is not None:
|
|
320
330
|
# TODO assert that we're not overwriting?
|
|
@@ -362,6 +372,8 @@ def openai_chat_completions_process_stream(
|
|
|
362
372
|
chat_completion_response.usage.completion_tokens = n_chunks
|
|
363
373
|
chat_completion_response.usage.total_tokens = prompt_tokens + n_chunks
|
|
364
374
|
|
|
375
|
+
assert len(chat_completion_response.choices) > 0, chat_completion_response
|
|
376
|
+
|
|
365
377
|
# printd(chat_completion_response)
|
|
366
378
|
return chat_completion_response
|
|
367
379
|
|
|
@@ -461,6 +473,13 @@ def openai_chat_completions_request_stream(
|
|
|
461
473
|
data.pop("tools")
|
|
462
474
|
data.pop("tool_choice", None) # extra safe, should exist always (default="auto")
|
|
463
475
|
|
|
476
|
+
if "tools" in data:
|
|
477
|
+
for tool in data["tools"]:
|
|
478
|
+
# tool["strict"] = True
|
|
479
|
+
tool["function"] = convert_to_structured_output(tool["function"])
|
|
480
|
+
|
|
481
|
+
# print(f"\n\n\n\nData[tools]: {json.dumps(data['tools'], indent=2)}")
|
|
482
|
+
|
|
464
483
|
printd(f"Sending request to {url}")
|
|
465
484
|
try:
|
|
466
485
|
return _sse_post(url=url, data=data, headers=headers)
|
letta/metadata.py
CHANGED
|
@@ -20,8 +20,8 @@ from sqlalchemy import (
|
|
|
20
20
|
)
|
|
21
21
|
from sqlalchemy.sql import func
|
|
22
22
|
|
|
23
|
-
from letta.base import Base
|
|
24
23
|
from letta.config import LettaConfig
|
|
24
|
+
from letta.orm.base import Base
|
|
25
25
|
from letta.schemas.agent import AgentState
|
|
26
26
|
from letta.schemas.api_key import APIKey
|
|
27
27
|
from letta.schemas.block import Block, Human, Persona
|
|
@@ -34,7 +34,6 @@ from letta.schemas.memory import Memory
|
|
|
34
34
|
|
|
35
35
|
# from letta.schemas.message import Message, Passage, Record, RecordType, ToolCall
|
|
36
36
|
from letta.schemas.openai.chat_completions import ToolCall, ToolCallFunction
|
|
37
|
-
from letta.schemas.organization import Organization
|
|
38
37
|
from letta.schemas.source import Source
|
|
39
38
|
from letta.schemas.tool import Tool
|
|
40
39
|
from letta.schemas.user import User
|
|
@@ -174,21 +173,6 @@ class UserModel(Base):
|
|
|
174
173
|
return User(id=self.id, name=self.name, created_at=self.created_at, org_id=self.org_id)
|
|
175
174
|
|
|
176
175
|
|
|
177
|
-
class OrganizationModel(Base):
|
|
178
|
-
__tablename__ = "organizations"
|
|
179
|
-
__table_args__ = {"extend_existing": True}
|
|
180
|
-
|
|
181
|
-
id = Column(String, primary_key=True)
|
|
182
|
-
name = Column(String, nullable=False)
|
|
183
|
-
created_at = Column(DateTime(timezone=True))
|
|
184
|
-
|
|
185
|
-
def __repr__(self) -> str:
|
|
186
|
-
return f"<Organization(id='{self.id}' name='{self.name}')>"
|
|
187
|
-
|
|
188
|
-
def to_record(self) -> Organization:
|
|
189
|
-
return Organization(id=self.id, name=self.name, created_at=self.created_at)
|
|
190
|
-
|
|
191
|
-
|
|
192
176
|
# TODO: eventually store providers?
|
|
193
177
|
# class Provider(Base):
|
|
194
178
|
# __tablename__ = "providers"
|
|
@@ -551,14 +535,6 @@ class MetadataStore:
|
|
|
551
535
|
session.add(UserModel(**vars(user)))
|
|
552
536
|
session.commit()
|
|
553
537
|
|
|
554
|
-
@enforce_types
|
|
555
|
-
def create_organization(self, organization: Organization):
|
|
556
|
-
with self.session_maker() as session:
|
|
557
|
-
if session.query(OrganizationModel).filter(OrganizationModel.id == organization.id).count() > 0:
|
|
558
|
-
raise ValueError(f"Organization with id {organization.id} already exists")
|
|
559
|
-
session.add(OrganizationModel(**vars(organization)))
|
|
560
|
-
session.commit()
|
|
561
|
-
|
|
562
538
|
@enforce_types
|
|
563
539
|
def create_block(self, block: Block):
|
|
564
540
|
with self.session_maker() as session:
|
|
@@ -698,16 +674,6 @@ class MetadataStore:
|
|
|
698
674
|
|
|
699
675
|
session.commit()
|
|
700
676
|
|
|
701
|
-
@enforce_types
|
|
702
|
-
def delete_organization(self, org_id: str):
|
|
703
|
-
with self.session_maker() as session:
|
|
704
|
-
# delete from organizations table
|
|
705
|
-
session.query(OrganizationModel).filter(OrganizationModel.id == org_id).delete()
|
|
706
|
-
|
|
707
|
-
# TODO: delete associated data
|
|
708
|
-
|
|
709
|
-
session.commit()
|
|
710
|
-
|
|
711
677
|
@enforce_types
|
|
712
678
|
def list_tools(self, cursor: Optional[str] = None, limit: Optional[int] = 50, user_id: Optional[str] = None) -> List[ToolModel]:
|
|
713
679
|
with self.session_maker() as session:
|
|
@@ -762,30 +728,6 @@ class MetadataStore:
|
|
|
762
728
|
assert len(results) == 1, f"Expected 1 result, got {len(results)}"
|
|
763
729
|
return results[0].to_record()
|
|
764
730
|
|
|
765
|
-
@enforce_types
|
|
766
|
-
def get_organization(self, org_id: str) -> Optional[Organization]:
|
|
767
|
-
with self.session_maker() as session:
|
|
768
|
-
results = session.query(OrganizationModel).filter(OrganizationModel.id == org_id).all()
|
|
769
|
-
if len(results) == 0:
|
|
770
|
-
return None
|
|
771
|
-
assert len(results) == 1, f"Expected 1 result, got {len(results)}"
|
|
772
|
-
return results[0].to_record()
|
|
773
|
-
|
|
774
|
-
@enforce_types
|
|
775
|
-
def list_organizations(self, cursor: Optional[str] = None, limit: Optional[int] = 50):
|
|
776
|
-
with self.session_maker() as session:
|
|
777
|
-
query = session.query(OrganizationModel).order_by(desc(OrganizationModel.id))
|
|
778
|
-
if cursor:
|
|
779
|
-
query = query.filter(OrganizationModel.id < cursor)
|
|
780
|
-
results = query.limit(limit).all()
|
|
781
|
-
if not results:
|
|
782
|
-
return None, []
|
|
783
|
-
organization_records = [r.to_record() for r in results]
|
|
784
|
-
next_cursor = organization_records[-1].id
|
|
785
|
-
assert isinstance(next_cursor, str)
|
|
786
|
-
|
|
787
|
-
return next_cursor, organization_records
|
|
788
|
-
|
|
789
731
|
@enforce_types
|
|
790
732
|
def get_all_users(self, cursor: Optional[str] = None, limit: Optional[int] = 50):
|
|
791
733
|
with self.session_maker() as session:
|
letta/orm/__all__.py
ADDED
|
File without changes
|
letta/orm/__init__.py
ADDED
|
File without changes
|
letta/orm/base.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import UUID as SQLUUID
|
|
6
|
+
from sqlalchemy import Boolean, DateTime, func, text
|
|
7
|
+
from sqlalchemy.orm import (
|
|
8
|
+
DeclarativeBase,
|
|
9
|
+
Mapped,
|
|
10
|
+
declarative_mixin,
|
|
11
|
+
declared_attr,
|
|
12
|
+
mapped_column,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Base(DeclarativeBase):
|
|
17
|
+
"""absolute base for sqlalchemy classes"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@declarative_mixin
|
|
21
|
+
class CommonSqlalchemyMetaMixins(Base):
|
|
22
|
+
__abstract__ = True
|
|
23
|
+
|
|
24
|
+
created_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
|
25
|
+
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), server_default=func.now(), server_onupdate=func.now())
|
|
26
|
+
is_deleted: Mapped[bool] = mapped_column(Boolean, server_default=text("FALSE"))
|
|
27
|
+
|
|
28
|
+
@declared_attr
|
|
29
|
+
def _created_by_id(cls):
|
|
30
|
+
return cls._user_by_id()
|
|
31
|
+
|
|
32
|
+
@declared_attr
|
|
33
|
+
def _last_updated_by_id(cls):
|
|
34
|
+
return cls._user_by_id()
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def _user_by_id(cls):
|
|
38
|
+
"""a flexible non-constrained record of a user.
|
|
39
|
+
This way users can get added, deleted etc without history freaking out
|
|
40
|
+
"""
|
|
41
|
+
return mapped_column(SQLUUID(), nullable=True)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def last_updated_by_id(self) -> Optional[str]:
|
|
45
|
+
return self._user_id_getter("last_updated")
|
|
46
|
+
|
|
47
|
+
@last_updated_by_id.setter
|
|
48
|
+
def last_updated_by_id(self, value: str) -> None:
|
|
49
|
+
self._user_id_setter("last_updated", value)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def created_by_id(self) -> Optional[str]:
|
|
53
|
+
return self._user_id_getter("created")
|
|
54
|
+
|
|
55
|
+
@created_by_id.setter
|
|
56
|
+
def created_by_id(self, value: str) -> None:
|
|
57
|
+
self._user_id_setter("created", value)
|
|
58
|
+
|
|
59
|
+
def _user_id_getter(self, prop: str) -> Optional[str]:
|
|
60
|
+
"""returns the user id for the specified property"""
|
|
61
|
+
full_prop = f"_{prop}_by_id"
|
|
62
|
+
prop_value = getattr(self, full_prop, None)
|
|
63
|
+
if not prop_value:
|
|
64
|
+
return
|
|
65
|
+
return f"user-{prop_value}"
|
|
66
|
+
|
|
67
|
+
def _user_id_setter(self, prop: str, value: str) -> None:
|
|
68
|
+
"""returns the user id for the specified property"""
|
|
69
|
+
full_prop = f"_{prop}_by_id"
|
|
70
|
+
if not value:
|
|
71
|
+
setattr(self, full_prop, None)
|
|
72
|
+
return
|
|
73
|
+
prefix, id_ = value.split("-", 1)
|
|
74
|
+
assert prefix == "user", f"{prefix} is not a valid id prefix for a user id"
|
|
75
|
+
setattr(self, full_prop, UUID(id_))
|
letta/orm/enums.py
ADDED
letta/orm/errors.py
ADDED