letta-nightly 0.7.16.dev20250515205957__py3-none-any.whl → 0.7.17.dev20250516090339__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.
- letta/__init__.py +1 -1
- letta/agents/letta_agent.py +23 -15
- letta/interfaces/openai_streaming_interface.py +303 -0
- letta/orm/sqlalchemy_base.py +90 -21
- letta/server/rest_api/routers/v1/agents.py +18 -16
- letta/server/rest_api/routers/v1/blocks.py +1 -1
- letta/server/rest_api/routers/v1/groups.py +2 -2
- letta/server/rest_api/routers/v1/messages.py +5 -5
- letta/server/rest_api/routers/v1/runs.py +2 -2
- letta/server/rest_api/routers/v1/tools.py +1 -1
- letta/server/rest_api/routers/v1/users.py +9 -9
- letta/server/rest_api/routers/v1/voice.py +1 -1
- letta/services/agent_manager.py +4 -8
- letta/services/message_manager.py +15 -0
- letta/services/passage_manager.py +14 -0
- letta/services/user_manager.py +70 -0
- {letta_nightly-0.7.16.dev20250515205957.dist-info → letta_nightly-0.7.17.dev20250516090339.dist-info}/METADATA +1 -1
- {letta_nightly-0.7.16.dev20250515205957.dist-info → letta_nightly-0.7.17.dev20250516090339.dist-info}/RECORD +21 -20
- {letta_nightly-0.7.16.dev20250515205957.dist-info → letta_nightly-0.7.17.dev20250516090339.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.16.dev20250515205957.dist-info → letta_nightly-0.7.17.dev20250516090339.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.16.dev20250515205957.dist-info → letta_nightly-0.7.17.dev20250516090339.dist-info}/entry_points.txt +0 -0
letta/__init__.py
CHANGED
letta/agents/letta_agent.py
CHANGED
@@ -8,10 +8,11 @@ from openai.types import CompletionUsage
|
|
8
8
|
from openai.types.chat import ChatCompletion, ChatCompletionChunk
|
9
9
|
|
10
10
|
from letta.agents.base_agent import BaseAgent
|
11
|
-
from letta.agents.helpers import _create_letta_response,
|
11
|
+
from letta.agents.helpers import _create_letta_response, _prepare_in_context_messages_async
|
12
12
|
from letta.helpers import ToolRulesSolver
|
13
13
|
from letta.helpers.tool_execution_helper import enable_strict_mode
|
14
14
|
from letta.interfaces.anthropic_streaming_interface import AnthropicStreamingInterface
|
15
|
+
from letta.interfaces.openai_streaming_interface import OpenAIStreamingInterface
|
15
16
|
from letta.llm_api.llm_client import LLMClient
|
16
17
|
from letta.llm_api.llm_client_base import LLMClientBase
|
17
18
|
from letta.local_llm.constants import INNER_THOUGHTS_KWARG
|
@@ -61,12 +62,8 @@ class LettaAgent(BaseAgent):
|
|
61
62
|
self.last_function_response = None
|
62
63
|
|
63
64
|
# Cached archival memory/message size
|
64
|
-
self.num_messages =
|
65
|
-
self.num_archival_memories =
|
66
|
-
|
67
|
-
# Cached archival memory/message size
|
68
|
-
self.num_messages = self.message_manager.size(actor=self.actor, agent_id=agent_id)
|
69
|
-
self.num_archival_memories = self.passage_manager.size(actor=self.actor, agent_id=agent_id)
|
65
|
+
self.num_messages = 0
|
66
|
+
self.num_archival_memories = 0
|
70
67
|
|
71
68
|
@trace_method
|
72
69
|
async def step(self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool = True) -> LettaResponse:
|
@@ -81,7 +78,7 @@ class LettaAgent(BaseAgent):
|
|
81
78
|
async def _step(
|
82
79
|
self, agent_state: AgentState, input_messages: List[MessageCreate], max_steps: int = 10
|
83
80
|
) -> Tuple[List[Message], List[Message], CompletionUsage]:
|
84
|
-
current_in_context_messages, new_in_context_messages =
|
81
|
+
current_in_context_messages, new_in_context_messages = await _prepare_in_context_messages_async(
|
85
82
|
input_messages, agent_state, self.message_manager, self.actor
|
86
83
|
)
|
87
84
|
tool_rules_solver = ToolRulesSolver(agent_state.tool_rules)
|
@@ -129,14 +126,14 @@ class LettaAgent(BaseAgent):
|
|
129
126
|
|
130
127
|
@trace_method
|
131
128
|
async def step_stream(
|
132
|
-
self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool = True
|
129
|
+
self, input_messages: List[MessageCreate], max_steps: int = 10, use_assistant_message: bool = True, stream_tokens: bool = False
|
133
130
|
) -> AsyncGenerator[str, None]:
|
134
131
|
"""
|
135
132
|
Main streaming loop that yields partial tokens.
|
136
133
|
Whenever we detect a tool call, we yield from _handle_ai_response as well.
|
137
134
|
"""
|
138
135
|
agent_state = await self.agent_manager.get_agent_by_id_async(self.agent_id, actor=self.actor)
|
139
|
-
current_in_context_messages, new_in_context_messages =
|
136
|
+
current_in_context_messages, new_in_context_messages = await _prepare_in_context_messages_async(
|
140
137
|
input_messages, agent_state, self.message_manager, self.actor
|
141
138
|
)
|
142
139
|
tool_rules_solver = ToolRulesSolver(agent_state.tool_rules)
|
@@ -157,9 +154,16 @@ class LettaAgent(BaseAgent):
|
|
157
154
|
)
|
158
155
|
# TODO: THIS IS INCREDIBLY UGLY
|
159
156
|
# TODO: THERE ARE MULTIPLE COPIES OF THE LLM_CONFIG EVERYWHERE THAT ARE GETTING MANIPULATED
|
160
|
-
|
161
|
-
|
162
|
-
|
157
|
+
if agent_state.llm_config.model_endpoint_type == "anthropic":
|
158
|
+
interface = AnthropicStreamingInterface(
|
159
|
+
use_assistant_message=use_assistant_message,
|
160
|
+
put_inner_thoughts_in_kwarg=agent_state.llm_config.put_inner_thoughts_in_kwargs,
|
161
|
+
)
|
162
|
+
elif agent_state.llm_config.model_endpoint_type == "openai":
|
163
|
+
interface = OpenAIStreamingInterface(
|
164
|
+
use_assistant_message=use_assistant_message,
|
165
|
+
put_inner_thoughts_in_kwarg=agent_state.llm_config.put_inner_thoughts_in_kwargs,
|
166
|
+
)
|
163
167
|
async for chunk in interface.process(stream):
|
164
168
|
yield f"data: {chunk.model_dump_json()}\n\n"
|
165
169
|
|
@@ -197,8 +201,8 @@ class LettaAgent(BaseAgent):
|
|
197
201
|
|
198
202
|
# TODO: This may be out of sync, if in between steps users add files
|
199
203
|
# NOTE (cliandy): temporary for now for particlar use cases.
|
200
|
-
self.num_messages = self.message_manager.
|
201
|
-
self.num_archival_memories = self.passage_manager.
|
204
|
+
self.num_messages = await self.message_manager.size_async(actor=self.actor, agent_id=agent_state.id)
|
205
|
+
self.num_archival_memories = await self.passage_manager.size_async(actor=self.actor, agent_id=agent_state.id)
|
202
206
|
|
203
207
|
# TODO: Also yield out a letta usage stats SSE
|
204
208
|
yield f"data: {usage.model_dump_json()}\n\n"
|
@@ -215,6 +219,10 @@ class LettaAgent(BaseAgent):
|
|
215
219
|
stream: bool,
|
216
220
|
) -> ChatCompletion | AsyncStream[ChatCompletionChunk]:
|
217
221
|
if settings.experimental_enable_async_db_engine:
|
222
|
+
self.num_messages = self.num_messages or (await self.message_manager.size_async(actor=self.actor, agent_id=agent_state.id))
|
223
|
+
self.num_archival_memories = self.num_archival_memories or (
|
224
|
+
await self.passage_manager.size_async(actor=self.actor, agent_id=agent_state.id)
|
225
|
+
)
|
218
226
|
in_context_messages = await self._rebuild_memory_async(
|
219
227
|
in_context_messages, agent_state, num_messages=self.num_messages, num_archival_memories=self.num_archival_memories
|
220
228
|
)
|
@@ -0,0 +1,303 @@
|
|
1
|
+
from datetime import datetime, timezone
|
2
|
+
from typing import AsyncGenerator, List, Optional
|
3
|
+
|
4
|
+
from openai import AsyncStream
|
5
|
+
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
|
6
|
+
|
7
|
+
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
|
8
|
+
from letta.schemas.letta_message import AssistantMessage, LettaMessage, ReasoningMessage, ToolCallDelta, ToolCallMessage
|
9
|
+
from letta.schemas.letta_message_content import TextContent
|
10
|
+
from letta.schemas.message import Message
|
11
|
+
from letta.schemas.openai.chat_completion_response import FunctionCall, ToolCall
|
12
|
+
from letta.server.rest_api.json_parser import OptimisticJSONParser
|
13
|
+
from letta.streaming_utils import JSONInnerThoughtsExtractor
|
14
|
+
|
15
|
+
|
16
|
+
class OpenAIStreamingInterface:
|
17
|
+
"""
|
18
|
+
Encapsulates the logic for streaming responses from OpenAI.
|
19
|
+
This class handles parsing of partial tokens, pre-execution messages,
|
20
|
+
and detection of tool call events.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self, use_assistant_message: bool = False, put_inner_thoughts_in_kwarg: bool = False):
|
24
|
+
self.use_assistant_message = use_assistant_message
|
25
|
+
self.assistant_message_tool_name = DEFAULT_MESSAGE_TOOL
|
26
|
+
self.assistant_message_tool_kwarg = DEFAULT_MESSAGE_TOOL_KWARG
|
27
|
+
|
28
|
+
self.optimistic_json_parser: OptimisticJSONParser = OptimisticJSONParser()
|
29
|
+
self.function_args_reader = JSONInnerThoughtsExtractor(wait_for_first_key=True) # TODO: pass in kward
|
30
|
+
self.function_name_buffer = None
|
31
|
+
self.function_args_buffer = None
|
32
|
+
self.function_id_buffer = None
|
33
|
+
self.last_flushed_function_name = None
|
34
|
+
|
35
|
+
# Buffer to hold function arguments until inner thoughts are complete
|
36
|
+
self.current_function_arguments = ""
|
37
|
+
self.current_json_parse_result = {}
|
38
|
+
|
39
|
+
# Premake IDs for database writes
|
40
|
+
self.letta_assistant_message_id = Message.generate_id()
|
41
|
+
self.letta_tool_message_id = Message.generate_id()
|
42
|
+
|
43
|
+
# token counters
|
44
|
+
self.input_tokens = 0
|
45
|
+
self.output_tokens = 0
|
46
|
+
|
47
|
+
self.content_buffer: List[str] = []
|
48
|
+
self.tool_call_name: Optional[str] = None
|
49
|
+
self.tool_call_id: Optional[str] = None
|
50
|
+
self.reasoning_messages = []
|
51
|
+
|
52
|
+
def get_reasoning_content(self) -> List[TextContent]:
|
53
|
+
content = "".join(self.reasoning_messages)
|
54
|
+
return [TextContent(text=content)]
|
55
|
+
|
56
|
+
def get_tool_call_object(self) -> ToolCall:
|
57
|
+
"""Useful for agent loop"""
|
58
|
+
return ToolCall(
|
59
|
+
id=self.letta_tool_message_id,
|
60
|
+
function=FunctionCall(arguments=self.current_function_arguments, name=self.last_flushed_function_name),
|
61
|
+
)
|
62
|
+
|
63
|
+
async def process(self, stream: AsyncStream[ChatCompletionChunk]) -> AsyncGenerator[LettaMessage, None]:
|
64
|
+
"""
|
65
|
+
Iterates over the OpenAI stream, yielding SSE events.
|
66
|
+
It also collects tokens and detects if a tool call is triggered.
|
67
|
+
"""
|
68
|
+
async with stream:
|
69
|
+
prev_message_type = None
|
70
|
+
message_index = 0
|
71
|
+
async for chunk in stream:
|
72
|
+
# track usage
|
73
|
+
if chunk.usage:
|
74
|
+
self.input_tokens += len(chunk.usage.prompt_tokens)
|
75
|
+
self.output_tokens += len(chunk.usage.completion_tokens)
|
76
|
+
|
77
|
+
if chunk.choices:
|
78
|
+
choice = chunk.choices[0]
|
79
|
+
message_delta = choice.delta
|
80
|
+
|
81
|
+
if message_delta.tool_calls is not None and len(message_delta.tool_calls) > 0:
|
82
|
+
tool_call = message_delta.tool_calls[0]
|
83
|
+
|
84
|
+
if tool_call.function.name:
|
85
|
+
# If we're waiting for the first key, then we should hold back the name
|
86
|
+
# ie add it to a buffer instead of returning it as a chunk
|
87
|
+
if self.function_name_buffer is None:
|
88
|
+
self.function_name_buffer = tool_call.function.name
|
89
|
+
else:
|
90
|
+
self.function_name_buffer += tool_call.function.name
|
91
|
+
|
92
|
+
if tool_call.id:
|
93
|
+
# Buffer until next time
|
94
|
+
if self.function_id_buffer is None:
|
95
|
+
self.function_id_buffer = tool_call.id
|
96
|
+
else:
|
97
|
+
self.function_id_buffer += tool_call.id
|
98
|
+
|
99
|
+
if tool_call.function.arguments:
|
100
|
+
# updates_main_json, updates_inner_thoughts = self.function_args_reader.process_fragment(tool_call.function.arguments)
|
101
|
+
self.current_function_arguments += tool_call.function.arguments
|
102
|
+
updates_main_json, updates_inner_thoughts = self.function_args_reader.process_fragment(
|
103
|
+
tool_call.function.arguments
|
104
|
+
)
|
105
|
+
|
106
|
+
# If we have inner thoughts, we should output them as a chunk
|
107
|
+
if updates_inner_thoughts:
|
108
|
+
if prev_message_type and prev_message_type != "reasoning_message":
|
109
|
+
message_index += 1
|
110
|
+
self.reasoning_messages.append(updates_inner_thoughts)
|
111
|
+
reasoning_message = ReasoningMessage(
|
112
|
+
id=self.letta_tool_message_id,
|
113
|
+
date=datetime.now(timezone.utc),
|
114
|
+
reasoning=updates_inner_thoughts,
|
115
|
+
# name=name,
|
116
|
+
otid=Message.generate_otid_from_id(self.letta_tool_message_id, message_index),
|
117
|
+
)
|
118
|
+
prev_message_type = reasoning_message.message_type
|
119
|
+
yield reasoning_message
|
120
|
+
|
121
|
+
# Additionally inner thoughts may stream back with a chunk of main JSON
|
122
|
+
# In that case, since we can only return a chunk at a time, we should buffer it
|
123
|
+
if updates_main_json:
|
124
|
+
if self.function_args_buffer is None:
|
125
|
+
self.function_args_buffer = updates_main_json
|
126
|
+
else:
|
127
|
+
self.function_args_buffer += updates_main_json
|
128
|
+
|
129
|
+
# If we have main_json, we should output a ToolCallMessage
|
130
|
+
elif updates_main_json:
|
131
|
+
|
132
|
+
# If there's something in the function_name buffer, we should release it first
|
133
|
+
# NOTE: we could output it as part of a chunk that has both name and args,
|
134
|
+
# however the frontend may expect name first, then args, so to be
|
135
|
+
# safe we'll output name first in a separate chunk
|
136
|
+
if self.function_name_buffer:
|
137
|
+
|
138
|
+
# use_assisitant_message means that we should also not release main_json raw, and instead should only release the contents of "message": "..."
|
139
|
+
if self.use_assistant_message and self.function_name_buffer == self.assistant_message_tool_name:
|
140
|
+
|
141
|
+
# Store the ID of the tool call so allow skipping the corresponding response
|
142
|
+
if self.function_id_buffer:
|
143
|
+
self.prev_assistant_message_id = self.function_id_buffer
|
144
|
+
|
145
|
+
else:
|
146
|
+
if prev_message_type and prev_message_type != "tool_call_message":
|
147
|
+
message_index += 1
|
148
|
+
self.tool_call_name = str(self.function_name_buffer)
|
149
|
+
tool_call_msg = ToolCallMessage(
|
150
|
+
id=self.letta_tool_message_id,
|
151
|
+
date=datetime.now(timezone.utc),
|
152
|
+
tool_call=ToolCallDelta(
|
153
|
+
name=self.function_name_buffer,
|
154
|
+
arguments=None,
|
155
|
+
tool_call_id=self.function_id_buffer,
|
156
|
+
),
|
157
|
+
otid=Message.generate_otid_from_id(self.letta_tool_message_id, message_index),
|
158
|
+
)
|
159
|
+
prev_message_type = tool_call_msg.message_type
|
160
|
+
yield tool_call_msg
|
161
|
+
|
162
|
+
# Record what the last function name we flushed was
|
163
|
+
self.last_flushed_function_name = self.function_name_buffer
|
164
|
+
# Clear the buffer
|
165
|
+
self.function_name_buffer = None
|
166
|
+
self.function_id_buffer = None
|
167
|
+
# Since we're clearing the name buffer, we should store
|
168
|
+
# any updates to the arguments inside a separate buffer
|
169
|
+
|
170
|
+
# Add any main_json updates to the arguments buffer
|
171
|
+
if self.function_args_buffer is None:
|
172
|
+
self.function_args_buffer = updates_main_json
|
173
|
+
else:
|
174
|
+
self.function_args_buffer += updates_main_json
|
175
|
+
|
176
|
+
# If there was nothing in the name buffer, we can proceed to
|
177
|
+
# output the arguments chunk as a ToolCallMessage
|
178
|
+
else:
|
179
|
+
|
180
|
+
# use_assisitant_message means that we should also not release main_json raw, and instead should only release the contents of "message": "..."
|
181
|
+
if self.use_assistant_message and (
|
182
|
+
self.last_flushed_function_name is not None
|
183
|
+
and self.last_flushed_function_name == self.assistant_message_tool_name
|
184
|
+
):
|
185
|
+
# do an additional parse on the updates_main_json
|
186
|
+
if self.function_args_buffer:
|
187
|
+
updates_main_json = self.function_args_buffer + updates_main_json
|
188
|
+
self.function_args_buffer = None
|
189
|
+
|
190
|
+
# Pretty gross hardcoding that assumes that if we're toggling into the keywords, we have the full prefix
|
191
|
+
match_str = '{"' + self.assistant_message_tool_kwarg + '":"'
|
192
|
+
if updates_main_json == match_str:
|
193
|
+
updates_main_json = None
|
194
|
+
|
195
|
+
else:
|
196
|
+
# Some hardcoding to strip off the trailing "}"
|
197
|
+
if updates_main_json in ["}", '"}']:
|
198
|
+
updates_main_json = None
|
199
|
+
if updates_main_json and len(updates_main_json) > 0 and updates_main_json[-1:] == '"':
|
200
|
+
updates_main_json = updates_main_json[:-1]
|
201
|
+
|
202
|
+
if not updates_main_json:
|
203
|
+
# early exit to turn into content mode
|
204
|
+
continue
|
205
|
+
|
206
|
+
# There may be a buffer from a previous chunk, for example
|
207
|
+
# if the previous chunk had arguments but we needed to flush name
|
208
|
+
if self.function_args_buffer:
|
209
|
+
# In this case, we should release the buffer + new data at once
|
210
|
+
combined_chunk = self.function_args_buffer + updates_main_json
|
211
|
+
|
212
|
+
if prev_message_type and prev_message_type != "assistant_message":
|
213
|
+
message_index += 1
|
214
|
+
assistant_message = AssistantMessage(
|
215
|
+
id=self.letta_assistant_message_id,
|
216
|
+
date=datetime.now(timezone.utc),
|
217
|
+
content=combined_chunk,
|
218
|
+
otid=Message.generate_otid_from_id(self.letta_assistant_message_id, message_index),
|
219
|
+
)
|
220
|
+
prev_message_type = assistant_message.message_type
|
221
|
+
yield assistant_message
|
222
|
+
# Store the ID of the tool call so allow skipping the corresponding response
|
223
|
+
if self.function_id_buffer:
|
224
|
+
self.prev_assistant_message_id = self.function_id_buffer
|
225
|
+
# clear buffer
|
226
|
+
self.function_args_buffer = None
|
227
|
+
self.function_id_buffer = None
|
228
|
+
|
229
|
+
else:
|
230
|
+
# If there's no buffer to clear, just output a new chunk with new data
|
231
|
+
# TODO: THIS IS HORRIBLE
|
232
|
+
# TODO: WE USE THE OLD JSON PARSER EARLIER (WHICH DOES NOTHING) AND NOW THE NEW JSON PARSER
|
233
|
+
# TODO: THIS IS TOTALLY WRONG AND BAD, BUT SAVING FOR A LARGER REWRITE IN THE NEAR FUTURE
|
234
|
+
parsed_args = self.optimistic_json_parser.parse(self.current_function_arguments)
|
235
|
+
|
236
|
+
if parsed_args.get(self.assistant_message_tool_kwarg) and parsed_args.get(
|
237
|
+
self.assistant_message_tool_kwarg
|
238
|
+
) != self.current_json_parse_result.get(self.assistant_message_tool_kwarg):
|
239
|
+
new_content = parsed_args.get(self.assistant_message_tool_kwarg)
|
240
|
+
prev_content = self.current_json_parse_result.get(self.assistant_message_tool_kwarg, "")
|
241
|
+
# TODO: Assumes consistent state and that prev_content is subset of new_content
|
242
|
+
diff = new_content.replace(prev_content, "", 1)
|
243
|
+
self.current_json_parse_result = parsed_args
|
244
|
+
if prev_message_type and prev_message_type != "assistant_message":
|
245
|
+
message_index += 1
|
246
|
+
assistant_message = AssistantMessage(
|
247
|
+
id=self.letta_assistant_message_id,
|
248
|
+
date=datetime.now(timezone.utc),
|
249
|
+
content=diff,
|
250
|
+
# name=name,
|
251
|
+
otid=Message.generate_otid_from_id(self.letta_assistant_message_id, message_index),
|
252
|
+
)
|
253
|
+
prev_message_type = assistant_message.message_type
|
254
|
+
yield assistant_message
|
255
|
+
|
256
|
+
# Store the ID of the tool call so allow skipping the corresponding response
|
257
|
+
if self.function_id_buffer:
|
258
|
+
self.prev_assistant_message_id = self.function_id_buffer
|
259
|
+
# clear buffers
|
260
|
+
self.function_id_buffer = None
|
261
|
+
else:
|
262
|
+
|
263
|
+
# There may be a buffer from a previous chunk, for example
|
264
|
+
# if the previous chunk had arguments but we needed to flush name
|
265
|
+
if self.function_args_buffer:
|
266
|
+
# In this case, we should release the buffer + new data at once
|
267
|
+
combined_chunk = self.function_args_buffer + updates_main_json
|
268
|
+
if prev_message_type and prev_message_type != "tool_call_message":
|
269
|
+
message_index += 1
|
270
|
+
tool_call_msg = ToolCallMessage(
|
271
|
+
id=self.letta_tool_message_id,
|
272
|
+
date=datetime.now(timezone.utc),
|
273
|
+
tool_call=ToolCallDelta(
|
274
|
+
name=None,
|
275
|
+
arguments=combined_chunk,
|
276
|
+
tool_call_id=self.function_id_buffer,
|
277
|
+
),
|
278
|
+
# name=name,
|
279
|
+
otid=Message.generate_otid_from_id(self.letta_tool_message_id, message_index),
|
280
|
+
)
|
281
|
+
prev_message_type = tool_call_msg.message_type
|
282
|
+
yield tool_call_msg
|
283
|
+
# clear buffer
|
284
|
+
self.function_args_buffer = None
|
285
|
+
self.function_id_buffer = None
|
286
|
+
else:
|
287
|
+
# If there's no buffer to clear, just output a new chunk with new data
|
288
|
+
if prev_message_type and prev_message_type != "tool_call_message":
|
289
|
+
message_index += 1
|
290
|
+
tool_call_msg = ToolCallMessage(
|
291
|
+
id=self.letta_tool_message_id,
|
292
|
+
date=datetime.now(timezone.utc),
|
293
|
+
tool_call=ToolCallDelta(
|
294
|
+
name=None,
|
295
|
+
arguments=updates_main_json,
|
296
|
+
tool_call_id=self.function_id_buffer,
|
297
|
+
),
|
298
|
+
# name=name,
|
299
|
+
otid=Message.generate_otid_from_id(self.letta_tool_message_id, message_index),
|
300
|
+
)
|
301
|
+
prev_message_type = tool_call_msg.message_type
|
302
|
+
yield tool_call_msg
|
303
|
+
self.function_id_buffer = None
|
letta/orm/sqlalchemy_base.py
CHANGED
@@ -745,6 +745,17 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
745
745
|
self.is_deleted = True
|
746
746
|
return self.update(db_session)
|
747
747
|
|
748
|
+
@handle_db_timeout
|
749
|
+
async def delete_async(self, db_session: "AsyncSession", actor: Optional["User"] = None) -> "SqlalchemyBase":
|
750
|
+
"""Soft delete a record asynchronously (mark as deleted)."""
|
751
|
+
logger.debug(f"Soft deleting {self.__class__.__name__} with ID: {self.id} with actor={actor} (async)")
|
752
|
+
|
753
|
+
if actor:
|
754
|
+
self._set_created_and_updated_by_fields(actor.id)
|
755
|
+
|
756
|
+
self.is_deleted = True
|
757
|
+
return await self.update_async(db_session)
|
758
|
+
|
748
759
|
@handle_db_timeout
|
749
760
|
def hard_delete(self, db_session: "Session", actor: Optional["User"] = None) -> None:
|
750
761
|
"""Permanently removes the record from the database."""
|
@@ -761,6 +772,20 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
761
772
|
else:
|
762
773
|
logger.debug(f"{self.__class__.__name__} with ID {self.id} successfully hard deleted")
|
763
774
|
|
775
|
+
@handle_db_timeout
|
776
|
+
async def hard_delete_async(self, db_session: "AsyncSession", actor: Optional["User"] = None) -> None:
|
777
|
+
"""Permanently removes the record from the database asynchronously."""
|
778
|
+
logger.debug(f"Hard deleting {self.__class__.__name__} with ID: {self.id} with actor={actor} (async)")
|
779
|
+
|
780
|
+
async with db_session as session:
|
781
|
+
try:
|
782
|
+
await session.delete(self)
|
783
|
+
await session.commit()
|
784
|
+
except Exception as e:
|
785
|
+
await session.rollback()
|
786
|
+
logger.exception(f"Failed to hard delete {self.__class__.__name__} with ID {self.id}")
|
787
|
+
raise ValueError(f"Failed to hard delete {self.__class__.__name__} with ID {self.id}: {e}")
|
788
|
+
|
764
789
|
@handle_db_timeout
|
765
790
|
def update(self, db_session: Session, actor: Optional["User"] = None, no_commit: bool = False) -> "SqlalchemyBase":
|
766
791
|
logger.debug(...)
|
@@ -793,6 +818,39 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
793
818
|
await db_session.refresh(self)
|
794
819
|
return self
|
795
820
|
|
821
|
+
@classmethod
|
822
|
+
def _size_preprocess(
|
823
|
+
cls,
|
824
|
+
*,
|
825
|
+
db_session: "Session",
|
826
|
+
actor: Optional["User"] = None,
|
827
|
+
access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
|
828
|
+
access_type: AccessType = AccessType.ORGANIZATION,
|
829
|
+
**kwargs,
|
830
|
+
):
|
831
|
+
logger.debug(f"Calculating size for {cls.__name__} with filters {kwargs}")
|
832
|
+
query = select(func.count()).select_from(cls)
|
833
|
+
|
834
|
+
if actor:
|
835
|
+
query = cls.apply_access_predicate(query, actor, access, access_type)
|
836
|
+
|
837
|
+
# Apply filtering logic based on kwargs
|
838
|
+
for key, value in kwargs.items():
|
839
|
+
if value:
|
840
|
+
column = getattr(cls, key, None)
|
841
|
+
if not column:
|
842
|
+
raise AttributeError(f"{cls.__name__} has no attribute '{key}'")
|
843
|
+
if isinstance(value, (list, tuple, set)): # Check for iterables
|
844
|
+
query = query.where(column.in_(value))
|
845
|
+
else: # Single value for equality filtering
|
846
|
+
query = query.where(column == value)
|
847
|
+
|
848
|
+
# Handle soft deletes if the class has the 'is_deleted' attribute
|
849
|
+
if hasattr(cls, "is_deleted"):
|
850
|
+
query = query.where(cls.is_deleted == False)
|
851
|
+
|
852
|
+
return query
|
853
|
+
|
796
854
|
@classmethod
|
797
855
|
@handle_db_timeout
|
798
856
|
def size(
|
@@ -817,28 +875,8 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
817
875
|
Raises:
|
818
876
|
DBAPIError: If a database error occurs
|
819
877
|
"""
|
820
|
-
logger.debug(f"Calculating size for {cls.__name__} with filters {kwargs}")
|
821
|
-
|
822
878
|
with db_session as session:
|
823
|
-
query =
|
824
|
-
|
825
|
-
if actor:
|
826
|
-
query = cls.apply_access_predicate(query, actor, access, access_type)
|
827
|
-
|
828
|
-
# Apply filtering logic based on kwargs
|
829
|
-
for key, value in kwargs.items():
|
830
|
-
if value:
|
831
|
-
column = getattr(cls, key, None)
|
832
|
-
if not column:
|
833
|
-
raise AttributeError(f"{cls.__name__} has no attribute '{key}'")
|
834
|
-
if isinstance(value, (list, tuple, set)): # Check for iterables
|
835
|
-
query = query.where(column.in_(value))
|
836
|
-
else: # Single value for equality filtering
|
837
|
-
query = query.where(column == value)
|
838
|
-
|
839
|
-
# Handle soft deletes if the class has the 'is_deleted' attribute
|
840
|
-
if hasattr(cls, "is_deleted"):
|
841
|
-
query = query.where(cls.is_deleted == False)
|
879
|
+
query = cls._size_preprocess(db_session=session, actor=actor, access=access, access_type=access_type, **kwargs)
|
842
880
|
|
843
881
|
try:
|
844
882
|
count = session.execute(query).scalar()
|
@@ -847,6 +885,37 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
847
885
|
logger.exception(f"Failed to calculate size for {cls.__name__}")
|
848
886
|
raise e
|
849
887
|
|
888
|
+
@classmethod
|
889
|
+
@handle_db_timeout
|
890
|
+
async def size_async(
|
891
|
+
cls,
|
892
|
+
*,
|
893
|
+
db_session: "AsyncSession",
|
894
|
+
actor: Optional["User"] = None,
|
895
|
+
access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
|
896
|
+
access_type: AccessType = AccessType.ORGANIZATION,
|
897
|
+
**kwargs,
|
898
|
+
) -> int:
|
899
|
+
"""
|
900
|
+
Get the count of rows that match the provided filters.
|
901
|
+
Args:
|
902
|
+
db_session: SQLAlchemy session
|
903
|
+
**kwargs: Filters to apply to the query (e.g., column_name=value)
|
904
|
+
Returns:
|
905
|
+
int: The count of rows that match the filters
|
906
|
+
Raises:
|
907
|
+
DBAPIError: If a database error occurs
|
908
|
+
"""
|
909
|
+
async with db_session as session:
|
910
|
+
query = cls._size_preprocess(db_session=session, actor=actor, access=access, access_type=access_type, **kwargs)
|
911
|
+
|
912
|
+
try:
|
913
|
+
count = await session.execute(query).scalar()
|
914
|
+
return count if count else 0
|
915
|
+
except DBAPIError as e:
|
916
|
+
logger.exception(f"Failed to calculate size for {cls.__name__}")
|
917
|
+
raise e
|
918
|
+
|
850
919
|
@classmethod
|
851
920
|
def apply_access_predicate(
|
852
921
|
cls,
|
@@ -83,7 +83,7 @@ async def list_agents(
|
|
83
83
|
"""
|
84
84
|
|
85
85
|
# Retrieve the actor (user) details
|
86
|
-
actor = server.user_manager.
|
86
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
87
87
|
|
88
88
|
# Call list_agents directly without unnecessary dict handling
|
89
89
|
return await server.agent_manager.list_agents_async(
|
@@ -163,7 +163,7 @@ async def import_agent_serialized(
|
|
163
163
|
"""
|
164
164
|
Import a serialized agent file and recreate the agent in the system.
|
165
165
|
"""
|
166
|
-
actor = server.user_manager.
|
166
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
167
167
|
|
168
168
|
try:
|
169
169
|
serialized_data = await file.read()
|
@@ -233,7 +233,7 @@ async def create_agent(
|
|
233
233
|
Create a new agent with the specified configuration.
|
234
234
|
"""
|
235
235
|
try:
|
236
|
-
actor = server.user_manager.
|
236
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
237
237
|
return await server.create_agent_async(agent, actor=actor)
|
238
238
|
except Exception as e:
|
239
239
|
traceback.print_exc()
|
@@ -248,7 +248,7 @@ async def modify_agent(
|
|
248
248
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
249
249
|
):
|
250
250
|
"""Update an existing agent"""
|
251
|
-
actor = server.user_manager.
|
251
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
252
252
|
return await server.update_agent_async(agent_id=agent_id, request=update_agent, actor=actor)
|
253
253
|
|
254
254
|
|
@@ -333,7 +333,7 @@ def detach_source(
|
|
333
333
|
|
334
334
|
|
335
335
|
@router.get("/{agent_id}", response_model=AgentState, operation_id="retrieve_agent")
|
336
|
-
def retrieve_agent(
|
336
|
+
async def retrieve_agent(
|
337
337
|
agent_id: str,
|
338
338
|
server: "SyncServer" = Depends(get_letta_server),
|
339
339
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
@@ -344,7 +344,7 @@ def retrieve_agent(
|
|
344
344
|
actor = server.user_manager.get_user_or_default(user_id=actor_id)
|
345
345
|
|
346
346
|
try:
|
347
|
-
return server.agent_manager.
|
347
|
+
return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor)
|
348
348
|
except NoResultFound as e:
|
349
349
|
raise HTTPException(status_code=404, detail=str(e))
|
350
350
|
|
@@ -414,7 +414,7 @@ def retrieve_block(
|
|
414
414
|
|
415
415
|
|
416
416
|
@router.get("/{agent_id}/core-memory/blocks", response_model=List[Block], operation_id="list_core_memory_blocks")
|
417
|
-
def list_blocks(
|
417
|
+
async def list_blocks(
|
418
418
|
agent_id: str,
|
419
419
|
server: "SyncServer" = Depends(get_letta_server),
|
420
420
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
@@ -424,7 +424,7 @@ def list_blocks(
|
|
424
424
|
"""
|
425
425
|
actor = server.user_manager.get_user_or_default(user_id=actor_id)
|
426
426
|
try:
|
427
|
-
agent = server.agent_manager.
|
427
|
+
agent = await server.agent_manager.get_agent_by_id_async(agent_id, actor)
|
428
428
|
return agent.memory.blocks
|
429
429
|
except NoResultFound as e:
|
430
430
|
raise HTTPException(status_code=404, detail=str(e))
|
@@ -628,9 +628,9 @@ async def send_message(
|
|
628
628
|
Process a user message and return the agent's response.
|
629
629
|
This endpoint accepts a message from a user and processes it through the agent.
|
630
630
|
"""
|
631
|
-
actor = server.user_manager.
|
631
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
632
632
|
# TODO: This is redundant, remove soon
|
633
|
-
agent = server.agent_manager.
|
633
|
+
agent = await server.agent_manager.get_agent_by_id_async(agent_id, actor)
|
634
634
|
agent_eligible = not agent.enable_sleeptime and not agent.multi_agent_group and agent.agent_type != AgentType.sleeptime_agent
|
635
635
|
experimental_header = request_obj.headers.get("X-EXPERIMENTAL") or "false"
|
636
636
|
feature_enabled = settings.use_experimental or experimental_header.lower() == "true"
|
@@ -686,13 +686,13 @@ async def send_message_streaming(
|
|
686
686
|
It will stream the steps of the response always, and stream the tokens if 'stream_tokens' is set to True.
|
687
687
|
"""
|
688
688
|
request_start_timestamp_ns = get_utc_timestamp_ns()
|
689
|
-
actor = server.user_manager.
|
689
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
690
690
|
# TODO: This is redundant, remove soon
|
691
|
-
agent = server.agent_manager.
|
691
|
+
agent = await server.agent_manager.get_agent_by_id_async(agent_id, actor)
|
692
692
|
agent_eligible = not agent.enable_sleeptime and not agent.multi_agent_group and agent.agent_type != AgentType.sleeptime_agent
|
693
693
|
experimental_header = request_obj.headers.get("X-EXPERIMENTAL") or "false"
|
694
694
|
feature_enabled = settings.use_experimental or experimental_header.lower() == "true"
|
695
|
-
model_compatible = agent.llm_config.model_endpoint_type
|
695
|
+
model_compatible = agent.llm_config.model_endpoint_type in ["anthropic", "openai"]
|
696
696
|
|
697
697
|
if agent_eligible and feature_enabled and model_compatible and request.stream_tokens:
|
698
698
|
experimental_agent = LettaAgent(
|
@@ -705,7 +705,9 @@ async def send_message_streaming(
|
|
705
705
|
)
|
706
706
|
|
707
707
|
result = StreamingResponse(
|
708
|
-
experimental_agent.step_stream(
|
708
|
+
experimental_agent.step_stream(
|
709
|
+
request.messages, max_steps=10, use_assistant_message=request.use_assistant_message, stream_tokens=request.stream_tokens
|
710
|
+
),
|
709
711
|
media_type="text/event-stream",
|
710
712
|
)
|
711
713
|
else:
|
@@ -784,7 +786,7 @@ async def send_message_async(
|
|
784
786
|
Asynchronously process a user message and return a run object.
|
785
787
|
The actual processing happens in the background, and the status can be checked using the run ID.
|
786
788
|
"""
|
787
|
-
actor = server.user_manager.
|
789
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
788
790
|
|
789
791
|
# Create a new job
|
790
792
|
run = Run(
|
@@ -838,6 +840,6 @@ async def list_agent_groups(
|
|
838
840
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
839
841
|
):
|
840
842
|
"""Lists the groups for an agent"""
|
841
|
-
actor = server.user_manager.
|
843
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
842
844
|
print("in list agents with manager_type", manager_type)
|
843
845
|
return server.agent_manager.list_groups(agent_id=agent_id, manager_type=manager_type, actor=actor)
|
@@ -26,7 +26,7 @@ async def list_blocks(
|
|
26
26
|
server: SyncServer = Depends(get_letta_server),
|
27
27
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
28
28
|
):
|
29
|
-
actor = server.user_manager.
|
29
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
30
30
|
return await server.block_manager.get_blocks_async(
|
31
31
|
actor=actor,
|
32
32
|
label=label,
|
@@ -135,7 +135,7 @@ async def send_group_message(
|
|
135
135
|
Process a user message and return the group's response.
|
136
136
|
This endpoint accepts a message from a user and processes it through through agents in the group based on the specified pattern
|
137
137
|
"""
|
138
|
-
actor = server.user_manager.
|
138
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
139
139
|
result = await server.send_group_message_to_agent(
|
140
140
|
group_id=group_id,
|
141
141
|
actor=actor,
|
@@ -174,7 +174,7 @@ async def send_group_message_streaming(
|
|
174
174
|
This endpoint accepts a message from a user and processes it through agents in the group based on the specified pattern.
|
175
175
|
It will stream the steps of the response always, and stream the tokens if 'stream_tokens' is set to True.
|
176
176
|
"""
|
177
|
-
actor = server.user_manager.
|
177
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
178
178
|
result = await server.send_group_message_to_agent(
|
179
179
|
group_id=group_id,
|
180
180
|
actor=actor,
|
@@ -52,7 +52,7 @@ async def create_messages_batch(
|
|
52
52
|
detail=f"Server misconfiguration: LETTA_ENABLE_BATCH_JOB_POLLING is set to False.",
|
53
53
|
)
|
54
54
|
|
55
|
-
actor = server.user_manager.
|
55
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
56
56
|
batch_job = BatchJob(
|
57
57
|
user_id=actor.id,
|
58
58
|
status=JobStatus.running,
|
@@ -100,7 +100,7 @@ async def retrieve_batch_run(
|
|
100
100
|
"""
|
101
101
|
Get the status of a batch run.
|
102
102
|
"""
|
103
|
-
actor = server.user_manager.
|
103
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
104
104
|
|
105
105
|
try:
|
106
106
|
job = await server.job_manager.get_job_by_id_async(job_id=batch_id, actor=actor)
|
@@ -118,7 +118,7 @@ async def list_batch_runs(
|
|
118
118
|
List all batch runs.
|
119
119
|
"""
|
120
120
|
# TODO: filter
|
121
|
-
actor = server.user_manager.
|
121
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
122
122
|
|
123
123
|
jobs = server.job_manager.list_jobs(actor=actor, statuses=[JobStatus.created, JobStatus.running], job_type=JobType.BATCH)
|
124
124
|
return [BatchJob.from_job(job) for job in jobs]
|
@@ -150,7 +150,7 @@ async def list_batch_messages(
|
|
150
150
|
- For subsequent pages, use the ID of the last message from the previous response as the cursor
|
151
151
|
- Results will include messages before/after the cursor based on sort_descending
|
152
152
|
"""
|
153
|
-
actor = server.user_manager.
|
153
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
154
154
|
|
155
155
|
# First, verify the batch job exists and the user has access to it
|
156
156
|
try:
|
@@ -177,7 +177,7 @@ async def cancel_batch_run(
|
|
177
177
|
"""
|
178
178
|
Cancel a batch run.
|
179
179
|
"""
|
180
|
-
actor = server.user_manager.
|
180
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
181
181
|
|
182
182
|
try:
|
183
183
|
job = await server.job_manager.get_job_by_id_async(job_id=batch_id, actor=actor)
|
@@ -115,7 +115,7 @@ async def list_run_messages(
|
|
115
115
|
if order not in ["asc", "desc"]:
|
116
116
|
raise HTTPException(status_code=400, detail="Order must be 'asc' or 'desc'")
|
117
117
|
|
118
|
-
actor = server.user_manager.
|
118
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
119
119
|
|
120
120
|
try:
|
121
121
|
messages = server.job_manager.get_run_messages(
|
@@ -182,7 +182,7 @@ async def list_run_steps(
|
|
182
182
|
if order not in ["asc", "desc"]:
|
183
183
|
raise HTTPException(status_code=400, detail="Order must be 'asc' or 'desc'")
|
184
184
|
|
185
|
-
actor = server.user_manager.
|
185
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
186
186
|
|
187
187
|
try:
|
188
188
|
steps = server.job_manager.get_job_steps(
|
@@ -87,7 +87,7 @@ async def list_tools(
|
|
87
87
|
Get a list of all tools available to agents belonging to the org of the user
|
88
88
|
"""
|
89
89
|
try:
|
90
|
-
actor = server.user_manager.
|
90
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
91
91
|
if name is not None:
|
92
92
|
tool = await server.tool_manager.get_tool_by_name_async(tool_name=name, actor=actor)
|
93
93
|
return [tool] if tool else []
|
@@ -14,7 +14,7 @@ router = APIRouter(prefix="/users", tags=["users", "admin"])
|
|
14
14
|
|
15
15
|
|
16
16
|
@router.get("/", tags=["admin"], response_model=List[User], operation_id="list_users")
|
17
|
-
def list_users(
|
17
|
+
async def list_users(
|
18
18
|
after: Optional[str] = Query(None),
|
19
19
|
limit: Optional[int] = Query(50),
|
20
20
|
server: "SyncServer" = Depends(get_letta_server),
|
@@ -23,7 +23,7 @@ def list_users(
|
|
23
23
|
Get a list of all users in the database
|
24
24
|
"""
|
25
25
|
try:
|
26
|
-
users = server.user_manager.
|
26
|
+
users = await server.user_manager.list_actors_async(after=after, limit=limit)
|
27
27
|
except HTTPException:
|
28
28
|
raise
|
29
29
|
except Exception as e:
|
@@ -32,7 +32,7 @@ def list_users(
|
|
32
32
|
|
33
33
|
|
34
34
|
@router.post("/", tags=["admin"], response_model=User, operation_id="create_user")
|
35
|
-
def create_user(
|
35
|
+
async def create_user(
|
36
36
|
request: UserCreate = Body(...),
|
37
37
|
server: "SyncServer" = Depends(get_letta_server),
|
38
38
|
):
|
@@ -40,33 +40,33 @@ def create_user(
|
|
40
40
|
Create a new user in the database
|
41
41
|
"""
|
42
42
|
user = User(**request.model_dump())
|
43
|
-
user = server.user_manager.
|
43
|
+
user = await server.user_manager.create_actor_async(user)
|
44
44
|
return user
|
45
45
|
|
46
46
|
|
47
47
|
@router.put("/", tags=["admin"], response_model=User, operation_id="update_user")
|
48
|
-
def update_user(
|
48
|
+
async def update_user(
|
49
49
|
user: UserUpdate = Body(...),
|
50
50
|
server: "SyncServer" = Depends(get_letta_server),
|
51
51
|
):
|
52
52
|
"""
|
53
53
|
Update a user in the database
|
54
54
|
"""
|
55
|
-
user = server.user_manager.
|
55
|
+
user = await server.user_manager.update_actor_async(user)
|
56
56
|
return user
|
57
57
|
|
58
58
|
|
59
59
|
@router.delete("/", tags=["admin"], response_model=User, operation_id="delete_user")
|
60
|
-
def delete_user(
|
60
|
+
async def delete_user(
|
61
61
|
user_id: str = Query(..., description="The user_id key to be deleted."),
|
62
62
|
server: "SyncServer" = Depends(get_letta_server),
|
63
63
|
):
|
64
64
|
# TODO make a soft deletion, instead of a hard deletion
|
65
65
|
try:
|
66
|
-
user = server.user_manager.
|
66
|
+
user = await server.user_manager.get_actor_by_id_async(actor_id=user_id)
|
67
67
|
if user is None:
|
68
68
|
raise HTTPException(status_code=404, detail=f"User does not exist")
|
69
|
-
server.user_manager.
|
69
|
+
await server.user_manager.delete_actor_by_id_async(user_id=user_id)
|
70
70
|
except HTTPException:
|
71
71
|
raise
|
72
72
|
except Exception as e:
|
@@ -36,7 +36,7 @@ async def create_voice_chat_completions(
|
|
36
36
|
server: "SyncServer" = Depends(get_letta_server),
|
37
37
|
user_id: Optional[str] = Header(None, alias="user_id"),
|
38
38
|
):
|
39
|
-
actor = server.user_manager.
|
39
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=user_id)
|
40
40
|
|
41
41
|
# Create OpenAI async client
|
42
42
|
client = openai.AsyncClient(
|
letta/services/agent_manager.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import asyncio
|
1
2
|
from datetime import datetime, timezone
|
2
3
|
from typing import Dict, List, Optional, Set, Tuple
|
3
4
|
|
@@ -905,12 +906,7 @@ class AgentManager:
|
|
905
906
|
|
906
907
|
result = await session.execute(query)
|
907
908
|
agents = result.scalars().all()
|
908
|
-
|
909
|
-
for agent in agents:
|
910
|
-
pydantic_agent = await agent.to_pydantic_async(include_relationships=include_relationships)
|
911
|
-
pydantic_agents.append(pydantic_agent)
|
912
|
-
|
913
|
-
return pydantic_agents
|
909
|
+
return await asyncio.gather(*[agent.to_pydantic_async(include_relationships=include_relationships) for agent in agents])
|
914
910
|
|
915
911
|
@enforce_types
|
916
912
|
def list_agents_matching_tags(
|
@@ -1195,8 +1191,8 @@ class AgentManager:
|
|
1195
1191
|
|
1196
1192
|
@enforce_types
|
1197
1193
|
async def get_in_context_messages_async(self, agent_id: str, actor: PydanticUser) -> List[PydanticMessage]:
|
1198
|
-
|
1199
|
-
return await self.message_manager.get_messages_by_ids_async(message_ids=message_ids, actor=actor)
|
1194
|
+
agent = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor)
|
1195
|
+
return await self.message_manager.get_messages_by_ids_async(message_ids=agent.message_ids, actor=actor)
|
1200
1196
|
|
1201
1197
|
@enforce_types
|
1202
1198
|
def get_system_message(self, agent_id: str, actor: PydanticUser) -> PydanticMessage:
|
@@ -286,6 +286,21 @@ class MessageManager:
|
|
286
286
|
with db_registry.session() as session:
|
287
287
|
return MessageModel.size(db_session=session, actor=actor, role=role, agent_id=agent_id)
|
288
288
|
|
289
|
+
@enforce_types
|
290
|
+
async def size_async(
|
291
|
+
self,
|
292
|
+
actor: PydanticUser,
|
293
|
+
role: Optional[MessageRole] = None,
|
294
|
+
agent_id: Optional[str] = None,
|
295
|
+
) -> int:
|
296
|
+
"""Get the total count of messages with optional filters.
|
297
|
+
Args:
|
298
|
+
actor: The user requesting the count
|
299
|
+
role: The role of the message
|
300
|
+
"""
|
301
|
+
async with db_registry.async_session() as session:
|
302
|
+
return await MessageModel.size_async(db_session=session, actor=actor, role=role, agent_id=agent_id)
|
303
|
+
|
289
304
|
@enforce_types
|
290
305
|
def list_user_messages_for_agent(
|
291
306
|
self,
|
@@ -216,6 +216,20 @@ class PassageManager:
|
|
216
216
|
with db_registry.session() as session:
|
217
217
|
return AgentPassage.size(db_session=session, actor=actor, agent_id=agent_id)
|
218
218
|
|
219
|
+
@enforce_types
|
220
|
+
async def size_async(
|
221
|
+
self,
|
222
|
+
actor: PydanticUser,
|
223
|
+
agent_id: Optional[str] = None,
|
224
|
+
) -> int:
|
225
|
+
"""Get the total count of messages with optional filters.
|
226
|
+
Args:
|
227
|
+
actor: The user requesting the count
|
228
|
+
agent_id: The agent ID of the messages
|
229
|
+
"""
|
230
|
+
async with db_registry.async_session() as session:
|
231
|
+
return await AgentPassage.size_async(db_session=session, actor=actor, agent_id=agent_id)
|
232
|
+
|
219
233
|
def estimate_embeddings_size(
|
220
234
|
self,
|
221
235
|
actor: PydanticUser,
|
letta/services/user_manager.py
CHANGED
@@ -44,6 +44,14 @@ class UserManager:
|
|
44
44
|
new_user.create(session)
|
45
45
|
return new_user.to_pydantic()
|
46
46
|
|
47
|
+
@enforce_types
|
48
|
+
async def create_actor_async(self, pydantic_user: PydanticUser) -> PydanticUser:
|
49
|
+
"""Create a new user if it doesn't already exist (async version)."""
|
50
|
+
async with db_registry.async_session() as session:
|
51
|
+
new_user = UserModel(**pydantic_user.model_dump(to_orm=True))
|
52
|
+
await new_user.create_async(session)
|
53
|
+
return new_user.to_pydantic()
|
54
|
+
|
47
55
|
@enforce_types
|
48
56
|
def update_user(self, user_update: UserUpdate) -> PydanticUser:
|
49
57
|
"""Update user details."""
|
@@ -60,6 +68,22 @@ class UserManager:
|
|
60
68
|
existing_user.update(session)
|
61
69
|
return existing_user.to_pydantic()
|
62
70
|
|
71
|
+
@enforce_types
|
72
|
+
async def update_actor_async(self, user_update: UserUpdate) -> PydanticUser:
|
73
|
+
"""Update user details (async version)."""
|
74
|
+
async with db_registry.async_session() as session:
|
75
|
+
# Retrieve the existing user by ID
|
76
|
+
existing_user = await UserModel.read_async(db_session=session, identifier=user_update.id)
|
77
|
+
|
78
|
+
# Update only the fields that are provided in UserUpdate
|
79
|
+
update_data = user_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
|
80
|
+
for key, value in update_data.items():
|
81
|
+
setattr(existing_user, key, value)
|
82
|
+
|
83
|
+
# Commit the updated user
|
84
|
+
await existing_user.update_async(session)
|
85
|
+
return existing_user.to_pydantic()
|
86
|
+
|
63
87
|
@enforce_types
|
64
88
|
def delete_user_by_id(self, user_id: str):
|
65
89
|
"""Delete a user and their associated records (agents, sources, mappings)."""
|
@@ -70,6 +94,14 @@ class UserManager:
|
|
70
94
|
|
71
95
|
session.commit()
|
72
96
|
|
97
|
+
@enforce_types
|
98
|
+
async def delete_actor_by_id_async(self, user_id: str):
|
99
|
+
"""Delete a user and their associated records (agents, sources, mappings) asynchronously."""
|
100
|
+
async with db_registry.async_session() as session:
|
101
|
+
# Delete from user table
|
102
|
+
user = await UserModel.read_async(db_session=session, identifier=user_id)
|
103
|
+
await user.hard_delete_async(session)
|
104
|
+
|
73
105
|
@enforce_types
|
74
106
|
def get_user_by_id(self, user_id: str) -> PydanticUser:
|
75
107
|
"""Fetch a user by ID."""
|
@@ -77,6 +109,13 @@ class UserManager:
|
|
77
109
|
user = UserModel.read(db_session=session, identifier=user_id)
|
78
110
|
return user.to_pydantic()
|
79
111
|
|
112
|
+
@enforce_types
|
113
|
+
async def get_actor_by_id_async(self, actor_id: str) -> PydanticUser:
|
114
|
+
"""Fetch a user by ID asynchronously."""
|
115
|
+
async with db_registry.async_session() as session:
|
116
|
+
user = await UserModel.read_async(db_session=session, identifier=actor_id)
|
117
|
+
return user.to_pydantic()
|
118
|
+
|
80
119
|
@enforce_types
|
81
120
|
def get_default_user(self) -> PydanticUser:
|
82
121
|
"""Fetch the default user. If it doesn't exist, create it."""
|
@@ -96,6 +135,26 @@ class UserManager:
|
|
96
135
|
except NoResultFound:
|
97
136
|
return self.get_default_user()
|
98
137
|
|
138
|
+
@enforce_types
|
139
|
+
async def get_default_actor_async(self) -> PydanticUser:
|
140
|
+
"""Fetch the default user asynchronously. If it doesn't exist, create it."""
|
141
|
+
try:
|
142
|
+
return await self.get_actor_by_id_async(self.DEFAULT_USER_ID)
|
143
|
+
except NoResultFound:
|
144
|
+
# Fall back to synchronous version since create_default_user isn't async yet
|
145
|
+
return self.create_default_user(org_id=self.DEFAULT_ORG_ID)
|
146
|
+
|
147
|
+
@enforce_types
|
148
|
+
async def get_actor_or_default_async(self, actor_id: Optional[str] = None):
|
149
|
+
"""Fetch the user or default user asynchronously."""
|
150
|
+
if not actor_id:
|
151
|
+
return await self.get_default_actor_async()
|
152
|
+
|
153
|
+
try:
|
154
|
+
return await self.get_actor_by_id_async(actor_id=actor_id)
|
155
|
+
except NoResultFound:
|
156
|
+
return await self.get_default_actor_async()
|
157
|
+
|
99
158
|
@enforce_types
|
100
159
|
def list_users(self, after: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticUser]:
|
101
160
|
"""List all users with optional pagination."""
|
@@ -106,3 +165,14 @@ class UserManager:
|
|
106
165
|
limit=limit,
|
107
166
|
)
|
108
167
|
return [user.to_pydantic() for user in users]
|
168
|
+
|
169
|
+
@enforce_types
|
170
|
+
async def list_actors_async(self, after: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticUser]:
|
171
|
+
"""List all users with optional pagination (async version)."""
|
172
|
+
async with db_registry.async_session() as session:
|
173
|
+
users = await UserModel.list_async(
|
174
|
+
db_session=session,
|
175
|
+
after=after,
|
176
|
+
limit=limit,
|
177
|
+
)
|
178
|
+
return [user.to_pydantic() for user in users]
|
@@ -1,4 +1,4 @@
|
|
1
|
-
letta/__init__.py,sha256=
|
1
|
+
letta/__init__.py,sha256=P8KgAQ7KWTfdnf40ctwC28i6fsLOCgFMMT1GAU26_gE,916
|
2
2
|
letta/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
|
3
3
|
letta/agent.py,sha256=7f_vLO0b6pbCoXXzvgSIifViGRpg1MzeiesudknZyLk,72618
|
4
4
|
letta/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -6,7 +6,7 @@ letta/agents/base_agent.py,sha256=ZmuSpYF8a5I0QXs8LDgS4jaA9k-6Pu2W-hl4E8A1ELo,82
|
|
6
6
|
letta/agents/ephemeral_agent.py,sha256=el-SUF_16vv_7OouIR-6z0pAE9Yc0PLibygvfCKwqfo,2736
|
7
7
|
letta/agents/exceptions.py,sha256=BQY4D4w32OYHM63CM19ko7dPwZiAzUs3NbKvzmCTcJg,318
|
8
8
|
letta/agents/helpers.py,sha256=qJUmGgwVzNYTmV28sFoUu7MTuyqwzZ4qEpNXHVmSu6s,6055
|
9
|
-
letta/agents/letta_agent.py,sha256=
|
9
|
+
letta/agents/letta_agent.py,sha256=f_DlUwOQ-Uogvb1XgSh63veBFkMotU7Fx1YTeMtyERo,21109
|
10
10
|
letta/agents/letta_agent_batch.py,sha256=Z82Me5FV_jQ9PTopUvJ73F4NDFZeiDTgNPTQP5p9BIg,25292
|
11
11
|
letta/agents/voice_agent.py,sha256=wCF2adlbDTEk_P3UrGPCHZy4IGvw75TUGDePW6N-sGA,21402
|
12
12
|
letta/agents/voice_sleeptime_agent.py,sha256=gB44pOeIQJer_XqdxSNt7Txv0JaQAv9pzsiEFO9GYdY,7346
|
@@ -64,6 +64,7 @@ letta/interface.py,sha256=6GKasvJMASu-kcZch6Hffz1vnHuPA_ryI6cLH2bMArc,13023
|
|
64
64
|
letta/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
65
65
|
letta/interfaces/anthropic_streaming_interface.py,sha256=w2-adcgT18g_IZgOhjdxIALIHLD_lJrKGG_mx3VWn1c,21118
|
66
66
|
letta/interfaces/openai_chat_completions_streaming_interface.py,sha256=LANdVBA8UNWscBvsFbWTT8cxNg5fHA_woWU2jkTf6TQ,4911
|
67
|
+
letta/interfaces/openai_streaming_interface.py,sha256=_N_vSs6sSwzzSSjCA8lKdinDlcvcWZDloQa4uFRstvQ,19978
|
67
68
|
letta/interfaces/utils.py,sha256=c6jvO0dBYHh8DQnlN-B0qeNC64d3CSunhfqlFA4pJTY,278
|
68
69
|
letta/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
69
70
|
letta/jobs/helpers.py,sha256=kO4aj954xsQ1RAmkjY6LQQ7JEIGuhaxB1e9pzrYKHAY,914
|
@@ -160,7 +161,7 @@ letta/orm/provider.py,sha256=KxIyUijtFapxXsgD86tWCRt1sG0TIETEyqlHEUWB7Fg,1312
|
|
160
161
|
letta/orm/sandbox_config.py,sha256=DyOy_1_zCMlp13elCqPcuuA6OwUove6mrjhcpROTg50,4150
|
161
162
|
letta/orm/source.py,sha256=rtehzez80rRrJigXeRBgTlfTZEUy6cVqDizWEN2tvuY,2224
|
162
163
|
letta/orm/sources_agents.py,sha256=Ik_PokCBrXRd9wXWomeNeb8EtLUwjb9VMZ8LWXqpK5A,473
|
163
|
-
letta/orm/sqlalchemy_base.py,sha256=
|
164
|
+
letta/orm/sqlalchemy_base.py,sha256=AgDqsKfgsQSJjQGXm4VpgSG1ov4NKltlGkuQWuHiECM,41985
|
164
165
|
letta/orm/sqlite_functions.py,sha256=JCScKiRlYCKxy9hChQ8wsk4GMKknZE24MunnG3fM1Gw,4255
|
165
166
|
letta/orm/step.py,sha256=fjm7fLtYLCtFM6Mj6e2boP6P7dHSFG24Nem85VfVqHg,3216
|
166
167
|
letta/orm/tool.py,sha256=ft3BDA7Pt-zsXLyPvS_Z_Ibis6H6vY20F7Li7p6nPu8,2652
|
@@ -266,25 +267,25 @@ letta/server/rest_api/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
|
|
266
267
|
letta/server/rest_api/routers/openai/chat_completions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
267
268
|
letta/server/rest_api/routers/openai/chat_completions/chat_completions.py,sha256=QBWab1fn2LXVDMtc6li3gOzmrNzDiUw5WUJsMeeMZII,5076
|
268
269
|
letta/server/rest_api/routers/v1/__init__.py,sha256=_skmAcDOK9ovHKfywRaBgigo3IvPmnUSQSR2hGVCOhY,1664
|
269
|
-
letta/server/rest_api/routers/v1/agents.py,sha256=
|
270
|
-
letta/server/rest_api/routers/v1/blocks.py,sha256=
|
270
|
+
letta/server/rest_api/routers/v1/agents.py,sha256=EM0bpwJ2Dt3cGIux89N1v5PKxJbooFkLO0IbSmR7FOI,35716
|
271
|
+
letta/server/rest_api/routers/v1/blocks.py,sha256=Q0ZWhKiW1lOZmLLXy0IrJpTQ-Hp03psOWdfjJR2uoxI,4645
|
271
272
|
letta/server/rest_api/routers/v1/embeddings.py,sha256=P-Dvt_HNKoTyjRwkScAMg1hlB3cNxMeAQwV7bSatsKI,957
|
272
|
-
letta/server/rest_api/routers/v1/groups.py,sha256=
|
273
|
+
letta/server/rest_api/routers/v1/groups.py,sha256=DT2tc4wwiq_gzmxefltEIrFSoqOntzhvmgqQy23varA,10738
|
273
274
|
letta/server/rest_api/routers/v1/health.py,sha256=MoOjkydhGcJXTiuJrKIB0etVXiRMdTa51S8RQ8-50DQ,399
|
274
275
|
letta/server/rest_api/routers/v1/identities.py,sha256=fvp-0cwvb4iX1fUGPkL--9nq8YD3tIE47kYRxUgOlp4,7462
|
275
276
|
letta/server/rest_api/routers/v1/jobs.py,sha256=4oeJfI2odNGubU_g7WSORJhn_usFsbRaD-qm86rve1E,2746
|
276
277
|
letta/server/rest_api/routers/v1/llms.py,sha256=PZWNHq7QuKj71HzOIzNwLWgATqDQo54K26zzg9dLom0,1683
|
277
|
-
letta/server/rest_api/routers/v1/messages.py,sha256=
|
278
|
+
letta/server/rest_api/routers/v1/messages.py,sha256=JvszNvPIe9mArExNInmJkcX33WInMbS5Vlds1eLqkjc,7850
|
278
279
|
letta/server/rest_api/routers/v1/organizations.py,sha256=r7rj-cA3shgAgM0b2JCMqjYsDIFv3ruZjU7SYbPGGqg,2831
|
279
280
|
letta/server/rest_api/routers/v1/providers.py,sha256=qp6XT20tcZac64XDGF2QUyLhselnShrRcTDQBHExEbQ,4322
|
280
|
-
letta/server/rest_api/routers/v1/runs.py,sha256=
|
281
|
+
letta/server/rest_api/routers/v1/runs.py,sha256=rq-k5kYN0On7VBNSzoPJxZcBf13hZFaDx0IUJJ04_K8,8875
|
281
282
|
letta/server/rest_api/routers/v1/sandbox_configs.py,sha256=9hqnnMwJ3wCwO-Bezu3Xl8i3TDSIuInw3gSeHaKUXfE,8526
|
282
283
|
letta/server/rest_api/routers/v1/sources.py,sha256=cNDIckY1zqKUeB9xKg6jIoi-cePzyIew-OHMGeQvyqE,11222
|
283
284
|
letta/server/rest_api/routers/v1/steps.py,sha256=ra7ttm7HDs3N52M6s80XdpwiSMTLyf776_SmEILWDvo,3276
|
284
285
|
letta/server/rest_api/routers/v1/tags.py,sha256=coydgvL6-9cuG2Hy5Ea7QY3inhTHlsf69w0tcZenBus,880
|
285
|
-
letta/server/rest_api/routers/v1/tools.py,sha256=
|
286
|
-
letta/server/rest_api/routers/v1/users.py,sha256=
|
287
|
-
letta/server/rest_api/routers/v1/voice.py,sha256=
|
286
|
+
letta/server/rest_api/routers/v1/tools.py,sha256=IyGvDTRDY6UyadcmszqsprtwXKkAnA1C4fJyWCtRlfs,19588
|
287
|
+
letta/server/rest_api/routers/v1/users.py,sha256=a0J3Ad8kWHxi3vUJB5r9K2GmiplSABZXwhA83o8HbpI,2367
|
288
|
+
letta/server/rest_api/routers/v1/voice.py,sha256=NZa7ksEqXTWSqh7CqmbVMClO7wOmrqlRnSqFi6Qh-WM,1949
|
288
289
|
letta/server/rest_api/static_files.py,sha256=NG8sN4Z5EJ8JVQdj19tkFa9iQ1kBPTab9f_CUxd_u4Q,3143
|
289
290
|
letta/server/rest_api/utils.py,sha256=n5ZwtCtF3Oa4b9NFQ8l9f13v4eOI4mWdWNQqFp5d3A0,16516
|
290
291
|
letta/server/server.py,sha256=ccbYv9c6xZNbLU3JQ7bgBthPvbl61fodubHDNRsnJxI,89795
|
@@ -301,7 +302,7 @@ letta/server/ws_api/interface.py,sha256=TWl9vkcMCnLsUtgsuENZ-ku2oMDA-OUTzLh_yNRo
|
|
301
302
|
letta/server/ws_api/protocol.py,sha256=5mDgpfNZn_kNwHnpt5Dsuw8gdNH298sgxTGed3etzYg,1836
|
302
303
|
letta/server/ws_api/server.py,sha256=cBSzf-V4zT1bL_0i54OTI3cMXhTIIxqjSRF8pYjk7fg,5835
|
303
304
|
letta/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
304
|
-
letta/services/agent_manager.py,sha256=
|
305
|
+
letta/services/agent_manager.py,sha256=bcQRrWjECZCoIUD67LLh3XO7AY-9tRDfOY5d8OgmVNg,91590
|
305
306
|
letta/services/block_manager.py,sha256=VxiDoshWbM27HQH9AqGQc4x1tsgjz8Csm-TBYiLy3IE,20779
|
306
307
|
letta/services/group_manager.py,sha256=X5Z-0j9h95H5p3kKo8m5FZbl0HFN1slxFvcph7fTdvc,15833
|
307
308
|
letta/services/helpers/agent_manager_helper.py,sha256=q7GfVgKI-e8k0BZS-V_PuUCjK-PYciZDoig_sYHi_Go,21334
|
@@ -314,9 +315,9 @@ letta/services/mcp/base_client.py,sha256=YoRb9eKKTGaLxaMVtuH5UcC74iXyWlcyYbC5xOe
|
|
314
315
|
letta/services/mcp/sse_client.py,sha256=Vj0AgaadgMnpFQOWkSoPfeOI00ZvURMf3TIU7fv_DN8,1012
|
315
316
|
letta/services/mcp/stdio_client.py,sha256=wdPzTqSRkibjt9pXhwi0Nul_z_cTAPim-OHjLc__yBE,925
|
316
317
|
letta/services/mcp/types.py,sha256=nmcnQn2EpxXzXg5_pWPsHZobfxO6OucaUgz1bVvam7o,1411
|
317
|
-
letta/services/message_manager.py,sha256=
|
318
|
+
letta/services/message_manager.py,sha256=JENrzGpPIGX9STMnqktmbGX9C8ttI9MthQWlea3JphM,21274
|
318
319
|
letta/services/organization_manager.py,sha256=Z87kY22pWm6yOmPJCsMUVQmu0kaxyK8WGKkyYaRM2sU,3760
|
319
|
-
letta/services/passage_manager.py,sha256=
|
320
|
+
letta/services/passage_manager.py,sha256=6-mVw6C1TWFJuaoE1CE8xaaiCPJLptfTb3k9XbhxGPQ,10419
|
320
321
|
letta/services/per_agent_lock_manager.py,sha256=porM0cKKANQ1FvcGXOO_qM7ARk5Fgi1HVEAhXsAg9-4,546
|
321
322
|
letta/services/provider_manager.py,sha256=l5gfCLMQ5imSoS1xT-6uFqNWEHBqXEriy6VRNkuJZ80,4758
|
322
323
|
letta/services/sandbox_config_manager.py,sha256=tN6TYyOSNOZ3daX2QdbJcyGtUvczQc3-YZ-7dn5yLyE,13300
|
@@ -334,7 +335,7 @@ letta/services/tool_sandbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
|
|
334
335
|
letta/services/tool_sandbox/base.py,sha256=pUnPFkEg9I5ktMuT4AOOxbTnTmZTGcTA2phLe1H1EdY,8306
|
335
336
|
letta/services/tool_sandbox/e2b_sandbox.py,sha256=umsXfolzM_j67izswECDdVfnlcm03wLpMoZtS6SZ0sc,6147
|
336
337
|
letta/services/tool_sandbox/local_sandbox.py,sha256=ksbraC-zcMWt3vS7kSi98uWI9L73I0h73rMayhuTWsw,10474
|
337
|
-
letta/services/user_manager.py,sha256=
|
338
|
+
letta/services/user_manager.py,sha256=fDtPq8q2_LrIPHyn4zyx0orrCqKlpZRoqPU_IIaiBBc,7549
|
338
339
|
letta/settings.py,sha256=h0d3tN3W3dEri5xlBthGwDUQBwaz_oZopy2vwRiILXA,8770
|
339
340
|
letta/streaming_interface.py,sha256=c-T7zoMTXGXFwDWJJXrv7UypeMPXwPOmNHeuuh0b9zk,16398
|
340
341
|
letta/streaming_utils.py,sha256=jLqFTVhUL76FeOuYk8TaRQHmPTf3HSRc2EoJwxJNK6U,11946
|
@@ -342,8 +343,8 @@ letta/system.py,sha256=mKxmvvekuP8mdgsebRINGBoFbUdJhxLJ260crPBNVyk,8386
|
|
342
343
|
letta/tracing.py,sha256=j9uyBbx02erQZ307XmZmZSNyzQt-d7ZDB7vhFhjDlsU,8448
|
343
344
|
letta/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
344
345
|
letta/utils.py,sha256=W8J1FfhRADFqoyx3J8-Z1_aWyG433PBoEh_b5wdOZIg,32262
|
345
|
-
letta_nightly-0.7.
|
346
|
-
letta_nightly-0.7.
|
347
|
-
letta_nightly-0.7.
|
348
|
-
letta_nightly-0.7.
|
349
|
-
letta_nightly-0.7.
|
346
|
+
letta_nightly-0.7.17.dev20250516090339.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
|
347
|
+
letta_nightly-0.7.17.dev20250516090339.dist-info/METADATA,sha256=fnOCAqQ2y95z92UXfHUiu32N_8UF-Zr6AExf_cqf7TI,22274
|
348
|
+
letta_nightly-0.7.17.dev20250516090339.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
349
|
+
letta_nightly-0.7.17.dev20250516090339.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
|
350
|
+
letta_nightly-0.7.17.dev20250516090339.dist-info/RECORD,,
|
File without changes
|
File without changes
|