letta-nightly 0.11.6.dev20250903104037__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.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/METADATA +11 -7
  134. {letta_nightly-0.11.6.dev20250903104037.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.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/WHEEL +0 -0
  137. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/entry_points.txt +0 -0
  138. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/licenses/LICENSE +0 -0
@@ -9,7 +9,6 @@ from letta.schemas.memory import Memory
9
9
 
10
10
 
11
11
  class PromptGenerator:
12
-
13
12
  # TODO: This code is kind of wonky and deserves a rewrite
14
13
  @trace_method
15
14
  @staticmethod
@@ -18,6 +17,7 @@ class PromptGenerator:
18
17
  timezone: str,
19
18
  previous_message_count: int = 0,
20
19
  archival_memory_size: Optional[int] = 0,
20
+ archive_tags: Optional[List[str]] = None,
21
21
  ) -> str:
22
22
  """
23
23
  Generate a memory metadata block for the agent's system prompt.
@@ -32,6 +32,7 @@ class PromptGenerator:
32
32
  timezone: The timezone to use for formatting timestamps (e.g., 'America/Los_Angeles')
33
33
  previous_message_count: Number of messages in recall memory (conversation history)
34
34
  archival_memory_size: Number of items in archival memory (long-term storage)
35
+ archive_tags: List of unique tags available in archival memory
35
36
 
36
37
  Returns:
37
38
  A formatted string containing the memory metadata block with XML-style tags
@@ -42,6 +43,7 @@ class PromptGenerator:
42
43
  - Memory blocks were last modified: 2024-01-15 09:00 AM PST
43
44
  - 42 previous messages between you and the user are stored in recall memory (use tools to access them)
44
45
  - 156 total memories you created are stored in archival memory (use tools to access them)
46
+ - Available archival memory tags: project_x, meeting_notes, research, ideas
45
47
  </memory_metadata>
46
48
  """
47
49
  # Put the timestamp in the local timezone (mimicking get_local_time())
@@ -50,7 +52,7 @@ class PromptGenerator:
50
52
  # Create a metadata block of info so the agent knows about the metadata of out-of-context memories
51
53
  metadata_lines = [
52
54
  "<memory_metadata>",
53
- f"- The current time is: {get_local_time_fast(timezone)}",
55
+ f"- The current system date is: {get_local_time_fast(timezone)}",
54
56
  f"- Memory blocks were last modified: {timestamp_str}",
55
57
  f"- {previous_message_count} previous messages between you and the user are stored in recall memory (use tools to access them)",
56
58
  ]
@@ -61,6 +63,10 @@ class PromptGenerator:
61
63
  f"- {archival_memory_size} total memories you created are stored in archival memory (use tools to access them)"
62
64
  )
63
65
 
66
+ # Include archive tags if available
67
+ if archive_tags:
68
+ metadata_lines.append(f"- Available archival memory tags: {', '.join(archive_tags)}")
69
+
64
70
  metadata_lines.append("</memory_metadata>")
65
71
  memory_metadata_block = "\n".join(metadata_lines)
66
72
  return memory_metadata_block
@@ -91,6 +97,7 @@ class PromptGenerator:
91
97
  template_format: Literal["f-string", "mustache", "jinja2"] = "f-string",
92
98
  previous_message_count: int = 0,
93
99
  archival_memory_size: int = 0,
100
+ archive_tags: Optional[List[str]] = None,
94
101
  ) -> str:
95
102
  """Prepare the final/full system message that will be fed into the LLM API
96
103
 
@@ -115,6 +122,7 @@ class PromptGenerator:
115
122
  previous_message_count=previous_message_count,
116
123
  archival_memory_size=archival_memory_size,
117
124
  timezone=timezone,
125
+ archive_tags=archive_tags,
118
126
  )
119
127
 
120
128
  full_memory_string = memory_with_sources + "\n\n" + memory_metadata_string
letta/schemas/agent.py CHANGED
@@ -91,6 +91,8 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
91
91
  project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
92
92
  template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
93
93
  base_template_id: Optional[str] = Field(None, description="The base template id of the agent.")
94
+ deployment_id: Optional[str] = Field(None, description="The id of the deployment.")
95
+ entity_id: Optional[str] = Field(None, description="The id of the entity within the template.")
94
96
  identity_ids: List[str] = Field([], description="The ids of the identities associated with this agent.")
95
97
 
96
98
  # An advanced configuration that makes it so this agent does not remember any previous messages
@@ -304,6 +306,15 @@ class CreateAgent(BaseModel, validate_assignment=True): #
304
306
  return self
305
307
 
306
308
 
309
+ class InternalTemplateAgentCreate(CreateAgent):
310
+ """Used for Letta Cloud"""
311
+
312
+ base_template_id: str = Field(..., description="The id of the base template.")
313
+ template_id: str = Field(..., description="The id of the template.")
314
+ deployment_id: str = Field(..., description="The id of the deployment.")
315
+ entity_id: str = Field(..., description="The id of the entity within the template.")
316
+
317
+
307
318
  class UpdateAgent(BaseModel):
308
319
  name: Optional[str] = Field(None, description="The name of the agent.")
309
320
  tool_ids: Optional[List[str]] = Field(None, description="The ids of the tools used by the agent.")
letta/schemas/archive.py CHANGED
@@ -3,6 +3,7 @@ from typing import Dict, Optional
3
3
 
4
4
  from pydantic import Field
5
5
 
6
+ from letta.schemas.enums import VectorDBProvider
6
7
  from letta.schemas.letta_base import OrmMetadataBase
7
8
 
8
9
 
@@ -12,6 +13,9 @@ class ArchiveBase(OrmMetadataBase):
12
13
  name: str = Field(..., description="The name of the archive")
13
14
  description: Optional[str] = Field(None, description="A description of the archive")
14
15
  organization_id: str = Field(..., description="The organization this archive belongs to")
16
+ vector_db_provider: VectorDBProvider = Field(
17
+ default=VectorDBProvider.NATIVE, description="The vector database provider used for this archive's passages"
18
+ )
15
19
  metadata: Optional[Dict] = Field(default_factory=dict, validation_alias="metadata_", description="Additional metadata")
16
20
 
17
21
 
letta/schemas/block.py CHANGED
@@ -23,6 +23,10 @@ class BaseBlock(LettaBase, validate_assignment=True):
23
23
  # template data (optional)
24
24
  template_name: Optional[str] = Field(None, description="Name of the block if it is a template.", alias="name")
25
25
  is_template: bool = Field(False, description="Whether the block is a template (e.g. saved human/persona options).")
26
+ template_id: Optional[str] = Field(None, description="The id of the template.", alias="name")
27
+ base_template_id: Optional[str] = Field(None, description="The base template id of the block.")
28
+ deployment_id: Optional[str] = Field(None, description="The id of the deployment.")
29
+ entity_id: Optional[str] = Field(None, description="The id of the entity within the template.")
26
30
  preserve_on_migration: Optional[bool] = Field(False, description="Preserve the block on template migration.")
27
31
 
28
32
  # context window label
@@ -168,3 +172,12 @@ class CreatePersonaBlockTemplate(CreatePersona):
168
172
 
169
173
  is_template: bool = True
170
174
  label: str = "persona"
175
+
176
+
177
+ class InternalTemplateBlockCreate(CreateBlock):
178
+ """Used for Letta Cloud"""
179
+
180
+ base_template_id: str = Field(..., description="The id of the base template.")
181
+ template_id: str = Field(..., description="The id of the template.")
182
+ deployment_id: str = Field(..., description="The id of the deployment.")
183
+ entity_id: str = Field(..., description="The id of the entity within the template.")
@@ -43,7 +43,6 @@ class EmbeddingConfig(BaseModel):
43
43
 
44
44
  @classmethod
45
45
  def default_config(cls, model_name: Optional[str] = None, provider: Optional[str] = None):
46
-
47
46
  if model_name == "text-embedding-ada-002" and provider == "openai":
48
47
  return cls(
49
48
  embedding_model="text-embedding-ada-002",
letta/schemas/enums.py CHANGED
@@ -3,21 +3,22 @@ from enum import Enum, StrEnum
3
3
 
4
4
  class ProviderType(str, Enum):
5
5
  anthropic = "anthropic"
6
+ azure = "azure"
7
+ bedrock = "bedrock"
8
+ cerebras = "cerebras"
9
+ deepseek = "deepseek"
6
10
  google_ai = "google_ai"
7
11
  google_vertex = "google_vertex"
8
- openai = "openai"
12
+ groq = "groq"
13
+ hugging_face = "hugging-face"
9
14
  letta = "letta"
10
- deepseek = "deepseek"
11
- cerebras = "cerebras"
12
15
  lmstudio_openai = "lmstudio_openai"
13
- xai = "xai"
14
16
  mistral = "mistral"
15
17
  ollama = "ollama"
16
- groq = "groq"
18
+ openai = "openai"
17
19
  together = "together"
18
- azure = "azure"
19
20
  vllm = "vllm"
20
- bedrock = "bedrock"
21
+ xai = "xai"
21
22
 
22
23
 
23
24
  class ProviderCategory(str, Enum):
@@ -31,6 +32,7 @@ class MessageRole(str, Enum):
31
32
  tool = "tool"
32
33
  function = "function"
33
34
  system = "system"
35
+ approval = "approval"
34
36
 
35
37
 
36
38
  class OptionState(str, Enum):
@@ -93,6 +95,7 @@ class ToolRuleType(str, Enum):
93
95
  max_count_per_step = "max_count_per_step"
94
96
  parent_last_tool = "parent_last_tool"
95
97
  required_before_exit = "required_before_exit" # tool must be called before loop can exit
98
+ requires_approval = "requires_approval"
96
99
 
97
100
 
98
101
  class FileProcessingStatus(str, Enum):
@@ -170,3 +173,17 @@ class StepStatus(str, Enum):
170
173
  SUCCESS = "success"
171
174
  FAILED = "failed"
172
175
  CANCELLED = "cancelled"
176
+
177
+
178
+ class VectorDBProvider(str, Enum):
179
+ """Supported vector database providers for archival memory"""
180
+
181
+ NATIVE = "native"
182
+ TPUF = "tpuf"
183
+
184
+
185
+ class TagMatchMode(str, Enum):
186
+ """Tag matching behavior for filtering"""
187
+
188
+ ANY = "any"
189
+ ALL = "all"
letta/schemas/group.py CHANGED
@@ -29,6 +29,10 @@ class Group(GroupBase):
29
29
  agent_ids: List[str] = Field(..., description="")
30
30
  description: str = Field(..., description="")
31
31
  project_id: Optional[str] = Field(None, description="The associated project id.")
32
+ # Template fields
33
+ template_id: Optional[str] = Field(None, description="The id of the template.")
34
+ base_template_id: Optional[str] = Field(None, description="The base template id.")
35
+ deployment_id: Optional[str] = Field(None, description="The id of the deployment.")
32
36
  shared_block_ids: List[str] = Field([], description="")
33
37
  # Pattern fields
34
38
  manager_agent_id: Optional[str] = Field(None, description="")
@@ -168,6 +172,14 @@ class GroupCreate(BaseModel):
168
172
  shared_block_ids: List[str] = Field([], description="")
169
173
 
170
174
 
175
+ class InternalTemplateGroupCreate(GroupCreate):
176
+ """Used for Letta Cloud"""
177
+
178
+ base_template_id: str = Field(..., description="The id of the base template.")
179
+ template_id: str = Field(..., description="The id of the template.")
180
+ deployment_id: str = Field(..., description="The id of the deployment.")
181
+
182
+
171
183
  class GroupUpdate(BaseModel):
172
184
  agent_ids: Optional[List[str]] = Field(None, description="")
173
185
  description: Optional[str] = Field(None, description="")
@@ -25,6 +25,8 @@ class MessageType(str, Enum):
25
25
  hidden_reasoning_message = "hidden_reasoning_message"
26
26
  tool_call_message = "tool_call_message"
27
27
  tool_return_message = "tool_return_message"
28
+ approval_request_message = "approval_request_message"
29
+ approval_response_message = "approval_response_message"
28
30
 
29
31
 
30
32
  class LettaMessage(BaseModel):
@@ -249,6 +251,44 @@ class ToolReturnMessage(LettaMessage):
249
251
  stderr: Optional[List[str]] = None
250
252
 
251
253
 
254
+ class ApprovalRequestMessage(LettaMessage):
255
+ """
256
+ A message representing a request for approval to call a tool (generated by the LLM to trigger tool execution).
257
+
258
+ Args:
259
+ id (str): The ID of the message
260
+ date (datetime): The date the message was created in ISO format
261
+ name (Optional[str]): The name of the sender of the message
262
+ tool_call (ToolCall): The tool call
263
+ """
264
+
265
+ message_type: Literal[MessageType.approval_request_message] = Field(
266
+ default=MessageType.approval_request_message, description="The type of the message."
267
+ )
268
+ tool_call: ToolCall = Field(..., description="The tool call that has been requested by the llm to run")
269
+
270
+
271
+ class ApprovalResponseMessage(LettaMessage):
272
+ """
273
+ A message representing a response form the user indicating whether a tool has been approved to run.
274
+
275
+ Args:
276
+ id (str): The ID of the message
277
+ date (datetime): The date the message was created in ISO format
278
+ name (Optional[str]): The name of the sender of the message
279
+ approve: (bool) Whether the tool has been approved
280
+ approval_request_id: The ID of the approval request
281
+ reason: (Optional[str]) An optional explanation for the provided approval status
282
+ """
283
+
284
+ message_type: Literal[MessageType.approval_response_message] = Field(
285
+ default=MessageType.approval_response_message, description="The type of the message."
286
+ )
287
+ approve: bool = Field(..., description="Whether the tool has been approved")
288
+ approval_request_id: str = Field(..., description="The message ID of the approval request")
289
+ reason: Optional[str] = Field(None, description="An optional explanation for the provided approval status")
290
+
291
+
252
292
  class AssistantMessage(LettaMessage):
253
293
  """
254
294
  A message sent by the LLM in response to user input. Used in the LLM context.
@@ -272,7 +312,17 @@ class AssistantMessage(LettaMessage):
272
312
 
273
313
  # NOTE: use Pydantic's discriminated unions feature: https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions
274
314
  LettaMessageUnion = Annotated[
275
- Union[SystemMessage, UserMessage, ReasoningMessage, HiddenReasoningMessage, ToolCallMessage, ToolReturnMessage, AssistantMessage],
315
+ Union[
316
+ SystemMessage,
317
+ UserMessage,
318
+ ReasoningMessage,
319
+ HiddenReasoningMessage,
320
+ ToolCallMessage,
321
+ ToolReturnMessage,
322
+ AssistantMessage,
323
+ ApprovalRequestMessage,
324
+ ApprovalResponseMessage,
325
+ ],
276
326
  Field(discriminator="message_type"),
277
327
  ]
278
328
 
@@ -287,6 +337,8 @@ def create_letta_message_union_schema():
287
337
  {"$ref": "#/components/schemas/ToolCallMessage"},
288
338
  {"$ref": "#/components/schemas/ToolReturnMessage"},
289
339
  {"$ref": "#/components/schemas/AssistantMessage"},
340
+ {"$ref": "#/components/schemas/ApprovalRequestMessage"},
341
+ {"$ref": "#/components/schemas/ApprovalResponseMessage"},
290
342
  ],
291
343
  "discriminator": {
292
344
  "propertyName": "message_type",
@@ -298,6 +350,8 @@ def create_letta_message_union_schema():
298
350
  "tool_call_message": "#/components/schemas/ToolCallMessage",
299
351
  "tool_return_message": "#/components/schemas/ToolReturnMessage",
300
352
  "assistant_message": "#/components/schemas/AssistantMessage",
353
+ "approval_request_message": "#/components/schemas/ApprovalRequestMessage",
354
+ "approval_response_message": "#/components/schemas/ApprovalResponseMessage",
301
355
  },
302
356
  },
303
357
  }
@@ -17,6 +17,14 @@ class MessageContentType(str, Enum):
17
17
  class MessageContent(BaseModel):
18
18
  type: MessageContentType = Field(..., description="The type of the message.")
19
19
 
20
+ def to_text(self) -> Optional[str]:
21
+ """Extract text representation from this content type.
22
+
23
+ Returns:
24
+ Text representation of the content, None if no text available.
25
+ """
26
+ return None
27
+
20
28
 
21
29
  # -------------------------------
22
30
  # Text Content
@@ -27,6 +35,10 @@ class TextContent(MessageContent):
27
35
  type: Literal[MessageContentType.text] = Field(default=MessageContentType.text, description="The type of the message.")
28
36
  text: str = Field(..., description="The text content of the message.")
29
37
 
38
+ def to_text(self) -> str:
39
+ """Return the text content."""
40
+ return self.text
41
+
30
42
 
31
43
  # -------------------------------
32
44
  # Image Content
@@ -172,6 +184,13 @@ class ToolCallContent(MessageContent):
172
184
  ..., description="The parameters being passed to the tool, structured as a dictionary of parameter names to values."
173
185
  )
174
186
 
187
+ def to_text(self) -> str:
188
+ """Return a text representation of the tool call."""
189
+ import json
190
+
191
+ input_str = json.dumps(self.input, indent=2)
192
+ return f"Tool call: {self.name}({input_str})"
193
+
175
194
 
176
195
  class ToolReturnContent(MessageContent):
177
196
  type: Literal[MessageContentType.tool_return] = Field(
@@ -181,6 +200,11 @@ class ToolReturnContent(MessageContent):
181
200
  content: str = Field(..., description="The content returned by the tool execution.")
182
201
  is_error: bool = Field(..., description="Indicates whether the tool execution resulted in an error.")
183
202
 
203
+ def to_text(self) -> str:
204
+ """Return the tool return content."""
205
+ prefix = "Tool error: " if self.is_error else "Tool result: "
206
+ return f"{prefix}{self.content}"
207
+
184
208
 
185
209
  class ReasoningContent(MessageContent):
186
210
  type: Literal[MessageContentType.reasoning] = Field(
@@ -190,6 +214,10 @@ class ReasoningContent(MessageContent):
190
214
  reasoning: str = Field(..., description="The intermediate reasoning or thought process content.")
191
215
  signature: Optional[str] = Field(default=None, description="A unique identifier for this reasoning step.")
192
216
 
217
+ def to_text(self) -> str:
218
+ """Return the reasoning content."""
219
+ return self.reasoning
220
+
193
221
 
194
222
  class RedactedReasoningContent(MessageContent):
195
223
  type: Literal[MessageContentType.redacted_reasoning] = Field(
@@ -1,14 +1,14 @@
1
1
  from typing import List, Optional
2
2
 
3
- from pydantic import BaseModel, Field, HttpUrl
3
+ from pydantic import BaseModel, Field, HttpUrl, field_validator
4
4
 
5
5
  from letta.constants import DEFAULT_MAX_STEPS, DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
6
6
  from letta.schemas.letta_message import MessageType
7
- from letta.schemas.message import MessageCreate
7
+ from letta.schemas.message import MessageCreateUnion
8
8
 
9
9
 
10
10
  class LettaRequest(BaseModel):
11
- messages: List[MessageCreate] = Field(..., description="The messages to be sent to the agent.")
11
+ messages: List[MessageCreateUnion] = Field(..., description="The messages to be sent to the agent.")
12
12
  max_steps: int = Field(
13
13
  default=DEFAULT_MAX_STEPS,
14
14
  description="Maximum number of steps the agent should take to process the request.",
@@ -36,11 +36,28 @@ class LettaRequest(BaseModel):
36
36
  description="If set to True, enables reasoning before responses or tool calls from the agent.",
37
37
  )
38
38
 
39
+ @field_validator("messages", mode="before")
40
+ @classmethod
41
+ def add_default_type_to_messages(cls, v):
42
+ """Handle union without discriminator - default to 'message' type if not specified"""
43
+ if isinstance(v, list):
44
+ for item in v:
45
+ if isinstance(item, dict):
46
+ # If type is not present, determine based on fields
47
+ if "type" not in item:
48
+ # If it has approval-specific fields, it's an approval
49
+ if "approval_request_id" in item or "approve" in item:
50
+ item["type"] = "approval"
51
+ else:
52
+ # Default to message
53
+ item["type"] = "message"
54
+ return v
55
+
39
56
 
40
57
  class LettaStreamingRequest(LettaRequest):
41
58
  stream_tokens: bool = Field(
42
59
  default=False,
43
- description="Flag to determine if individual tokens should be streamed. Set to True for token streaming (requires stream_steps = True).",
60
+ description="Flag to determine if individual tokens should be streamed, rather than streaming per step.",
44
61
  )
45
62
  include_pings: bool = Field(
46
63
  default=False,
@@ -9,11 +9,13 @@ from letta.schemas.enums import JobStatus
9
9
  class StopReasonType(str, Enum):
10
10
  end_turn = "end_turn"
11
11
  error = "error"
12
+ invalid_llm_response = "invalid_llm_response"
12
13
  invalid_tool_call = "invalid_tool_call"
13
14
  max_steps = "max_steps"
14
15
  no_tool_call = "no_tool_call"
15
16
  tool_rule = "tool_rule"
16
17
  cancelled = "cancelled"
18
+ requires_approval = "requires_approval"
17
19
 
18
20
  @property
19
21
  def run_status(self) -> JobStatus:
@@ -21,9 +23,15 @@ class StopReasonType(str, Enum):
21
23
  StopReasonType.end_turn,
22
24
  StopReasonType.max_steps,
23
25
  StopReasonType.tool_rule,
26
+ StopReasonType.requires_approval,
24
27
  ):
25
28
  return JobStatus.completed
26
- elif self in (StopReasonType.error, StopReasonType.invalid_tool_call, StopReasonType.no_tool_call):
29
+ elif self in (
30
+ StopReasonType.error,
31
+ StopReasonType.invalid_tool_call,
32
+ StopReasonType.no_tool_call,
33
+ StopReasonType.invalid_llm_response,
34
+ ):
27
35
  return JobStatus.failed
28
36
  elif self == StopReasonType.cancelled:
29
37
  return JobStatus.cancelled
@@ -51,7 +51,7 @@ class LLMConfig(BaseModel):
51
51
  description="The temperature to use when generating text with the model. A higher temperature will result in more random text.",
52
52
  )
53
53
  max_tokens: Optional[int] = Field(
54
- 4096,
54
+ None,
55
55
  description="The maximum number of tokens to generate. If not set, the model will use its default value.",
56
56
  )
57
57
  enable_reasoner: bool = Field(
@@ -71,9 +71,10 @@ class LLMConfig(BaseModel):
71
71
  )
72
72
  compatibility_type: Optional[Literal["gguf", "mlx"]] = Field(None, description="The framework compatibility type for the model.")
73
73
  verbosity: Optional[Literal["low", "medium", "high"]] = Field(
74
- "medium",
74
+ None,
75
75
  description="Soft control for how verbose model output should be, used for GPT-5 models.",
76
76
  )
77
+ tier: Optional[str] = Field(None, description="The cost tier for the model (cloud only).")
77
78
 
78
79
  # FIXME hack to silence pydantic protected namespace warning
79
80
  model_config = ConfigDict(protected_namespaces=())
@@ -205,6 +206,7 @@ class LLMConfig(BaseModel):
205
206
  model_endpoint="https://api.openai.com/v1",
206
207
  model_wrapper=None,
207
208
  context_window=128000,
209
+ reasoning_effort="minimal",
208
210
  verbosity="medium",
209
211
  max_tokens=16384,
210
212
  )
@@ -227,9 +229,9 @@ class LLMConfig(BaseModel):
227
229
 
228
230
  @classmethod
229
231
  def is_openai_reasoning_model(cls, config: "LLMConfig") -> bool:
230
- return config.model_endpoint_type == "openai" and (
231
- config.model.startswith("o1") or config.model.startswith("o3") or config.model.startswith("o4")
232
- )
232
+ from letta.llm_api.openai_client import is_openai_reasoning_model
233
+
234
+ return config.model_endpoint_type == "openai" and is_openai_reasoning_model(config.model)
233
235
 
234
236
  @classmethod
235
237
  def is_anthropic_reasoning_model(cls, config: "LLMConfig") -> bool:
@@ -260,11 +262,18 @@ class LLMConfig(BaseModel):
260
262
  def apply_reasoning_setting_to_config(cls, config: "LLMConfig", reasoning: bool):
261
263
  if not reasoning:
262
264
  if cls.is_openai_reasoning_model(config):
263
- logger.warning("Reasoning cannot be disabled for OpenAI o1/o3 models")
265
+ logger.warning("Reasoning cannot be disabled for OpenAI o1/o3/gpt-5 models")
264
266
  config.put_inner_thoughts_in_kwargs = False
265
267
  config.enable_reasoner = True
266
268
  if config.reasoning_effort is None:
267
- config.reasoning_effort = "medium"
269
+ # GPT-5 models default to minimal, others to medium
270
+ if config.model.startswith("gpt-5"):
271
+ config.reasoning_effort = "minimal"
272
+ else:
273
+ config.reasoning_effort = "medium"
274
+ # Set verbosity for GPT-5 models
275
+ if config.model.startswith("gpt-5") and config.verbosity is None:
276
+ config.verbosity = "medium"
268
277
  elif config.model.startswith("gemini-2.5-pro"):
269
278
  logger.warning("Reasoning cannot be disabled for Gemini 2.5 Pro model")
270
279
  # Handle as non-reasoner until we support summary
@@ -290,7 +299,14 @@ class LLMConfig(BaseModel):
290
299
  elif cls.is_openai_reasoning_model(config):
291
300
  config.put_inner_thoughts_in_kwargs = False
292
301
  if config.reasoning_effort is None:
293
- config.reasoning_effort = "medium"
302
+ # GPT-5 models default to minimal, others to medium
303
+ if config.model.startswith("gpt-5"):
304
+ config.reasoning_effort = "minimal"
305
+ else:
306
+ config.reasoning_effort = "medium"
307
+ # Set verbosity for GPT-5 models
308
+ if config.model.startswith("gpt-5") and config.verbosity is None:
309
+ config.verbosity = "medium"
294
310
  else:
295
311
  config.put_inner_thoughts_in_kwargs = True
296
312
 
letta/schemas/mcp.py CHANGED
@@ -84,7 +84,6 @@ class MCPServer(BaseMCPServer):
84
84
  class UpdateSSEMCPServer(LettaBase):
85
85
  """Update an SSE MCP server"""
86
86
 
87
- server_name: Optional[str] = Field(None, description="The name of the server")
88
87
  server_url: Optional[str] = Field(None, description="The URL of the server (MCP SSE client will connect to this URL)")
89
88
  token: Optional[str] = Field(None, description="The access token or API key for the MCP server (used for SSE authentication)")
90
89
  custom_headers: Optional[Dict[str, str]] = Field(None, description="Custom authentication headers as key-value pairs")
@@ -93,7 +92,6 @@ class UpdateSSEMCPServer(LettaBase):
93
92
  class UpdateStdioMCPServer(LettaBase):
94
93
  """Update a Stdio MCP server"""
95
94
 
96
- server_name: Optional[str] = Field(None, description="The name of the server")
97
95
  stdio_config: Optional[StdioServerConfig] = Field(
98
96
  None, description="The configuration for the server (MCP 'local' client will run this command)"
99
97
  )
@@ -102,7 +100,6 @@ class UpdateStdioMCPServer(LettaBase):
102
100
  class UpdateStreamableHTTPMCPServer(LettaBase):
103
101
  """Update a Streamable HTTP MCP server"""
104
102
 
105
- server_name: Optional[str] = Field(None, description="The name of the server")
106
103
  server_url: Optional[str] = Field(None, description="The URL path for the streamable HTTP server (e.g., 'example/mcp')")
107
104
  auth_header: Optional[str] = Field(None, description="The name of the authentication header (e.g., 'Authorization')")
108
105
  auth_token: Optional[str] = Field(None, description="The authentication token or API key value")
letta/schemas/memory.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import logging
3
+ from datetime import datetime
3
4
  from typing import TYPE_CHECKING, List, Optional
4
5
 
5
6
  from jinja2 import Template, TemplateSyntaxError
@@ -325,3 +326,16 @@ class RecallMemorySummary(BaseModel):
325
326
 
326
327
  class CreateArchivalMemory(BaseModel):
327
328
  text: str = Field(..., description="Text to write to archival memory.")
329
+ tags: Optional[List[str]] = Field(None, description="Optional list of tags to attach to the memory.")
330
+ created_at: Optional[datetime] = Field(None, description="Optional timestamp for the memory (defaults to current UTC time).")
331
+
332
+
333
+ class ArchivalMemorySearchResult(BaseModel):
334
+ timestamp: str = Field(..., description="Timestamp of when the memory was created, formatted in agent's timezone")
335
+ content: str = Field(..., description="Text content of the archival memory passage")
336
+ tags: List[str] = Field(default_factory=list, description="List of tags associated with this memory")
337
+
338
+
339
+ class ArchivalMemorySearchResponse(BaseModel):
340
+ results: List[ArchivalMemorySearchResult] = Field(..., description="List of search results matching the query")
341
+ count: int = Field(..., description="Total number of results returned")