letta-nightly 0.6.39.dev20250314104053__py3-none-any.whl → 0.6.40.dev20250314222759__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.
- letta/__init__.py +1 -1
- letta/agent.py +14 -4
- letta/agents/ephemeral_agent.py +2 -1
- letta/agents/low_latency_agent.py +8 -0
- letta/dynamic_multi_agent.py +274 -0
- letta/functions/function_sets/base.py +1 -0
- letta/functions/function_sets/extras.py +2 -1
- letta/functions/function_sets/multi_agent.py +17 -0
- letta/functions/helpers.py +41 -0
- letta/functions/mcp_client/__init__.py +0 -0
- letta/functions/mcp_client/base_client.py +61 -0
- letta/functions/mcp_client/sse_client.py +21 -0
- letta/functions/mcp_client/stdio_client.py +103 -0
- letta/functions/mcp_client/types.py +48 -0
- letta/functions/schema_generator.py +1 -1
- letta/helpers/converters.py +67 -0
- letta/llm_api/openai.py +1 -1
- letta/memory.py +2 -1
- letta/orm/__init__.py +2 -0
- letta/orm/agent.py +69 -20
- letta/orm/custom_columns.py +15 -0
- letta/orm/group.py +33 -0
- letta/orm/groups_agents.py +13 -0
- letta/orm/message.py +7 -4
- letta/orm/organization.py +1 -0
- letta/orm/sqlalchemy_base.py +3 -3
- letta/round_robin_multi_agent.py +152 -0
- letta/schemas/agent.py +3 -0
- letta/schemas/enums.py +0 -4
- letta/schemas/group.py +65 -0
- letta/schemas/letta_message.py +167 -106
- letta/schemas/letta_message_content.py +192 -0
- letta/schemas/message.py +28 -36
- letta/schemas/tool.py +1 -1
- letta/serialize_schemas/__init__.py +1 -1
- letta/serialize_schemas/marshmallow_agent.py +108 -0
- letta/serialize_schemas/{agent_environment_variable.py → marshmallow_agent_environment_variable.py} +1 -1
- letta/serialize_schemas/marshmallow_base.py +52 -0
- letta/serialize_schemas/{block.py → marshmallow_block.py} +1 -1
- letta/serialize_schemas/{custom_fields.py → marshmallow_custom_fields.py} +12 -0
- letta/serialize_schemas/marshmallow_message.py +42 -0
- letta/serialize_schemas/{tag.py → marshmallow_tag.py} +12 -2
- letta/serialize_schemas/{tool.py → marshmallow_tool.py} +1 -1
- letta/serialize_schemas/pydantic_agent_schema.py +111 -0
- letta/server/rest_api/app.py +15 -0
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +46 -40
- letta/server/rest_api/routers/v1/groups.py +233 -0
- letta/server/rest_api/routers/v1/tools.py +31 -3
- letta/server/rest_api/utils.py +1 -1
- letta/server/server.py +272 -22
- letta/services/agent_manager.py +65 -28
- letta/services/group_manager.py +147 -0
- letta/services/helpers/agent_manager_helper.py +151 -1
- letta/services/message_manager.py +11 -3
- letta/services/passage_manager.py +15 -0
- letta/settings.py +5 -0
- letta/supervisor_multi_agent.py +103 -0
- {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314222759.dist-info}/METADATA +1 -2
- {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314222759.dist-info}/RECORD +63 -49
- letta/helpers/mcp_helpers.py +0 -108
- letta/serialize_schemas/agent.py +0 -80
- letta/serialize_schemas/base.py +0 -64
- letta/serialize_schemas/message.py +0 -29
- {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314222759.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314222759.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314222759.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Annotated, Literal, Optional, Union
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MessageContentType(str, Enum):
|
|
8
|
+
text = "text"
|
|
9
|
+
tool_call = "tool_call"
|
|
10
|
+
tool_return = "tool_return"
|
|
11
|
+
reasoning = "reasoning"
|
|
12
|
+
redacted_reasoning = "redacted_reasoning"
|
|
13
|
+
omitted_reasoning = "omitted_reasoning"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MessageContent(BaseModel):
|
|
17
|
+
type: MessageContentType = Field(..., description="The type of the message.")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# -------------------------------
|
|
21
|
+
# User Content Types
|
|
22
|
+
# -------------------------------
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TextContent(MessageContent):
|
|
26
|
+
type: Literal[MessageContentType.text] = Field(MessageContentType.text, description="The type of the message.")
|
|
27
|
+
text: str = Field(..., description="The text content of the message.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
LettaUserMessageContentUnion = Annotated[
|
|
31
|
+
Union[TextContent],
|
|
32
|
+
Field(discriminator="type"),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create_letta_user_message_content_union_schema():
|
|
37
|
+
return {
|
|
38
|
+
"oneOf": [
|
|
39
|
+
{"$ref": "#/components/schemas/TextContent"},
|
|
40
|
+
],
|
|
41
|
+
"discriminator": {
|
|
42
|
+
"propertyName": "type",
|
|
43
|
+
"mapping": {
|
|
44
|
+
"text": "#/components/schemas/TextContent",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_letta_user_message_content_union_str_json_schema():
|
|
51
|
+
return {
|
|
52
|
+
"anyOf": [
|
|
53
|
+
{
|
|
54
|
+
"type": "array",
|
|
55
|
+
"items": {
|
|
56
|
+
"$ref": "#/components/schemas/LettaUserMessageContentUnion",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{"type": "string"},
|
|
60
|
+
],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# -------------------------------
|
|
65
|
+
# Assistant Content Types
|
|
66
|
+
# -------------------------------
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
LettaAssistantMessageContentUnion = Annotated[
|
|
70
|
+
Union[TextContent],
|
|
71
|
+
Field(discriminator="type"),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def create_letta_assistant_message_content_union_schema():
|
|
76
|
+
return {
|
|
77
|
+
"oneOf": [
|
|
78
|
+
{"$ref": "#/components/schemas/TextContent"},
|
|
79
|
+
],
|
|
80
|
+
"discriminator": {
|
|
81
|
+
"propertyName": "type",
|
|
82
|
+
"mapping": {
|
|
83
|
+
"text": "#/components/schemas/TextContent",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_letta_assistant_message_content_union_str_json_schema():
|
|
90
|
+
return {
|
|
91
|
+
"anyOf": [
|
|
92
|
+
{
|
|
93
|
+
"type": "array",
|
|
94
|
+
"items": {
|
|
95
|
+
"$ref": "#/components/schemas/LettaAssistantMessageContentUnion",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{"type": "string"},
|
|
99
|
+
],
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# -------------------------------
|
|
104
|
+
# Intermediate Step Content Types
|
|
105
|
+
# -------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ToolCallContent(MessageContent):
|
|
109
|
+
type: Literal[MessageContentType.tool_call] = Field(
|
|
110
|
+
MessageContentType.tool_call, description="Indicates this content represents a tool call event."
|
|
111
|
+
)
|
|
112
|
+
id: str = Field(..., description="A unique identifier for this specific tool call instance.")
|
|
113
|
+
name: str = Field(..., description="The name of the tool being called.")
|
|
114
|
+
input: dict = Field(
|
|
115
|
+
..., description="The parameters being passed to the tool, structured as a dictionary of parameter names to values."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ToolReturnContent(MessageContent):
|
|
120
|
+
type: Literal[MessageContentType.tool_return] = Field(
|
|
121
|
+
MessageContentType.tool_return, description="Indicates this content represents a tool return event."
|
|
122
|
+
)
|
|
123
|
+
tool_call_id: str = Field(..., description="References the ID of the ToolCallContent that initiated this tool call.")
|
|
124
|
+
content: str = Field(..., description="The content returned by the tool execution.")
|
|
125
|
+
is_error: bool = Field(..., description="Indicates whether the tool execution resulted in an error.")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ReasoningContent(MessageContent):
|
|
129
|
+
type: Literal[MessageContentType.reasoning] = Field(
|
|
130
|
+
MessageContentType.reasoning, description="Indicates this is a reasoning/intermediate step."
|
|
131
|
+
)
|
|
132
|
+
is_native: bool = Field(..., description="Whether the reasoning content was generated by a reasoner model that processed this step.")
|
|
133
|
+
reasoning: str = Field(..., description="The intermediate reasoning or thought process content.")
|
|
134
|
+
signature: Optional[str] = Field(None, description="A unique identifier for this reasoning step.")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class RedactedReasoningContent(MessageContent):
|
|
138
|
+
type: Literal[MessageContentType.redacted_reasoning] = Field(
|
|
139
|
+
MessageContentType.redacted_reasoning, description="Indicates this is a redacted thinking step."
|
|
140
|
+
)
|
|
141
|
+
data: str = Field(..., description="The redacted or filtered intermediate reasoning content.")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class OmittedReasoningContent(MessageContent):
|
|
145
|
+
type: Literal[MessageContentType.omitted_reasoning] = Field(
|
|
146
|
+
MessageContentType.omitted_reasoning, description="Indicates this is an omitted reasoning step."
|
|
147
|
+
)
|
|
148
|
+
tokens: int = Field(..., description="The reasoning token count for intermediate reasoning content.")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
LettaMessageContentUnion = Annotated[
|
|
152
|
+
Union[TextContent, ToolCallContent, ToolReturnContent, ReasoningContent, RedactedReasoningContent, OmittedReasoningContent],
|
|
153
|
+
Field(discriminator="type"),
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def create_letta_message_content_union_schema():
|
|
158
|
+
return {
|
|
159
|
+
"oneOf": [
|
|
160
|
+
{"$ref": "#/components/schemas/TextContent"},
|
|
161
|
+
{"$ref": "#/components/schemas/ToolCallContent"},
|
|
162
|
+
{"$ref": "#/components/schemas/ToolReturnContent"},
|
|
163
|
+
{"$ref": "#/components/schemas/ReasoningContent"},
|
|
164
|
+
{"$ref": "#/components/schemas/RedactedReasoningContent"},
|
|
165
|
+
{"$ref": "#/components/schemas/OmittedReasoningContent"},
|
|
166
|
+
],
|
|
167
|
+
"discriminator": {
|
|
168
|
+
"propertyName": "type",
|
|
169
|
+
"mapping": {
|
|
170
|
+
"text": "#/components/schemas/TextContent",
|
|
171
|
+
"tool_call": "#/components/schemas/ToolCallContent",
|
|
172
|
+
"tool_return": "#/components/schemas/ToolCallContent",
|
|
173
|
+
"reasoning": "#/components/schemas/ReasoningContent",
|
|
174
|
+
"redacted_reasoning": "#/components/schemas/RedactedReasoningContent",
|
|
175
|
+
"omitted_reasoning": "#/components/schemas/OmittedReasoningContent",
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_letta_message_content_union_str_json_schema():
|
|
182
|
+
return {
|
|
183
|
+
"anyOf": [
|
|
184
|
+
{
|
|
185
|
+
"type": "array",
|
|
186
|
+
"items": {
|
|
187
|
+
"$ref": "#/components/schemas/LettaMessageContentUnion",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{"type": "string"},
|
|
191
|
+
],
|
|
192
|
+
}
|
letta/schemas/message.py
CHANGED
|
@@ -9,26 +9,25 @@ from typing import Any, Dict, List, Literal, Optional, Union
|
|
|
9
9
|
|
|
10
10
|
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall
|
|
11
11
|
from openai.types.chat.chat_completion_message_tool_call import Function as OpenAIFunction
|
|
12
|
-
from pydantic import BaseModel, Field, field_validator
|
|
12
|
+
from pydantic import BaseModel, Field, field_validator
|
|
13
13
|
|
|
14
14
|
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, TOOL_CALL_ID_MAX_LEN
|
|
15
15
|
from letta.helpers.datetime_helpers import get_utc_time, is_utc_datetime
|
|
16
16
|
from letta.helpers.json_helpers import json_dumps
|
|
17
17
|
from letta.local_llm.constants import INNER_THOUGHTS_KWARG
|
|
18
|
-
from letta.schemas.enums import
|
|
18
|
+
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
22
|
LettaMessage,
|
|
23
|
-
MessageContentUnion,
|
|
24
23
|
ReasoningMessage,
|
|
25
24
|
SystemMessage,
|
|
26
|
-
TextContent,
|
|
27
25
|
ToolCall,
|
|
28
26
|
ToolCallMessage,
|
|
29
27
|
ToolReturnMessage,
|
|
30
28
|
UserMessage,
|
|
31
29
|
)
|
|
30
|
+
from letta.schemas.letta_message_content import LettaMessageContentUnion, TextContent, get_letta_message_content_union_str_json_schema
|
|
32
31
|
from letta.system import unpack_message
|
|
33
32
|
|
|
34
33
|
|
|
@@ -66,15 +65,30 @@ class MessageCreate(BaseModel):
|
|
|
66
65
|
MessageRole.user,
|
|
67
66
|
MessageRole.system,
|
|
68
67
|
] = Field(..., description="The role of the participant.")
|
|
69
|
-
content: Union[str, List[
|
|
68
|
+
content: Union[str, List[LettaMessageContentUnion]] = Field(
|
|
69
|
+
...,
|
|
70
|
+
description="The content of the message.",
|
|
71
|
+
json_schema_extra=get_letta_message_content_union_str_json_schema(),
|
|
72
|
+
)
|
|
70
73
|
name: Optional[str] = Field(None, description="The name of the participant.")
|
|
71
74
|
|
|
75
|
+
def model_dump(self, to_orm: bool = False, **kwargs) -> Dict[str, Any]:
|
|
76
|
+
data = super().model_dump(**kwargs)
|
|
77
|
+
if to_orm and "content" in data:
|
|
78
|
+
if isinstance(data["content"], str):
|
|
79
|
+
data["content"] = [TextContent(text=data["content"])]
|
|
80
|
+
return data
|
|
81
|
+
|
|
72
82
|
|
|
73
83
|
class MessageUpdate(BaseModel):
|
|
74
84
|
"""Request to update a message"""
|
|
75
85
|
|
|
76
86
|
role: Optional[MessageRole] = Field(None, description="The role of the participant.")
|
|
77
|
-
content: Optional[Union[str, List[
|
|
87
|
+
content: Optional[Union[str, List[LettaMessageContentUnion]]] = Field(
|
|
88
|
+
None,
|
|
89
|
+
description="The content of the message.",
|
|
90
|
+
json_schema_extra=get_letta_message_content_union_str_json_schema(),
|
|
91
|
+
)
|
|
78
92
|
# NOTE: probably doesn't make sense to allow remapping user_id or agent_id (vs creating a new message)
|
|
79
93
|
# user_id: Optional[str] = Field(None, description="The unique identifier of the user.")
|
|
80
94
|
# agent_id: Optional[str] = Field(None, description="The unique identifier of the agent.")
|
|
@@ -90,12 +104,7 @@ class MessageUpdate(BaseModel):
|
|
|
90
104
|
data = super().model_dump(**kwargs)
|
|
91
105
|
if to_orm and "content" in data:
|
|
92
106
|
if isinstance(data["content"], str):
|
|
93
|
-
data["
|
|
94
|
-
else:
|
|
95
|
-
for content in data["content"]:
|
|
96
|
-
if content["type"] == "text":
|
|
97
|
-
data["text"] = content["text"]
|
|
98
|
-
del data["content"]
|
|
107
|
+
data["content"] = [TextContent(text=data["content"])]
|
|
99
108
|
return data
|
|
100
109
|
|
|
101
110
|
|
|
@@ -119,7 +128,7 @@ class Message(BaseMessage):
|
|
|
119
128
|
|
|
120
129
|
id: str = BaseMessage.generate_id_field()
|
|
121
130
|
role: MessageRole = Field(..., description="The role of the participant.")
|
|
122
|
-
content: Optional[List[
|
|
131
|
+
content: Optional[List[LettaMessageContentUnion]] = Field(None, description="The content of the message.")
|
|
123
132
|
organization_id: Optional[str] = Field(None, description="The unique identifier of the organization.")
|
|
124
133
|
agent_id: Optional[str] = Field(None, description="The unique identifier of the agent.")
|
|
125
134
|
model: Optional[str] = Field(None, description="The model used to make the function call.")
|
|
@@ -129,6 +138,7 @@ class Message(BaseMessage):
|
|
|
129
138
|
step_id: Optional[str] = Field(None, description="The id of the step that this message was created in.")
|
|
130
139
|
otid: Optional[str] = Field(None, description="The offline threading id associated with this message")
|
|
131
140
|
tool_returns: Optional[List[ToolReturn]] = Field(None, description="Tool execution return information for prior tool calls")
|
|
141
|
+
group_id: Optional[str] = Field(None, description="The multi-agent group that the message was sent in")
|
|
132
142
|
|
|
133
143
|
# This overrides the optional base orm schema, created_at MUST exist on all messages objects
|
|
134
144
|
created_at: datetime = Field(default_factory=get_utc_time, description="The timestamp when the object was created.")
|
|
@@ -140,24 +150,6 @@ class Message(BaseMessage):
|
|
|
140
150
|
assert v in roles, f"Role must be one of {roles}"
|
|
141
151
|
return v
|
|
142
152
|
|
|
143
|
-
@model_validator(mode="before")
|
|
144
|
-
@classmethod
|
|
145
|
-
def convert_from_orm(cls, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
146
|
-
if isinstance(data, dict):
|
|
147
|
-
if "text" in data and "content" not in data:
|
|
148
|
-
data["content"] = [TextContent(text=data["text"])]
|
|
149
|
-
del data["text"]
|
|
150
|
-
return data
|
|
151
|
-
|
|
152
|
-
def model_dump(self, to_orm: bool = False, **kwargs) -> Dict[str, Any]:
|
|
153
|
-
data = super().model_dump(**kwargs)
|
|
154
|
-
if to_orm:
|
|
155
|
-
for content in data["content"]:
|
|
156
|
-
if content["type"] == "text":
|
|
157
|
-
data["text"] = content["text"]
|
|
158
|
-
del data["content"]
|
|
159
|
-
return data
|
|
160
|
-
|
|
161
153
|
def to_json(self):
|
|
162
154
|
json_message = vars(self)
|
|
163
155
|
if json_message["tool_calls"] is not None:
|
|
@@ -214,7 +206,7 @@ class Message(BaseMessage):
|
|
|
214
206
|
assistant_message_tool_kwarg: str = DEFAULT_MESSAGE_TOOL_KWARG,
|
|
215
207
|
) -> List[LettaMessage]:
|
|
216
208
|
"""Convert message object (in DB format) to the style used by the original Letta API"""
|
|
217
|
-
if self.content and len(self.content) == 1 and self.content[0]
|
|
209
|
+
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
|
218
210
|
text_content = self.content[0].text
|
|
219
211
|
else:
|
|
220
212
|
text_content = None
|
|
@@ -485,7 +477,7 @@ class Message(BaseMessage):
|
|
|
485
477
|
"""Go from Message class to ChatCompletion message object"""
|
|
486
478
|
|
|
487
479
|
# TODO change to pydantic casting, eg `return SystemMessageModel(self)`
|
|
488
|
-
if self.content and len(self.content) == 1 and self.content[0]
|
|
480
|
+
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
|
489
481
|
text_content = self.content[0].text
|
|
490
482
|
else:
|
|
491
483
|
text_content = None
|
|
@@ -560,7 +552,7 @@ class Message(BaseMessage):
|
|
|
560
552
|
Args:
|
|
561
553
|
inner_thoughts_xml_tag (str): The XML tag to wrap around inner thoughts
|
|
562
554
|
"""
|
|
563
|
-
if self.content and len(self.content) == 1 and self.content[0]
|
|
555
|
+
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
|
564
556
|
text_content = self.content[0].text
|
|
565
557
|
else:
|
|
566
558
|
text_content = None
|
|
@@ -655,7 +647,7 @@ class Message(BaseMessage):
|
|
|
655
647
|
# type Content: https://ai.google.dev/api/rest/v1/Content / https://ai.google.dev/api/rest/v1beta/Content
|
|
656
648
|
# parts[]: Part
|
|
657
649
|
# role: str ('user' or 'model')
|
|
658
|
-
if self.content and len(self.content) == 1 and self.content[0]
|
|
650
|
+
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
|
659
651
|
text_content = self.content[0].text
|
|
660
652
|
else:
|
|
661
653
|
text_content = None
|
|
@@ -781,7 +773,7 @@ class Message(BaseMessage):
|
|
|
781
773
|
|
|
782
774
|
# TODO: update this prompt style once guidance from Cohere on
|
|
783
775
|
# embedded function calls in multi-turn conversation become more clear
|
|
784
|
-
if self.content and len(self.content) == 1 and self.content[0]
|
|
776
|
+
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
|
785
777
|
text_content = self.content[0].text
|
|
786
778
|
else:
|
|
787
779
|
text_content = None
|
letta/schemas/tool.py
CHANGED
|
@@ -17,12 +17,12 @@ from letta.functions.helpers import (
|
|
|
17
17
|
generate_mcp_tool_wrapper,
|
|
18
18
|
generate_model_from_args_json_schema,
|
|
19
19
|
)
|
|
20
|
+
from letta.functions.mcp_client.types import MCPTool
|
|
20
21
|
from letta.functions.schema_generator import (
|
|
21
22
|
generate_schema_from_args_schema_v2,
|
|
22
23
|
generate_tool_schema_for_composio,
|
|
23
24
|
generate_tool_schema_for_mcp,
|
|
24
25
|
)
|
|
25
|
-
from letta.helpers.mcp_helpers import MCPTool
|
|
26
26
|
from letta.log import get_logger
|
|
27
27
|
from letta.orm.enums import ToolType
|
|
28
28
|
from letta.schemas.letta_base import LettaBase
|
|
@@ -1 +1 @@
|
|
|
1
|
-
from letta.serialize_schemas.
|
|
1
|
+
from letta.serialize_schemas.marshmallow_agent import MarshmallowAgentSchema
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from marshmallow import fields, post_dump, pre_load
|
|
4
|
+
|
|
5
|
+
import letta
|
|
6
|
+
from letta.orm import Agent
|
|
7
|
+
from letta.schemas.agent import AgentState as PydanticAgentState
|
|
8
|
+
from letta.schemas.user import User
|
|
9
|
+
from letta.serialize_schemas.marshmallow_agent_environment_variable import SerializedAgentEnvironmentVariableSchema
|
|
10
|
+
from letta.serialize_schemas.marshmallow_base import BaseSchema
|
|
11
|
+
from letta.serialize_schemas.marshmallow_block import SerializedBlockSchema
|
|
12
|
+
from letta.serialize_schemas.marshmallow_custom_fields import EmbeddingConfigField, LLMConfigField, ToolRulesField
|
|
13
|
+
from letta.serialize_schemas.marshmallow_message import SerializedMessageSchema
|
|
14
|
+
from letta.serialize_schemas.marshmallow_tag import SerializedAgentTagSchema
|
|
15
|
+
from letta.serialize_schemas.marshmallow_tool import SerializedToolSchema
|
|
16
|
+
from letta.server.db import SessionLocal
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MarshmallowAgentSchema(BaseSchema):
|
|
20
|
+
"""
|
|
21
|
+
Marshmallow schema for serializing/deserializing Agent objects.
|
|
22
|
+
Excludes relational fields.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__pydantic_model__ = PydanticAgentState
|
|
26
|
+
|
|
27
|
+
FIELD_VERSION = "version"
|
|
28
|
+
FIELD_MESSAGES = "messages"
|
|
29
|
+
FIELD_MESSAGE_IDS = "message_ids"
|
|
30
|
+
FIELD_IN_CONTEXT = "in_context"
|
|
31
|
+
FIELD_ID = "id"
|
|
32
|
+
|
|
33
|
+
llm_config = LLMConfigField()
|
|
34
|
+
embedding_config = EmbeddingConfigField()
|
|
35
|
+
tool_rules = ToolRulesField()
|
|
36
|
+
|
|
37
|
+
messages = fields.List(fields.Nested(SerializedMessageSchema))
|
|
38
|
+
core_memory = fields.List(fields.Nested(SerializedBlockSchema))
|
|
39
|
+
tools = fields.List(fields.Nested(SerializedToolSchema))
|
|
40
|
+
tool_exec_environment_variables = fields.List(fields.Nested(SerializedAgentEnvironmentVariableSchema))
|
|
41
|
+
tags = fields.List(fields.Nested(SerializedAgentTagSchema))
|
|
42
|
+
|
|
43
|
+
def __init__(self, *args, session: SessionLocal, actor: User, **kwargs):
|
|
44
|
+
super().__init__(*args, actor=actor, **kwargs)
|
|
45
|
+
self.session = session
|
|
46
|
+
|
|
47
|
+
# Propagate session and actor to nested schemas automatically
|
|
48
|
+
for field in self.fields.values():
|
|
49
|
+
if isinstance(field, fields.List) and isinstance(field.inner, fields.Nested):
|
|
50
|
+
field.inner.schema.session = session
|
|
51
|
+
field.inner.schema.actor = actor
|
|
52
|
+
elif isinstance(field, fields.Nested):
|
|
53
|
+
field.schema.session = session
|
|
54
|
+
field.schema.actor = actor
|
|
55
|
+
|
|
56
|
+
@post_dump
|
|
57
|
+
def sanitize_ids(self, data: Dict, **kwargs):
|
|
58
|
+
"""
|
|
59
|
+
- Removes `message_ids`
|
|
60
|
+
- Adds versioning
|
|
61
|
+
- Marks messages as in-context
|
|
62
|
+
- Removes individual message `id` fields
|
|
63
|
+
"""
|
|
64
|
+
data = super().sanitize_ids(data, **kwargs)
|
|
65
|
+
data[self.FIELD_VERSION] = letta.__version__
|
|
66
|
+
|
|
67
|
+
message_ids = set(data.pop(self.FIELD_MESSAGE_IDS, [])) # Store and remove message_ids
|
|
68
|
+
|
|
69
|
+
for message in data.get(self.FIELD_MESSAGES, []):
|
|
70
|
+
message[self.FIELD_IN_CONTEXT] = message[self.FIELD_ID] in message_ids # Mark messages as in-context
|
|
71
|
+
message.pop(self.FIELD_ID, None) # Remove the id field
|
|
72
|
+
|
|
73
|
+
return data
|
|
74
|
+
|
|
75
|
+
@pre_load
|
|
76
|
+
def check_version(self, data, **kwargs):
|
|
77
|
+
"""Check version and remove it from the schema"""
|
|
78
|
+
version = data[self.FIELD_VERSION]
|
|
79
|
+
if version != letta.__version__:
|
|
80
|
+
print(f"Version mismatch: expected {letta.__version__}, got {version}")
|
|
81
|
+
del data[self.FIELD_VERSION]
|
|
82
|
+
return data
|
|
83
|
+
|
|
84
|
+
@pre_load
|
|
85
|
+
def remap_in_context_messages(self, data, **kwargs):
|
|
86
|
+
"""
|
|
87
|
+
Restores `message_ids` by collecting message IDs where `in_context` is True,
|
|
88
|
+
generates new IDs for all messages, and removes `in_context` from all messages.
|
|
89
|
+
"""
|
|
90
|
+
message_ids = []
|
|
91
|
+
for msg in data.get(self.FIELD_MESSAGES, []):
|
|
92
|
+
msg[self.FIELD_ID] = SerializedMessageSchema.generate_id() # Generate new ID
|
|
93
|
+
if msg.pop(self.FIELD_IN_CONTEXT, False): # If it was in-context, track its new ID
|
|
94
|
+
message_ids.append(msg[self.FIELD_ID])
|
|
95
|
+
|
|
96
|
+
data[self.FIELD_MESSAGE_IDS] = message_ids
|
|
97
|
+
return data
|
|
98
|
+
|
|
99
|
+
class Meta(BaseSchema.Meta):
|
|
100
|
+
model = Agent
|
|
101
|
+
exclude = BaseSchema.Meta.exclude + (
|
|
102
|
+
"project_id",
|
|
103
|
+
"template_id",
|
|
104
|
+
"base_template_id",
|
|
105
|
+
"sources",
|
|
106
|
+
"source_passages",
|
|
107
|
+
"agent_passages",
|
|
108
|
+
)
|
letta/serialize_schemas/{agent_environment_variable.py → marshmallow_agent_environment_variable.py}
RENAMED
|
@@ -2,7 +2,7 @@ import uuid
|
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
4
|
from letta.orm.sandbox_config import AgentEnvironmentVariable
|
|
5
|
-
from letta.serialize_schemas.
|
|
5
|
+
from letta.serialize_schemas.marshmallow_base import BaseSchema
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class SerializedAgentEnvironmentVariableSchema(BaseSchema):
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Dict, Optional
|
|
2
|
+
|
|
3
|
+
from marshmallow import post_dump, pre_load
|
|
4
|
+
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
|
|
5
|
+
|
|
6
|
+
from letta.schemas.user import User
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseSchema(SQLAlchemyAutoSchema):
|
|
10
|
+
"""
|
|
11
|
+
Base schema for all SQLAlchemy models.
|
|
12
|
+
This ensures all schemas share the same session.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__pydantic_model__ = None
|
|
16
|
+
|
|
17
|
+
def __init__(self, *args, actor: Optional[User] = None, **kwargs):
|
|
18
|
+
super().__init__(*args, **kwargs)
|
|
19
|
+
self.actor = actor
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def generate_id(cls) -> Optional[str]:
|
|
23
|
+
if cls.__pydantic_model__:
|
|
24
|
+
return cls.__pydantic_model__.generate_id()
|
|
25
|
+
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
@post_dump
|
|
29
|
+
def sanitize_ids(self, data: Dict, **kwargs) -> Dict:
|
|
30
|
+
# delete id
|
|
31
|
+
del data["id"]
|
|
32
|
+
del data["_created_by_id"]
|
|
33
|
+
del data["_last_updated_by_id"]
|
|
34
|
+
del data["organization"]
|
|
35
|
+
|
|
36
|
+
return data
|
|
37
|
+
|
|
38
|
+
@pre_load
|
|
39
|
+
def regenerate_ids(self, data: Dict, **kwargs) -> Dict:
|
|
40
|
+
if self.Meta.model:
|
|
41
|
+
data["id"] = self.generate_id()
|
|
42
|
+
data["_created_by_id"] = self.actor.id
|
|
43
|
+
data["_last_updated_by_id"] = self.actor.id
|
|
44
|
+
data["organization"] = self.actor.organization_id
|
|
45
|
+
|
|
46
|
+
return data
|
|
47
|
+
|
|
48
|
+
class Meta:
|
|
49
|
+
model = None
|
|
50
|
+
include_relationships = True
|
|
51
|
+
load_instance = True
|
|
52
|
+
exclude = ()
|
|
@@ -3,10 +3,12 @@ from marshmallow import fields
|
|
|
3
3
|
from letta.helpers.converters import (
|
|
4
4
|
deserialize_embedding_config,
|
|
5
5
|
deserialize_llm_config,
|
|
6
|
+
deserialize_message_content,
|
|
6
7
|
deserialize_tool_calls,
|
|
7
8
|
deserialize_tool_rules,
|
|
8
9
|
serialize_embedding_config,
|
|
9
10
|
serialize_llm_config,
|
|
11
|
+
serialize_message_content,
|
|
10
12
|
serialize_tool_calls,
|
|
11
13
|
serialize_tool_rules,
|
|
12
14
|
)
|
|
@@ -67,3 +69,13 @@ class ToolCallField(fields.Field):
|
|
|
67
69
|
|
|
68
70
|
def _deserialize(self, value, attr, data, **kwargs):
|
|
69
71
|
return deserialize_tool_calls(value)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class MessageContentField(fields.Field):
|
|
75
|
+
"""Marshmallow field for handling a list of Message Content Part objects."""
|
|
76
|
+
|
|
77
|
+
def _serialize(self, value, attr, obj, **kwargs):
|
|
78
|
+
return serialize_message_content(value)
|
|
79
|
+
|
|
80
|
+
def _deserialize(self, value, attr, data, **kwargs):
|
|
81
|
+
return deserialize_message_content(value)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from marshmallow import post_dump, pre_load
|
|
4
|
+
|
|
5
|
+
from letta.orm.message import Message
|
|
6
|
+
from letta.schemas.message import Message as PydanticMessage
|
|
7
|
+
from letta.serialize_schemas.marshmallow_base import BaseSchema
|
|
8
|
+
from letta.serialize_schemas.marshmallow_custom_fields import ToolCallField
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SerializedMessageSchema(BaseSchema):
|
|
12
|
+
"""
|
|
13
|
+
Marshmallow schema for serializing/deserializing Message objects.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__pydantic_model__ = PydanticMessage
|
|
17
|
+
|
|
18
|
+
tool_calls = ToolCallField()
|
|
19
|
+
|
|
20
|
+
@post_dump
|
|
21
|
+
def sanitize_ids(self, data: Dict, **kwargs) -> Dict:
|
|
22
|
+
# keep id for remapping later on agent dump
|
|
23
|
+
# agent dump will then get rid of message ids
|
|
24
|
+
del data["_created_by_id"]
|
|
25
|
+
del data["_last_updated_by_id"]
|
|
26
|
+
del data["organization"]
|
|
27
|
+
|
|
28
|
+
return data
|
|
29
|
+
|
|
30
|
+
@pre_load
|
|
31
|
+
def regenerate_ids(self, data: Dict, **kwargs) -> Dict:
|
|
32
|
+
if self.Meta.model:
|
|
33
|
+
# Skip regenerating ID, as agent dump will do it
|
|
34
|
+
data["_created_by_id"] = self.actor.id
|
|
35
|
+
data["_last_updated_by_id"] = self.actor.id
|
|
36
|
+
data["organization"] = self.actor.organization_id
|
|
37
|
+
|
|
38
|
+
return data
|
|
39
|
+
|
|
40
|
+
class Meta(BaseSchema.Meta):
|
|
41
|
+
model = Message
|
|
42
|
+
exclude = BaseSchema.Meta.exclude + ("step", "job_message", "agent", "otid", "is_deleted")
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from marshmallow import fields, post_dump, pre_load
|
|
2
4
|
|
|
3
5
|
from letta.orm.agents_tags import AgentsTags
|
|
4
|
-
from letta.serialize_schemas.
|
|
6
|
+
from letta.serialize_schemas.marshmallow_base import BaseSchema
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
class SerializedAgentTagSchema(BaseSchema):
|
|
@@ -13,6 +15,14 @@ class SerializedAgentTagSchema(BaseSchema):
|
|
|
13
15
|
|
|
14
16
|
tag = fields.String(required=True)
|
|
15
17
|
|
|
18
|
+
@post_dump
|
|
19
|
+
def sanitize_ids(self, data: Dict, **kwargs):
|
|
20
|
+
return data
|
|
21
|
+
|
|
22
|
+
@pre_load
|
|
23
|
+
def regenerate_ids(self, data: Dict, **kwargs) -> Dict:
|
|
24
|
+
return data
|
|
25
|
+
|
|
16
26
|
class Meta(BaseSchema.Meta):
|
|
17
27
|
model = AgentsTags
|
|
18
28
|
exclude = BaseSchema.Meta.exclude + ("agent",)
|