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
@@ -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
|
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
|
-
|
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
|
-
|
18
|
+
openai = "openai"
|
17
19
|
together = "together"
|
18
|
-
azure = "azure"
|
19
20
|
vllm = "vllm"
|
20
|
-
|
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="")
|
letta/schemas/letta_message.py
CHANGED
@@ -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[
|
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(
|
letta/schemas/letta_request.py
CHANGED
@@ -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
|
7
|
+
from letta.schemas.message import MessageCreateUnion
|
8
8
|
|
9
9
|
|
10
10
|
class LettaRequest(BaseModel):
|
11
|
-
messages: List[
|
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
|
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 (
|
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
|
letta/schemas/llm_config.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
231
|
-
|
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
|
-
|
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
|
-
|
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")
|