letta-nightly 0.11.6.dev20250902104140__py3-none-any.whl → 0.11.7.dev20250904045700__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/agent.py +10 -14
- letta/agents/base_agent.py +18 -0
- letta/agents/helpers.py +32 -7
- letta/agents/letta_agent.py +953 -762
- letta/agents/voice_agent.py +1 -1
- letta/client/streaming.py +0 -1
- letta/constants.py +11 -8
- letta/errors.py +9 -0
- letta/functions/function_sets/base.py +77 -69
- letta/functions/function_sets/builtin.py +41 -22
- letta/functions/function_sets/multi_agent.py +1 -2
- letta/functions/schema_generator.py +0 -1
- letta/helpers/converters.py +8 -3
- letta/helpers/datetime_helpers.py +5 -4
- letta/helpers/message_helper.py +1 -2
- letta/helpers/pinecone_utils.py +0 -1
- letta/helpers/tool_rule_solver.py +10 -0
- letta/helpers/tpuf_client.py +848 -0
- letta/interface.py +8 -8
- letta/interfaces/anthropic_streaming_interface.py +7 -0
- letta/interfaces/openai_streaming_interface.py +29 -6
- letta/llm_api/anthropic_client.py +188 -18
- letta/llm_api/azure_client.py +0 -1
- letta/llm_api/bedrock_client.py +1 -2
- letta/llm_api/deepseek_client.py +319 -5
- letta/llm_api/google_vertex_client.py +75 -17
- letta/llm_api/groq_client.py +0 -1
- letta/llm_api/helpers.py +2 -2
- letta/llm_api/llm_api_tools.py +1 -50
- letta/llm_api/llm_client.py +6 -8
- letta/llm_api/mistral.py +1 -1
- letta/llm_api/openai.py +16 -13
- letta/llm_api/openai_client.py +31 -16
- letta/llm_api/together_client.py +0 -1
- letta/llm_api/xai_client.py +0 -1
- letta/local_llm/chat_completion_proxy.py +7 -6
- letta/local_llm/settings/settings.py +1 -1
- letta/orm/__init__.py +1 -0
- letta/orm/agent.py +8 -6
- letta/orm/archive.py +9 -1
- letta/orm/block.py +3 -4
- letta/orm/block_history.py +3 -1
- letta/orm/group.py +2 -3
- letta/orm/identity.py +1 -2
- letta/orm/job.py +1 -2
- letta/orm/llm_batch_items.py +1 -2
- letta/orm/message.py +8 -4
- letta/orm/mixins.py +18 -0
- letta/orm/organization.py +2 -0
- letta/orm/passage.py +8 -1
- letta/orm/passage_tag.py +55 -0
- letta/orm/sandbox_config.py +1 -3
- letta/orm/step.py +1 -2
- letta/orm/tool.py +1 -0
- letta/otel/resource.py +2 -2
- letta/plugins/plugins.py +1 -1
- letta/prompts/prompt_generator.py +10 -2
- letta/schemas/agent.py +11 -0
- letta/schemas/archive.py +4 -0
- letta/schemas/block.py +13 -0
- letta/schemas/embedding_config.py +0 -1
- letta/schemas/enums.py +24 -7
- letta/schemas/group.py +12 -0
- letta/schemas/letta_message.py +55 -1
- letta/schemas/letta_message_content.py +28 -0
- letta/schemas/letta_request.py +21 -4
- letta/schemas/letta_stop_reason.py +9 -1
- letta/schemas/llm_config.py +24 -8
- letta/schemas/mcp.py +0 -3
- letta/schemas/memory.py +14 -0
- letta/schemas/message.py +245 -141
- letta/schemas/openai/chat_completion_request.py +2 -1
- letta/schemas/passage.py +1 -0
- letta/schemas/providers/bedrock.py +1 -1
- letta/schemas/providers/openai.py +2 -2
- letta/schemas/tool.py +11 -5
- letta/schemas/tool_execution_result.py +0 -1
- letta/schemas/tool_rule.py +71 -0
- letta/serialize_schemas/marshmallow_agent.py +1 -2
- letta/server/rest_api/app.py +3 -3
- letta/server/rest_api/auth/index.py +0 -1
- letta/server/rest_api/interface.py +3 -11
- letta/server/rest_api/redis_stream_manager.py +3 -4
- letta/server/rest_api/routers/v1/agents.py +143 -84
- letta/server/rest_api/routers/v1/blocks.py +1 -1
- letta/server/rest_api/routers/v1/folders.py +1 -1
- letta/server/rest_api/routers/v1/groups.py +23 -22
- letta/server/rest_api/routers/v1/internal_templates.py +68 -0
- letta/server/rest_api/routers/v1/sandbox_configs.py +11 -5
- letta/server/rest_api/routers/v1/sources.py +1 -1
- letta/server/rest_api/routers/v1/tools.py +167 -15
- letta/server/rest_api/streaming_response.py +4 -3
- letta/server/rest_api/utils.py +75 -18
- letta/server/server.py +24 -35
- letta/services/agent_manager.py +359 -45
- letta/services/agent_serialization_manager.py +23 -3
- letta/services/archive_manager.py +72 -3
- letta/services/block_manager.py +1 -2
- letta/services/context_window_calculator/token_counter.py +11 -6
- letta/services/file_manager.py +1 -3
- letta/services/files_agents_manager.py +2 -4
- letta/services/group_manager.py +73 -12
- letta/services/helpers/agent_manager_helper.py +5 -5
- letta/services/identity_manager.py +8 -3
- letta/services/job_manager.py +2 -14
- letta/services/llm_batch_manager.py +1 -3
- letta/services/mcp/base_client.py +1 -2
- letta/services/mcp_manager.py +5 -6
- letta/services/message_manager.py +536 -15
- letta/services/organization_manager.py +1 -2
- letta/services/passage_manager.py +287 -12
- letta/services/provider_manager.py +1 -3
- letta/services/sandbox_config_manager.py +12 -7
- letta/services/source_manager.py +1 -2
- letta/services/step_manager.py +0 -1
- letta/services/summarizer/summarizer.py +4 -2
- letta/services/telemetry_manager.py +1 -3
- letta/services/tool_executor/builtin_tool_executor.py +136 -316
- letta/services/tool_executor/core_tool_executor.py +231 -74
- letta/services/tool_executor/files_tool_executor.py +2 -2
- letta/services/tool_executor/mcp_tool_executor.py +0 -1
- letta/services/tool_executor/multi_agent_tool_executor.py +2 -2
- letta/services/tool_executor/sandbox_tool_executor.py +0 -1
- letta/services/tool_executor/tool_execution_sandbox.py +2 -3
- letta/services/tool_manager.py +181 -64
- letta/services/tool_sandbox/modal_deployment_manager.py +2 -2
- letta/services/user_manager.py +1 -2
- letta/settings.py +5 -3
- letta/streaming_interface.py +3 -3
- letta/system.py +1 -1
- letta/utils.py +0 -1
- {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/METADATA +11 -7
- {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/RECORD +137 -135
- letta/llm_api/deepseek.py +0 -303
- {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/licenses/LICENSE +0 -0
letta/llm_api/deepseek.py
DELETED
@@ -1,303 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import re
|
3
|
-
import warnings
|
4
|
-
from typing import List, Optional
|
5
|
-
|
6
|
-
from letta.schemas.llm_config import LLMConfig
|
7
|
-
from letta.schemas.message import Message as _Message
|
8
|
-
from letta.schemas.openai.chat_completion_request import AssistantMessage, ChatCompletionRequest, ChatMessage
|
9
|
-
from letta.schemas.openai.chat_completion_request import FunctionCall as ToolFunctionChoiceFunctionCall
|
10
|
-
from letta.schemas.openai.chat_completion_request import Tool, ToolFunctionChoice, ToolMessage, UserMessage, cast_message_to_subtype
|
11
|
-
from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
|
12
|
-
from letta.schemas.openai.openai import Function, ToolCall
|
13
|
-
from letta.utils import get_tool_call_id
|
14
|
-
|
15
|
-
|
16
|
-
def merge_tool_message(previous_message: ChatMessage, tool_message: ToolMessage) -> ChatMessage:
|
17
|
-
"""
|
18
|
-
Merge `ToolMessage` objects into the previous message.
|
19
|
-
"""
|
20
|
-
previous_message.content += (
|
21
|
-
f"<ToolMessage> content: {tool_message.content}, role: {tool_message.role}, tool_call_id: {tool_message.tool_call_id}</ToolMessage>"
|
22
|
-
)
|
23
|
-
return previous_message
|
24
|
-
|
25
|
-
|
26
|
-
def handle_assistant_message(assistant_message: AssistantMessage) -> AssistantMessage:
|
27
|
-
"""
|
28
|
-
For `AssistantMessage` objects, remove the `tool_calls` field and add them to the `content` field.
|
29
|
-
"""
|
30
|
-
|
31
|
-
if "tool_calls" in assistant_message.dict().keys():
|
32
|
-
assistant_message.content = "".join(
|
33
|
-
[
|
34
|
-
# f"<ToolCall> name: {tool_call.function.name}, function: {tool_call.function}</ToolCall>"
|
35
|
-
f"<ToolCall> {json.dumps(tool_call.function.dict())} </ToolCall>"
|
36
|
-
for tool_call in assistant_message.tool_calls
|
37
|
-
]
|
38
|
-
)
|
39
|
-
del assistant_message.tool_calls
|
40
|
-
return assistant_message
|
41
|
-
|
42
|
-
|
43
|
-
def map_messages_to_deepseek_format(messages: List[ChatMessage]) -> List[_Message]:
|
44
|
-
"""
|
45
|
-
Deepeek API has the following constraints: messages must be interleaved between user and assistant messages, ending on a user message.
|
46
|
-
Tools are currently unstable for V3 and not supported for R1 in the API: https://api-docs.deepseek.com/guides/function_calling.
|
47
|
-
|
48
|
-
This function merges ToolMessages into AssistantMessages and removes ToolCalls from AssistantMessages, and adds a dummy user message
|
49
|
-
at the end.
|
50
|
-
|
51
|
-
"""
|
52
|
-
deepseek_messages = []
|
53
|
-
for idx, message in enumerate(messages):
|
54
|
-
# First message is the system prompt, add it
|
55
|
-
if idx == 0 and message.role == "system":
|
56
|
-
deepseek_messages.append(message)
|
57
|
-
continue
|
58
|
-
if message.role == "user":
|
59
|
-
if deepseek_messages[-1].role == "assistant" or deepseek_messages[-1].role == "system":
|
60
|
-
# User message, add it
|
61
|
-
deepseek_messages.append(UserMessage(content=message.content))
|
62
|
-
else:
|
63
|
-
# add to the content of the previous message
|
64
|
-
deepseek_messages[-1].content += message.content
|
65
|
-
elif message.role == "assistant":
|
66
|
-
if deepseek_messages[-1].role == "user":
|
67
|
-
# Assistant message, remove tool calls and add them to the content
|
68
|
-
deepseek_messages.append(handle_assistant_message(message))
|
69
|
-
else:
|
70
|
-
# add to the content of the previous message
|
71
|
-
deepseek_messages[-1].content += message.content
|
72
|
-
elif message.role == "tool" and deepseek_messages[-1].role == "assistant":
|
73
|
-
# Tool message, add it to the last assistant message
|
74
|
-
merged_message = merge_tool_message(deepseek_messages[-1], message)
|
75
|
-
deepseek_messages[-1] = merged_message
|
76
|
-
else:
|
77
|
-
print(f"Skipping message: {message}")
|
78
|
-
|
79
|
-
# This needs to end on a user message, add a dummy message if the last was assistant
|
80
|
-
if deepseek_messages[-1].role == "assistant":
|
81
|
-
deepseek_messages.append(UserMessage(content=""))
|
82
|
-
return deepseek_messages
|
83
|
-
|
84
|
-
|
85
|
-
def build_deepseek_chat_completions_request(
|
86
|
-
llm_config: LLMConfig,
|
87
|
-
messages: List[_Message],
|
88
|
-
user_id: Optional[str],
|
89
|
-
functions: Optional[list],
|
90
|
-
function_call: Optional[str],
|
91
|
-
use_tool_naming: bool,
|
92
|
-
max_tokens: Optional[int],
|
93
|
-
) -> ChatCompletionRequest:
|
94
|
-
# if functions and llm_config.put_inner_thoughts_in_kwargs:
|
95
|
-
# # Special case for LM Studio backend since it needs extra guidance to force out the thoughts first
|
96
|
-
# # TODO(fix)
|
97
|
-
# inner_thoughts_desc = (
|
98
|
-
# INNER_THOUGHTS_KWARG_DESCRIPTION_GO_FIRST if ":1234" in llm_config.model_endpoint else INNER_THOUGHTS_KWARG_DESCRIPTION
|
99
|
-
# )
|
100
|
-
# functions = add_inner_thoughts_to_functions(
|
101
|
-
# functions=functions,
|
102
|
-
# inner_thoughts_key=INNER_THOUGHTS_KWARG,
|
103
|
-
# inner_thoughts_description=inner_thoughts_desc,
|
104
|
-
# )
|
105
|
-
|
106
|
-
openai_message_list = [cast_message_to_subtype(m.to_openai_dict(put_inner_thoughts_in_kwargs=False)) for m in messages]
|
107
|
-
|
108
|
-
if llm_config.model:
|
109
|
-
model = llm_config.model
|
110
|
-
else:
|
111
|
-
warnings.warn(f"Model type not set in llm_config: {llm_config.model_dump_json(indent=4)}")
|
112
|
-
model = None
|
113
|
-
if use_tool_naming:
|
114
|
-
if function_call is None:
|
115
|
-
tool_choice = None
|
116
|
-
elif function_call not in ["none", "auto", "required"]:
|
117
|
-
tool_choice = ToolFunctionChoice(type="function", function=ToolFunctionChoiceFunctionCall(name=function_call))
|
118
|
-
else:
|
119
|
-
tool_choice = function_call
|
120
|
-
|
121
|
-
def add_functions_to_system_message(system_message: ChatMessage):
|
122
|
-
system_message.content += f"<available functions> {''.join(json.dumps(f) for f in functions)} </available functions>"
|
123
|
-
system_message.content += 'Select best function to call simply respond with a single json block with the fields "name" and "arguments". Use double quotes around the arguments.'
|
124
|
-
|
125
|
-
if llm_config.model == "deepseek-reasoner": # R1 currently doesn't support function calling natively
|
126
|
-
add_functions_to_system_message(
|
127
|
-
openai_message_list[0]
|
128
|
-
) # Inject additional instructions to the system prompt with the available functions
|
129
|
-
|
130
|
-
openai_message_list = map_messages_to_deepseek_format(openai_message_list)
|
131
|
-
|
132
|
-
data = ChatCompletionRequest(
|
133
|
-
model=model,
|
134
|
-
messages=openai_message_list,
|
135
|
-
user=str(user_id),
|
136
|
-
max_completion_tokens=max_tokens,
|
137
|
-
temperature=llm_config.temperature,
|
138
|
-
)
|
139
|
-
else:
|
140
|
-
data = ChatCompletionRequest(
|
141
|
-
model=model,
|
142
|
-
messages=openai_message_list,
|
143
|
-
tools=[Tool(type="function", function=f) for f in functions] if functions else None,
|
144
|
-
tool_choice=tool_choice,
|
145
|
-
user=str(user_id),
|
146
|
-
max_completion_tokens=max_tokens,
|
147
|
-
temperature=llm_config.temperature,
|
148
|
-
)
|
149
|
-
else:
|
150
|
-
data = ChatCompletionRequest(
|
151
|
-
model=model,
|
152
|
-
messages=openai_message_list,
|
153
|
-
functions=functions,
|
154
|
-
function_call=function_call,
|
155
|
-
user=str(user_id),
|
156
|
-
max_completion_tokens=max_tokens,
|
157
|
-
temperature=llm_config.temperature,
|
158
|
-
)
|
159
|
-
|
160
|
-
return data
|
161
|
-
|
162
|
-
|
163
|
-
def convert_deepseek_response_to_chatcompletion(
|
164
|
-
response: ChatCompletionResponse,
|
165
|
-
) -> ChatCompletionResponse:
|
166
|
-
"""
|
167
|
-
Example response from DeepSeek:
|
168
|
-
|
169
|
-
ChatCompletion(
|
170
|
-
id='bc7f7d25-82e4-443a-b217-dfad2b66da8e',
|
171
|
-
choices=[
|
172
|
-
Choice(
|
173
|
-
finish_reason='stop',
|
174
|
-
index=0,
|
175
|
-
logprobs=None,
|
176
|
-
message=ChatCompletionMessage(
|
177
|
-
content='{"function": "send_message", "arguments": {"message": "Hey! Whales are such majestic creatures, aren\'t they? How\'s your day going? 🌊 "}}',
|
178
|
-
refusal=None,
|
179
|
-
role='assistant',
|
180
|
-
audio=None,
|
181
|
-
function_call=None,
|
182
|
-
tool_calls=None,
|
183
|
-
reasoning_content='Okay, the user said "hello whales". Hmm, that\'s an interesting greeting. Maybe they meant "hello there" or are they actually talking about whales? Let me check if I misheard. Whales are fascinating creatures. I should respond in a friendly way. Let me ask them how they\'re doing and mention whales to keep the conversation going.'
|
184
|
-
)
|
185
|
-
)
|
186
|
-
],
|
187
|
-
created=1738266449,
|
188
|
-
model='deepseek-reasoner',
|
189
|
-
object='chat.completion',
|
190
|
-
service_tier=None,
|
191
|
-
system_fingerprint='fp_7e73fd9a08',
|
192
|
-
usage=CompletionUsage(
|
193
|
-
completion_tokens=111,
|
194
|
-
prompt_tokens=1270,
|
195
|
-
total_tokens=1381,
|
196
|
-
completion_tokens_details=CompletionTokensDetails(
|
197
|
-
accepted_prediction_tokens=None,
|
198
|
-
audio_tokens=None,
|
199
|
-
reasoning_tokens=72,
|
200
|
-
rejected_prediction_tokens=None
|
201
|
-
),
|
202
|
-
prompt_tokens_details=PromptTokensDetails(
|
203
|
-
audio_tokens=None,
|
204
|
-
cached_tokens=1088
|
205
|
-
),
|
206
|
-
prompt_cache_hit_tokens=1088,
|
207
|
-
prompt_cache_miss_tokens=182
|
208
|
-
)
|
209
|
-
)
|
210
|
-
"""
|
211
|
-
|
212
|
-
def convert_dict_quotes(input_dict: dict):
|
213
|
-
"""
|
214
|
-
Convert a dictionary with single-quoted keys to double-quoted keys,
|
215
|
-
properly handling boolean values and nested structures.
|
216
|
-
|
217
|
-
Args:
|
218
|
-
input_dict (dict): Input dictionary with single-quoted keys
|
219
|
-
|
220
|
-
Returns:
|
221
|
-
str: JSON string with double-quoted keys
|
222
|
-
"""
|
223
|
-
# First convert the dictionary to a JSON string to handle booleans properly
|
224
|
-
json_str = json.dumps(input_dict)
|
225
|
-
|
226
|
-
# Function to handle complex string replacements
|
227
|
-
def replace_quotes(match):
|
228
|
-
key = match.group(1)
|
229
|
-
# Escape any existing double quotes in the key
|
230
|
-
key = key.replace('"', '\\"')
|
231
|
-
return f'"{key}":'
|
232
|
-
|
233
|
-
# Replace single-quoted keys with double-quoted keys
|
234
|
-
# This regex looks for single-quoted keys followed by a colon
|
235
|
-
def strip_json_block(text):
|
236
|
-
# Check if text starts with ```json or similar
|
237
|
-
if text.strip().startswith("```"):
|
238
|
-
# Split by \n to remove the first and last lines
|
239
|
-
lines = text.split("\n")[1:-1]
|
240
|
-
return "\n".join(lines)
|
241
|
-
return text
|
242
|
-
|
243
|
-
pattern = r"'([^']*)':"
|
244
|
-
converted_str = re.sub(pattern, replace_quotes, strip_json_block(json_str))
|
245
|
-
|
246
|
-
# Parse the string back to ensure valid JSON format
|
247
|
-
try:
|
248
|
-
json.loads(converted_str)
|
249
|
-
return converted_str
|
250
|
-
except json.JSONDecodeError as e:
|
251
|
-
raise ValueError(f"Failed to create valid JSON with double quotes: {str(e)}")
|
252
|
-
|
253
|
-
def extract_json_block(text):
|
254
|
-
# Find the first {
|
255
|
-
start = text.find("{")
|
256
|
-
if start == -1:
|
257
|
-
return text
|
258
|
-
|
259
|
-
# Track nested braces to find the matching closing brace
|
260
|
-
brace_count = 0
|
261
|
-
end = start
|
262
|
-
|
263
|
-
for i in range(start, len(text)):
|
264
|
-
if text[i] == "{":
|
265
|
-
brace_count += 1
|
266
|
-
elif text[i] == "}":
|
267
|
-
brace_count -= 1
|
268
|
-
if brace_count == 0:
|
269
|
-
end = i + 1
|
270
|
-
break
|
271
|
-
|
272
|
-
return text[start:end]
|
273
|
-
|
274
|
-
content = response.choices[0].message.content
|
275
|
-
try:
|
276
|
-
content_dict = json.loads(extract_json_block(content))
|
277
|
-
|
278
|
-
if type(content_dict["arguments"]) == str:
|
279
|
-
content_dict["arguments"] = json.loads(content_dict["arguments"])
|
280
|
-
|
281
|
-
tool_calls = [
|
282
|
-
ToolCall(
|
283
|
-
id=get_tool_call_id(),
|
284
|
-
type="function",
|
285
|
-
function=Function(
|
286
|
-
name=content_dict["name"],
|
287
|
-
arguments=convert_dict_quotes(content_dict["arguments"]),
|
288
|
-
),
|
289
|
-
)
|
290
|
-
]
|
291
|
-
except (json.JSONDecodeError, TypeError, KeyError) as e:
|
292
|
-
print(e)
|
293
|
-
tool_calls = response.choices[0].message.tool_calls
|
294
|
-
raise ValueError(f"Failed to create valid JSON {content}")
|
295
|
-
|
296
|
-
# Move the "reasoning_content" into the "content" field
|
297
|
-
response.choices[0].message.content = response.choices[0].message.reasoning_content
|
298
|
-
response.choices[0].message.tool_calls = tool_calls
|
299
|
-
|
300
|
-
# Remove the "reasoning_content" field
|
301
|
-
response.choices[0].message.reasoning_content = None
|
302
|
-
|
303
|
-
return response
|
File without changes
|
File without changes
|