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.
Files changed (138) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +10 -14
  3. letta/agents/base_agent.py +18 -0
  4. letta/agents/helpers.py +32 -7
  5. letta/agents/letta_agent.py +953 -762
  6. letta/agents/voice_agent.py +1 -1
  7. letta/client/streaming.py +0 -1
  8. letta/constants.py +11 -8
  9. letta/errors.py +9 -0
  10. letta/functions/function_sets/base.py +77 -69
  11. letta/functions/function_sets/builtin.py +41 -22
  12. letta/functions/function_sets/multi_agent.py +1 -2
  13. letta/functions/schema_generator.py +0 -1
  14. letta/helpers/converters.py +8 -3
  15. letta/helpers/datetime_helpers.py +5 -4
  16. letta/helpers/message_helper.py +1 -2
  17. letta/helpers/pinecone_utils.py +0 -1
  18. letta/helpers/tool_rule_solver.py +10 -0
  19. letta/helpers/tpuf_client.py +848 -0
  20. letta/interface.py +8 -8
  21. letta/interfaces/anthropic_streaming_interface.py +7 -0
  22. letta/interfaces/openai_streaming_interface.py +29 -6
  23. letta/llm_api/anthropic_client.py +188 -18
  24. letta/llm_api/azure_client.py +0 -1
  25. letta/llm_api/bedrock_client.py +1 -2
  26. letta/llm_api/deepseek_client.py +319 -5
  27. letta/llm_api/google_vertex_client.py +75 -17
  28. letta/llm_api/groq_client.py +0 -1
  29. letta/llm_api/helpers.py +2 -2
  30. letta/llm_api/llm_api_tools.py +1 -50
  31. letta/llm_api/llm_client.py +6 -8
  32. letta/llm_api/mistral.py +1 -1
  33. letta/llm_api/openai.py +16 -13
  34. letta/llm_api/openai_client.py +31 -16
  35. letta/llm_api/together_client.py +0 -1
  36. letta/llm_api/xai_client.py +0 -1
  37. letta/local_llm/chat_completion_proxy.py +7 -6
  38. letta/local_llm/settings/settings.py +1 -1
  39. letta/orm/__init__.py +1 -0
  40. letta/orm/agent.py +8 -6
  41. letta/orm/archive.py +9 -1
  42. letta/orm/block.py +3 -4
  43. letta/orm/block_history.py +3 -1
  44. letta/orm/group.py +2 -3
  45. letta/orm/identity.py +1 -2
  46. letta/orm/job.py +1 -2
  47. letta/orm/llm_batch_items.py +1 -2
  48. letta/orm/message.py +8 -4
  49. letta/orm/mixins.py +18 -0
  50. letta/orm/organization.py +2 -0
  51. letta/orm/passage.py +8 -1
  52. letta/orm/passage_tag.py +55 -0
  53. letta/orm/sandbox_config.py +1 -3
  54. letta/orm/step.py +1 -2
  55. letta/orm/tool.py +1 -0
  56. letta/otel/resource.py +2 -2
  57. letta/plugins/plugins.py +1 -1
  58. letta/prompts/prompt_generator.py +10 -2
  59. letta/schemas/agent.py +11 -0
  60. letta/schemas/archive.py +4 -0
  61. letta/schemas/block.py +13 -0
  62. letta/schemas/embedding_config.py +0 -1
  63. letta/schemas/enums.py +24 -7
  64. letta/schemas/group.py +12 -0
  65. letta/schemas/letta_message.py +55 -1
  66. letta/schemas/letta_message_content.py +28 -0
  67. letta/schemas/letta_request.py +21 -4
  68. letta/schemas/letta_stop_reason.py +9 -1
  69. letta/schemas/llm_config.py +24 -8
  70. letta/schemas/mcp.py +0 -3
  71. letta/schemas/memory.py +14 -0
  72. letta/schemas/message.py +245 -141
  73. letta/schemas/openai/chat_completion_request.py +2 -1
  74. letta/schemas/passage.py +1 -0
  75. letta/schemas/providers/bedrock.py +1 -1
  76. letta/schemas/providers/openai.py +2 -2
  77. letta/schemas/tool.py +11 -5
  78. letta/schemas/tool_execution_result.py +0 -1
  79. letta/schemas/tool_rule.py +71 -0
  80. letta/serialize_schemas/marshmallow_agent.py +1 -2
  81. letta/server/rest_api/app.py +3 -3
  82. letta/server/rest_api/auth/index.py +0 -1
  83. letta/server/rest_api/interface.py +3 -11
  84. letta/server/rest_api/redis_stream_manager.py +3 -4
  85. letta/server/rest_api/routers/v1/agents.py +143 -84
  86. letta/server/rest_api/routers/v1/blocks.py +1 -1
  87. letta/server/rest_api/routers/v1/folders.py +1 -1
  88. letta/server/rest_api/routers/v1/groups.py +23 -22
  89. letta/server/rest_api/routers/v1/internal_templates.py +68 -0
  90. letta/server/rest_api/routers/v1/sandbox_configs.py +11 -5
  91. letta/server/rest_api/routers/v1/sources.py +1 -1
  92. letta/server/rest_api/routers/v1/tools.py +167 -15
  93. letta/server/rest_api/streaming_response.py +4 -3
  94. letta/server/rest_api/utils.py +75 -18
  95. letta/server/server.py +24 -35
  96. letta/services/agent_manager.py +359 -45
  97. letta/services/agent_serialization_manager.py +23 -3
  98. letta/services/archive_manager.py +72 -3
  99. letta/services/block_manager.py +1 -2
  100. letta/services/context_window_calculator/token_counter.py +11 -6
  101. letta/services/file_manager.py +1 -3
  102. letta/services/files_agents_manager.py +2 -4
  103. letta/services/group_manager.py +73 -12
  104. letta/services/helpers/agent_manager_helper.py +5 -5
  105. letta/services/identity_manager.py +8 -3
  106. letta/services/job_manager.py +2 -14
  107. letta/services/llm_batch_manager.py +1 -3
  108. letta/services/mcp/base_client.py +1 -2
  109. letta/services/mcp_manager.py +5 -6
  110. letta/services/message_manager.py +536 -15
  111. letta/services/organization_manager.py +1 -2
  112. letta/services/passage_manager.py +287 -12
  113. letta/services/provider_manager.py +1 -3
  114. letta/services/sandbox_config_manager.py +12 -7
  115. letta/services/source_manager.py +1 -2
  116. letta/services/step_manager.py +0 -1
  117. letta/services/summarizer/summarizer.py +4 -2
  118. letta/services/telemetry_manager.py +1 -3
  119. letta/services/tool_executor/builtin_tool_executor.py +136 -316
  120. letta/services/tool_executor/core_tool_executor.py +231 -74
  121. letta/services/tool_executor/files_tool_executor.py +2 -2
  122. letta/services/tool_executor/mcp_tool_executor.py +0 -1
  123. letta/services/tool_executor/multi_agent_tool_executor.py +2 -2
  124. letta/services/tool_executor/sandbox_tool_executor.py +0 -1
  125. letta/services/tool_executor/tool_execution_sandbox.py +2 -3
  126. letta/services/tool_manager.py +181 -64
  127. letta/services/tool_sandbox/modal_deployment_manager.py +2 -2
  128. letta/services/user_manager.py +1 -2
  129. letta/settings.py +5 -3
  130. letta/streaming_interface.py +3 -3
  131. letta/system.py +1 -1
  132. letta/utils.py +0 -1
  133. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/METADATA +11 -7
  134. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/RECORD +137 -135
  135. letta/llm_api/deepseek.py +0 -303
  136. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/WHEEL +0 -0
  137. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/entry_points.txt +0 -0
  138. {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