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.

Files changed (67) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +14 -4
  3. letta/agents/ephemeral_agent.py +2 -1
  4. letta/agents/low_latency_agent.py +8 -0
  5. letta/dynamic_multi_agent.py +274 -0
  6. letta/functions/function_sets/base.py +1 -0
  7. letta/functions/function_sets/extras.py +2 -1
  8. letta/functions/function_sets/multi_agent.py +17 -0
  9. letta/functions/helpers.py +41 -0
  10. letta/functions/mcp_client/__init__.py +0 -0
  11. letta/functions/mcp_client/base_client.py +61 -0
  12. letta/functions/mcp_client/sse_client.py +21 -0
  13. letta/functions/mcp_client/stdio_client.py +103 -0
  14. letta/functions/mcp_client/types.py +48 -0
  15. letta/functions/schema_generator.py +1 -1
  16. letta/helpers/converters.py +67 -0
  17. letta/llm_api/openai.py +1 -1
  18. letta/memory.py +2 -1
  19. letta/orm/__init__.py +2 -0
  20. letta/orm/agent.py +69 -20
  21. letta/orm/custom_columns.py +15 -0
  22. letta/orm/group.py +33 -0
  23. letta/orm/groups_agents.py +13 -0
  24. letta/orm/message.py +7 -4
  25. letta/orm/organization.py +1 -0
  26. letta/orm/sqlalchemy_base.py +3 -3
  27. letta/round_robin_multi_agent.py +152 -0
  28. letta/schemas/agent.py +3 -0
  29. letta/schemas/enums.py +0 -4
  30. letta/schemas/group.py +65 -0
  31. letta/schemas/letta_message.py +167 -106
  32. letta/schemas/letta_message_content.py +192 -0
  33. letta/schemas/message.py +28 -36
  34. letta/schemas/tool.py +1 -1
  35. letta/serialize_schemas/__init__.py +1 -1
  36. letta/serialize_schemas/marshmallow_agent.py +108 -0
  37. letta/serialize_schemas/{agent_environment_variable.py → marshmallow_agent_environment_variable.py} +1 -1
  38. letta/serialize_schemas/marshmallow_base.py +52 -0
  39. letta/serialize_schemas/{block.py → marshmallow_block.py} +1 -1
  40. letta/serialize_schemas/{custom_fields.py → marshmallow_custom_fields.py} +12 -0
  41. letta/serialize_schemas/marshmallow_message.py +42 -0
  42. letta/serialize_schemas/{tag.py → marshmallow_tag.py} +12 -2
  43. letta/serialize_schemas/{tool.py → marshmallow_tool.py} +1 -1
  44. letta/serialize_schemas/pydantic_agent_schema.py +111 -0
  45. letta/server/rest_api/app.py +15 -0
  46. letta/server/rest_api/routers/v1/__init__.py +2 -0
  47. letta/server/rest_api/routers/v1/agents.py +46 -40
  48. letta/server/rest_api/routers/v1/groups.py +233 -0
  49. letta/server/rest_api/routers/v1/tools.py +31 -3
  50. letta/server/rest_api/utils.py +1 -1
  51. letta/server/server.py +272 -22
  52. letta/services/agent_manager.py +65 -28
  53. letta/services/group_manager.py +147 -0
  54. letta/services/helpers/agent_manager_helper.py +151 -1
  55. letta/services/message_manager.py +11 -3
  56. letta/services/passage_manager.py +15 -0
  57. letta/settings.py +5 -0
  58. letta/supervisor_multi_agent.py +103 -0
  59. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314222759.dist-info}/METADATA +1 -2
  60. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314222759.dist-info}/RECORD +63 -49
  61. letta/helpers/mcp_helpers.py +0 -108
  62. letta/serialize_schemas/agent.py +0 -80
  63. letta/serialize_schemas/base.py +0 -64
  64. letta/serialize_schemas/message.py +0 -29
  65. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314222759.dist-info}/LICENSE +0 -0
  66. {letta_nightly-0.6.39.dev20250314104053.dist-info → letta_nightly-0.6.40.dev20250314222759.dist-info}/WHEEL +0 -0
  67. {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, model_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 MessageContentType, MessageRole
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[MessageContentUnion]] = Field(..., description="The content of the message.")
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[MessageContentUnion]]] = Field(None, description="The content of the message.")
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["text"] = data["content"]
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[MessageContentUnion]] = Field(None, description="The content of the message.")
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].type == MessageContentType.text:
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].type == MessageContentType.text:
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].type == MessageContentType.text:
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].type == MessageContentType.text:
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].type == MessageContentType.text:
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.agent import SerializedAgentSchema
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
+ )
@@ -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.base import BaseSchema
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 = ()
@@ -1,6 +1,6 @@
1
1
  from letta.orm.block import Block
2
2
  from letta.schemas.block import Block as PydanticBlock
3
- from letta.serialize_schemas.base import BaseSchema
3
+ from letta.serialize_schemas.marshmallow_base import BaseSchema
4
4
 
5
5
 
6
6
  class SerializedBlockSchema(BaseSchema):
@@ -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 marshmallow import fields
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.base import BaseSchema
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",)
@@ -1,6 +1,6 @@
1
1
  from letta.orm import Tool
2
2
  from letta.schemas.tool import Tool as PydanticTool
3
- from letta.serialize_schemas.base import BaseSchema
3
+ from letta.serialize_schemas.marshmallow_base import BaseSchema
4
4
 
5
5
 
6
6
  class SerializedToolSchema(BaseSchema):