letta-nightly 0.6.43.dev20250323104014__py3-none-any.whl → 0.6.44.dev20250325050316__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

Files changed (42) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +106 -104
  3. letta/agents/voice_agent.py +1 -1
  4. letta/client/streaming.py +3 -1
  5. letta/functions/function_sets/base.py +2 -1
  6. letta/functions/function_sets/multi_agent.py +51 -40
  7. letta/functions/helpers.py +26 -22
  8. letta/helpers/message_helper.py +41 -0
  9. letta/llm_api/anthropic.py +150 -44
  10. letta/llm_api/aws_bedrock.py +5 -3
  11. letta/llm_api/azure_openai.py +0 -1
  12. letta/llm_api/llm_api_tools.py +4 -0
  13. letta/orm/organization.py +1 -0
  14. letta/orm/sqlalchemy_base.py +2 -4
  15. letta/schemas/agent.py +8 -0
  16. letta/schemas/letta_message.py +8 -4
  17. letta/schemas/llm_config.py +6 -0
  18. letta/schemas/message.py +143 -24
  19. letta/schemas/openai/chat_completion_response.py +5 -0
  20. letta/schemas/organization.py +7 -0
  21. letta/schemas/providers.py +17 -0
  22. letta/schemas/tool.py +5 -1
  23. letta/schemas/usage.py +5 -1
  24. letta/serialize_schemas/pydantic_agent_schema.py +1 -1
  25. letta/server/rest_api/interface.py +44 -7
  26. letta/server/rest_api/routers/v1/agents.py +13 -2
  27. letta/server/rest_api/routers/v1/organizations.py +19 -1
  28. letta/server/rest_api/utils.py +1 -1
  29. letta/server/server.py +49 -70
  30. letta/services/agent_manager.py +6 -2
  31. letta/services/helpers/agent_manager_helper.py +24 -38
  32. letta/services/message_manager.py +7 -6
  33. letta/services/organization_manager.py +13 -0
  34. letta/services/tool_execution_sandbox.py +5 -1
  35. letta/services/tool_executor/__init__.py +0 -0
  36. letta/services/tool_executor/tool_execution_manager.py +74 -0
  37. letta/services/tool_executor/tool_executor.py +380 -0
  38. {letta_nightly-0.6.43.dev20250323104014.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/METADATA +2 -3
  39. {letta_nightly-0.6.43.dev20250323104014.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/RECORD +42 -38
  40. {letta_nightly-0.6.43.dev20250323104014.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/LICENSE +0 -0
  41. {letta_nightly-0.6.43.dev20250323104014.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/WHEEL +0 -0
  42. {letta_nightly-0.6.43.dev20250323104014.dist-info → letta_nightly-0.6.44.dev20250325050316.dist-info}/entry_points.txt +0 -0
letta/schemas/message.py CHANGED
@@ -19,6 +19,7 @@ from letta.schemas.enums import MessageRole
19
19
  from letta.schemas.letta_base import OrmMetadataBase
20
20
  from letta.schemas.letta_message import (
21
21
  AssistantMessage,
22
+ HiddenReasoningMessage,
22
23
  LettaMessage,
23
24
  ReasoningMessage,
24
25
  SystemMessage,
@@ -27,7 +28,13 @@ from letta.schemas.letta_message import (
27
28
  ToolReturnMessage,
28
29
  UserMessage,
29
30
  )
30
- from letta.schemas.letta_message_content import LettaMessageContentUnion, TextContent, get_letta_message_content_union_str_json_schema
31
+ from letta.schemas.letta_message_content import (
32
+ LettaMessageContentUnion,
33
+ ReasoningContent,
34
+ RedactedReasoningContent,
35
+ TextContent,
36
+ get_letta_message_content_union_str_json_schema,
37
+ )
31
38
  from letta.system import unpack_message
32
39
 
33
40
 
@@ -206,23 +213,58 @@ class Message(BaseMessage):
206
213
  assistant_message_tool_kwarg: str = DEFAULT_MESSAGE_TOOL_KWARG,
207
214
  ) -> List[LettaMessage]:
208
215
  """Convert message object (in DB format) to the style used by the original Letta API"""
209
- if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
210
- text_content = self.content[0].text
211
- else:
212
- text_content = None
213
-
214
216
  messages = []
215
217
 
216
218
  if self.role == MessageRole.assistant:
217
- if text_content is not None:
218
- # This is type InnerThoughts
219
- messages.append(
220
- ReasoningMessage(
221
- id=self.id,
222
- date=self.created_at,
223
- reasoning=text_content,
219
+
220
+ # Handle reasoning
221
+ if self.content:
222
+ # Check for ReACT-style COT inside of TextContent
223
+ if len(self.content) == 1 and isinstance(self.content[0], TextContent):
224
+ messages.append(
225
+ ReasoningMessage(
226
+ id=self.id,
227
+ date=self.created_at,
228
+ reasoning=self.content[0].text,
229
+ )
224
230
  )
225
- )
231
+ # Otherwise, we may have a list of multiple types
232
+ else:
233
+ # TODO we can probably collapse these two cases into a single loop
234
+ for content_part in self.content:
235
+ if isinstance(content_part, TextContent):
236
+ # COT
237
+ messages.append(
238
+ ReasoningMessage(
239
+ id=self.id,
240
+ date=self.created_at,
241
+ reasoning=content_part.text,
242
+ )
243
+ )
244
+ elif isinstance(content_part, ReasoningContent):
245
+ # "native" COT
246
+ messages.append(
247
+ ReasoningMessage(
248
+ id=self.id,
249
+ date=self.created_at,
250
+ reasoning=content_part.reasoning,
251
+ source="reasoner_model", # TODO do we want to tag like this?
252
+ signature=content_part.signature,
253
+ )
254
+ )
255
+ elif isinstance(content_part, RedactedReasoningContent):
256
+ # "native" redacted/hidden COT
257
+ messages.append(
258
+ HiddenReasoningMessage(
259
+ id=self.id,
260
+ date=self.created_at,
261
+ state="redacted",
262
+ hidden_reasoning=content_part.data,
263
+ )
264
+ )
265
+ else:
266
+ warnings.warn(f"Unrecognized content part in assistant message: {content_part}")
267
+
226
268
  if self.tool_calls is not None:
227
269
  # This is type FunctionCall
228
270
  for tool_call in self.tool_calls:
@@ -264,7 +306,11 @@ class Message(BaseMessage):
264
306
  # "message": response_string,
265
307
  # "time": formatted_time,
266
308
  # }
267
- assert text_content is not None, self
309
+ if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
310
+ text_content = self.content[0].text
311
+ else:
312
+ raise ValueError(f"Invalid tool return (no text object on message): {self.content}")
313
+
268
314
  try:
269
315
  function_return = json.loads(text_content)
270
316
  status = function_return["status"]
@@ -292,7 +338,11 @@ class Message(BaseMessage):
292
338
  )
293
339
  elif self.role == MessageRole.user:
294
340
  # This is type UserMessage
295
- assert text_content is not None, self
341
+ if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
342
+ text_content = self.content[0].text
343
+ else:
344
+ raise ValueError(f"Invalid user message (no text object on message): {self.content}")
345
+
296
346
  message_str = unpack_message(text_content)
297
347
  messages.append(
298
348
  UserMessage(
@@ -303,7 +353,11 @@ class Message(BaseMessage):
303
353
  )
304
354
  elif self.role == MessageRole.system:
305
355
  # This is type SystemMessage
306
- assert text_content is not None, self
356
+ if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
357
+ text_content = self.content[0].text
358
+ else:
359
+ raise ValueError(f"Invalid system message (no text object on system): {self.content}")
360
+
307
361
  messages.append(
308
362
  SystemMessage(
309
363
  id=self.id,
@@ -335,6 +389,29 @@ class Message(BaseMessage):
335
389
  assert "role" in openai_message_dict, openai_message_dict
336
390
  assert "content" in openai_message_dict, openai_message_dict
337
391
 
392
+ # TODO(caren) implicit support for only non-parts/list content types
393
+ if openai_message_dict["content"] is not None and type(openai_message_dict["content"]) is not str:
394
+ raise ValueError(f"Invalid content type: {type(openai_message_dict['content'])}")
395
+ content = [TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else []
396
+
397
+ # TODO(caren) bad assumption here that "reasoning_content" always comes before "redacted_reasoning_content"
398
+ if "reasoning_content" in openai_message_dict and openai_message_dict["reasoning_content"]:
399
+ content.append(
400
+ ReasoningContent(
401
+ reasoning=openai_message_dict["reasoning_content"],
402
+ is_native=True,
403
+ signature=(
404
+ openai_message_dict["reasoning_content_signature"] if openai_message_dict["reasoning_content_signature"] else None
405
+ ),
406
+ ),
407
+ )
408
+ if "redacted_reasoning_content" in openai_message_dict and openai_message_dict["redacted_reasoning_content"]:
409
+ content.append(
410
+ RedactedReasoningContent(
411
+ data=openai_message_dict["redacted_reasoning_content"] if "redacted_reasoning_content" in openai_message_dict else None,
412
+ ),
413
+ )
414
+
338
415
  # If we're going from deprecated function form
339
416
  if openai_message_dict["role"] == "function":
340
417
  if not allow_functions_style:
@@ -348,7 +425,7 @@ class Message(BaseMessage):
348
425
  model=model,
349
426
  # standard fields expected in an OpenAI ChatCompletion message object
350
427
  role=MessageRole.tool, # NOTE
351
- content=[TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else [],
428
+ content=content,
352
429
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
353
430
  tool_calls=openai_message_dict["tool_calls"] if "tool_calls" in openai_message_dict else None,
354
431
  tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
@@ -362,7 +439,7 @@ class Message(BaseMessage):
362
439
  model=model,
363
440
  # standard fields expected in an OpenAI ChatCompletion message object
364
441
  role=MessageRole.tool, # NOTE
365
- content=[TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else [],
442
+ content=content,
366
443
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
367
444
  tool_calls=openai_message_dict["tool_calls"] if "tool_calls" in openai_message_dict else None,
368
445
  tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
@@ -395,7 +472,7 @@ class Message(BaseMessage):
395
472
  model=model,
396
473
  # standard fields expected in an OpenAI ChatCompletion message object
397
474
  role=MessageRole(openai_message_dict["role"]),
398
- content=[TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else [],
475
+ content=content,
399
476
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
400
477
  tool_calls=tool_calls,
401
478
  tool_call_id=None, # NOTE: None, since this field is only non-null for role=='tool'
@@ -409,7 +486,7 @@ class Message(BaseMessage):
409
486
  model=model,
410
487
  # standard fields expected in an OpenAI ChatCompletion message object
411
488
  role=MessageRole(openai_message_dict["role"]),
412
- content=[TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else [],
489
+ content=content,
413
490
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
414
491
  tool_calls=tool_calls,
415
492
  tool_call_id=None, # NOTE: None, since this field is only non-null for role=='tool'
@@ -442,7 +519,7 @@ class Message(BaseMessage):
442
519
  model=model,
443
520
  # standard fields expected in an OpenAI ChatCompletion message object
444
521
  role=MessageRole(openai_message_dict["role"]),
445
- content=[TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else [],
522
+ content=content,
446
523
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
447
524
  tool_calls=tool_calls,
448
525
  tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
@@ -456,7 +533,7 @@ class Message(BaseMessage):
456
533
  model=model,
457
534
  # standard fields expected in an OpenAI ChatCompletion message object
458
535
  role=MessageRole(openai_message_dict["role"]),
459
- content=[TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else [],
536
+ content=content,
460
537
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
461
538
  tool_calls=tool_calls,
462
539
  tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
@@ -477,11 +554,25 @@ class Message(BaseMessage):
477
554
  """Go from Message class to ChatCompletion message object"""
478
555
 
479
556
  # TODO change to pydantic casting, eg `return SystemMessageModel(self)`
557
+ # If we only have one content part and it's text, treat it as COT
558
+ parse_content_parts = False
480
559
  if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
481
560
  text_content = self.content[0].text
561
+ # Otherwise, check if we have TextContent and multiple other parts
562
+ elif self.content and len(self.content) > 1:
563
+ text = [content for content in self.content if isinstance(self.content[0], TextContent)]
564
+ if len(text) > 1:
565
+ assert len(text) == 1, f"multiple text content parts found in a single message: {self.content}"
566
+ text_content = text[0].text
567
+ parse_content_parts = True
482
568
  else:
483
569
  text_content = None
484
570
 
571
+ # TODO(caren) we should eventually support multiple content parts here?
572
+ # ie, actually make dict['content'] type list
573
+ # But for now, it's OK until we support multi-modal,
574
+ # since the only "parts" we have are for supporting various COT
575
+
485
576
  if self.role == "system":
486
577
  assert all([v is not None for v in [self.role]]), vars(self)
487
578
  openai_message = {
@@ -539,6 +630,15 @@ class Message(BaseMessage):
539
630
  else:
540
631
  raise ValueError(self.role)
541
632
 
633
+ if parse_content_parts:
634
+ for content in self.content:
635
+ if isinstance(content, ReasoningContent):
636
+ openai_message["reasoning_content"] = content.reasoning
637
+ if content.signature:
638
+ openai_message["reasoning_content_signature"] = content.signature
639
+ if isinstance(content, RedactedReasoningContent):
640
+ openai_message["redacted_reasoning_content"] = content.data
641
+
542
642
  return openai_message
543
643
 
544
644
  def to_anthropic_dict(
@@ -552,6 +652,8 @@ class Message(BaseMessage):
552
652
  Args:
553
653
  inner_thoughts_xml_tag (str): The XML tag to wrap around inner thoughts
554
654
  """
655
+
656
+ # Check for COT
555
657
  if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
556
658
  text_content = self.content[0].text
557
659
  else:
@@ -587,7 +689,24 @@ class Message(BaseMessage):
587
689
  }
588
690
  content = []
589
691
  # COT / reasoning / thinking
590
- if text_content is not None and not put_inner_thoughts_in_kwargs:
692
+ if len(self.content) > 1:
693
+ for content_part in self.content:
694
+ if isinstance(content_part, ReasoningContent):
695
+ content.append(
696
+ {
697
+ "type": "thinking",
698
+ "thinking": content_part.reasoning,
699
+ "signature": content_part.signature,
700
+ }
701
+ )
702
+ if isinstance(content_part, RedactedReasoningContent):
703
+ content.append(
704
+ {
705
+ "type": "redacted_thinking",
706
+ "data": content_part.data,
707
+ }
708
+ )
709
+ elif text_content is not None:
591
710
  content.append(
592
711
  {
593
712
  "type": "text",
@@ -40,6 +40,8 @@ class Message(BaseModel):
40
40
  role: str
41
41
  function_call: Optional[FunctionCall] = None # Deprecated
42
42
  reasoning_content: Optional[str] = None # Used in newer reasoning APIs
43
+ reasoning_content_signature: Optional[str] = None # NOTE: for Anthropic
44
+ redacted_reasoning_content: Optional[str] = None # NOTE: for Anthropic
43
45
 
44
46
 
45
47
  class Choice(BaseModel):
@@ -117,6 +119,8 @@ class MessageDelta(BaseModel):
117
119
 
118
120
  content: Optional[str] = None
119
121
  reasoning_content: Optional[str] = None
122
+ reasoning_content_signature: Optional[str] = None # NOTE: for Anthropic
123
+ redacted_reasoning_content: Optional[str] = None # NOTE: for Anthropic
120
124
  tool_calls: Optional[List[ToolCallDelta]] = None
121
125
  role: Optional[str] = None
122
126
  function_call: Optional[FunctionCallDelta] = None # Deprecated
@@ -140,3 +144,4 @@ class ChatCompletionChunkResponse(BaseModel):
140
144
  system_fingerprint: Optional[str] = None
141
145
  # object: str = Field(default="chat.completion")
142
146
  object: Literal["chat.completion.chunk"] = "chat.completion.chunk"
147
+ output_tokens: int = 0
@@ -16,7 +16,14 @@ class Organization(OrganizationBase):
16
16
  id: str = OrganizationBase.generate_id_field()
17
17
  name: str = Field(create_random_username(), description="The name of the organization.", json_schema_extra={"default": "SincereYogurt"})
18
18
  created_at: Optional[datetime] = Field(default_factory=get_utc_time, description="The creation date of the organization.")
19
+ privileged_tools: bool = Field(False, description="Whether the organization has access to privileged tools.")
19
20
 
20
21
 
21
22
  class OrganizationCreate(OrganizationBase):
22
23
  name: Optional[str] = Field(None, description="The name of the organization.")
24
+ privileged_tools: Optional[bool] = Field(False, description="Whether the organization has access to privileged tools.")
25
+
26
+
27
+ class OrganizationUpdate(OrganizationBase):
28
+ name: Optional[str] = Field(None, description="The name of the organization.")
29
+ privileged_tools: Optional[bool] = Field(False, description="Whether the organization has access to privileged tools.")
@@ -157,6 +157,23 @@ class OpenAIProvider(Provider):
157
157
  # if "config" in data and "chat_template" in data["config"] and "tools" not in data["config"]["chat_template"]:
158
158
  # continue
159
159
 
160
+ # for openai, filter models
161
+ if self.base_url == "https://api.openai.com/v1":
162
+ allowed_types = ["gpt-4", "o1", "o3"]
163
+ disallowed_types = ["transcribe", "search", "realtime", "tts", "audio", "computer"]
164
+ skip = True
165
+ for model_type in allowed_types:
166
+ if model_name.startswith(model_type):
167
+ skip = False
168
+ break
169
+ for keyword in disallowed_types:
170
+ if keyword in model_name:
171
+ skip = True
172
+ break
173
+ # ignore this model
174
+ if skip:
175
+ continue
176
+
160
177
  configs.append(
161
178
  LLMConfig(
162
179
  model=model_name,
letta/schemas/tool.py CHANGED
@@ -93,7 +93,11 @@ class Tool(BaseTool):
93
93
  description=description,
94
94
  )
95
95
  else:
96
- self.json_schema = derive_openai_json_schema(source_code=self.source_code)
96
+ try:
97
+ self.json_schema = derive_openai_json_schema(source_code=self.source_code)
98
+ except Exception as e:
99
+ error_msg = f"Failed to derive json schema for tool with id={self.id} name={self.name}. Error: {str(e)}"
100
+ logger.error(error_msg)
97
101
  elif self.tool_type in {ToolType.LETTA_CORE, ToolType.LETTA_MEMORY_CORE}:
98
102
  # If it's letta core tool, we generate the json_schema on the fly here
99
103
  self.json_schema = get_json_schema_from_module(module_name=LETTA_CORE_TOOL_MODULE_NAME, function_name=self.name)
letta/schemas/usage.py CHANGED
@@ -1,7 +1,9 @@
1
- from typing import Literal
1
+ from typing import List, Literal, Optional
2
2
 
3
3
  from pydantic import BaseModel, Field
4
4
 
5
+ from letta.schemas.message import Message
6
+
5
7
 
6
8
  class LettaUsageStatistics(BaseModel):
7
9
  """
@@ -19,3 +21,5 @@ class LettaUsageStatistics(BaseModel):
19
21
  prompt_tokens: int = Field(0, description="The number of tokens in the prompt.")
20
22
  total_tokens: int = Field(0, description="The total number of tokens processed by the agent.")
21
23
  step_count: int = Field(0, description="The number of steps taken by the agent.")
24
+ # TODO: Optional for now. This field makes everyone's lives easier
25
+ steps_messages: Optional[List[List[Message]]] = Field(None, description="The messages generated per step")
@@ -92,7 +92,7 @@ class AgentSchema(BaseModel):
92
92
  agent_type: str
93
93
  core_memory: List[CoreMemoryBlockSchema]
94
94
  created_at: str
95
- description: str
95
+ description: Optional[str]
96
96
  embedding_config: EmbeddingConfig
97
97
  groups: List[Any]
98
98
  identities: List[Any]
@@ -13,6 +13,7 @@ from letta.local_llm.constants import INNER_THOUGHTS_KWARG
13
13
  from letta.schemas.enums import MessageStreamStatus
14
14
  from letta.schemas.letta_message import (
15
15
  AssistantMessage,
16
+ HiddenReasoningMessage,
16
17
  LegacyFunctionCallMessage,
17
18
  LegacyLettaMessage,
18
19
  LettaMessage,
@@ -22,6 +23,7 @@ from letta.schemas.letta_message import (
22
23
  ToolCallMessage,
23
24
  ToolReturnMessage,
24
25
  )
26
+ from letta.schemas.letta_message_content import ReasoningContent, RedactedReasoningContent, TextContent
25
27
  from letta.schemas.message import Message
26
28
  from letta.schemas.openai.chat_completion_response import ChatCompletionChunkResponse
27
29
  from letta.server.rest_api.optimistic_json_parser import OptimisticJSONParser
@@ -478,7 +480,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
478
480
 
479
481
  if (
480
482
  message_delta.content is None
481
- and (expect_reasoning_content and message_delta.reasoning_content is None)
483
+ and (expect_reasoning_content and message_delta.reasoning_content is None and message_delta.redacted_reasoning_content is None)
482
484
  and message_delta.tool_calls is None
483
485
  and message_delta.function_call is None
484
486
  and choice.finish_reason is None
@@ -493,6 +495,15 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
493
495
  id=message_id,
494
496
  date=message_date,
495
497
  reasoning=message_delta.reasoning_content,
498
+ signature=message_delta.reasoning_content_signature,
499
+ source="reasoner_model" if message_delta.reasoning_content_signature else "non_reasoner_model",
500
+ )
501
+ elif expect_reasoning_content and message_delta.redacted_reasoning_content is not None:
502
+ processed_chunk = HiddenReasoningMessage(
503
+ id=message_id,
504
+ date=message_date,
505
+ hidden_reasoning=message_delta.redacted_reasoning_content,
506
+ state="redacted",
496
507
  )
497
508
  elif expect_reasoning_content and message_delta.content is not None:
498
509
  # "ignore" content if we expect reasoning content
@@ -1071,13 +1082,39 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
1071
1082
  # "id": str(msg_obj.id) if msg_obj is not None else None,
1072
1083
  # }
1073
1084
  assert msg_obj is not None, "Internal monologue requires msg_obj references for metadata"
1074
- processed_chunk = ReasoningMessage(
1075
- id=msg_obj.id,
1076
- date=msg_obj.created_at,
1077
- reasoning=msg,
1078
- )
1085
+ if msg_obj.content and len(msg_obj.content) == 1 and isinstance(msg_obj.content[0], TextContent):
1086
+ processed_chunk = ReasoningMessage(
1087
+ id=msg_obj.id,
1088
+ date=msg_obj.created_at,
1089
+ reasoning=msg,
1090
+ )
1091
+
1092
+ self._push_to_buffer(processed_chunk)
1093
+ else:
1094
+ for content in msg_obj.content:
1095
+ if isinstance(content, TextContent):
1096
+ processed_chunk = ReasoningMessage(
1097
+ id=msg_obj.id,
1098
+ date=msg_obj.created_at,
1099
+ reasoning=content.text,
1100
+ )
1101
+ elif isinstance(content, ReasoningContent):
1102
+ processed_chunk = ReasoningMessage(
1103
+ id=msg_obj.id,
1104
+ date=msg_obj.created_at,
1105
+ source="reasoner_model",
1106
+ reasoning=content.reasoning,
1107
+ signature=content.signature,
1108
+ )
1109
+ elif isinstance(content, RedactedReasoningContent):
1110
+ processed_chunk = HiddenReasoningMessage(
1111
+ id=msg_obj.id,
1112
+ date=msg_obj.created_at,
1113
+ state="redacted",
1114
+ hidden_reasoning=content.data,
1115
+ )
1079
1116
 
1080
- self._push_to_buffer(processed_chunk)
1117
+ self._push_to_buffer(processed_chunk)
1081
1118
 
1082
1119
  return
1083
1120
 
@@ -63,6 +63,10 @@ def list_agents(
63
63
  "Using this can optimize performance by reducing unnecessary joins."
64
64
  ),
65
65
  ),
66
+ ascending: bool = Query(
67
+ False,
68
+ description="Whether to sort agents oldest to newest (True) or newest to oldest (False, default)",
69
+ ),
66
70
  ):
67
71
  """
68
72
  List all agents associated with a given user.
@@ -90,6 +94,7 @@ def list_agents(
90
94
  identity_id=identity_id,
91
95
  identifier_keys=identifier_keys,
92
96
  include_relationships=include_relationships,
97
+ ascending=ascending,
93
98
  )
94
99
 
95
100
 
@@ -432,9 +437,13 @@ def detach_block(
432
437
  def list_passages(
433
438
  agent_id: str,
434
439
  server: "SyncServer" = Depends(get_letta_server),
435
- after: Optional[int] = Query(None, description="Unique ID of the memory to start the query range at."),
436
- before: Optional[int] = Query(None, description="Unique ID of the memory to end the query range at."),
440
+ after: Optional[str] = Query(None, description="Unique ID of the memory to start the query range at."),
441
+ before: Optional[str] = Query(None, description="Unique ID of the memory to end the query range at."),
437
442
  limit: Optional[int] = Query(None, description="How many results to include in the response."),
443
+ search: Optional[str] = Query(None, description="Search passages by text"),
444
+ ascending: Optional[bool] = Query(
445
+ True, description="Whether to sort passages oldest to newest (True, default) or newest to oldest (False)"
446
+ ),
438
447
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
439
448
  ):
440
449
  """
@@ -447,7 +456,9 @@ def list_passages(
447
456
  agent_id=agent_id,
448
457
  after=after,
449
458
  before=before,
459
+ query_text=search,
450
460
  limit=limit,
461
+ ascending=ascending,
451
462
  )
452
463
 
453
464
 
@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, List, Optional
2
2
 
3
3
  from fastapi import APIRouter, Body, Depends, HTTPException, Query
4
4
 
5
- from letta.schemas.organization import Organization, OrganizationCreate
5
+ from letta.schemas.organization import Organization, OrganizationCreate, OrganizationUpdate
6
6
  from letta.server.rest_api.utils import get_letta_server
7
7
 
8
8
  if TYPE_CHECKING:
@@ -59,3 +59,21 @@ def delete_org(
59
59
  except Exception as e:
60
60
  raise HTTPException(status_code=500, detail=f"{e}")
61
61
  return org
62
+
63
+
64
+ @router.patch("/", tags=["admin"], response_model=Organization, operation_id="update_organization")
65
+ def update_org(
66
+ org_id: str = Query(..., description="The org_id key to be updated."),
67
+ request: OrganizationUpdate = Body(...),
68
+ server: "SyncServer" = Depends(get_letta_server),
69
+ ):
70
+ try:
71
+ org = server.organization_manager.get_organization_by_id(org_id=org_id)
72
+ if org is None:
73
+ raise HTTPException(status_code=404, detail=f"Organization does not exist")
74
+ org = server.organization_manager.update_organization(org_id=org_id, name=request.name)
75
+ except HTTPException:
76
+ raise
77
+ except Exception as e:
78
+ raise HTTPException(status_code=500, detail=f"{e}")
79
+ return org
@@ -80,7 +80,7 @@ async def sse_async_generator(
80
80
  err_msg = f"Expected LettaUsageStatistics, got {type(usage)}"
81
81
  logger.error(err_msg)
82
82
  raise ValueError(err_msg)
83
- yield sse_formatter(usage.model_dump())
83
+ yield sse_formatter(usage.model_dump(exclude={"steps_messages"}))
84
84
 
85
85
  except ContextWindowExceededError as e:
86
86
  log_error_to_sentry(e)