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,152 @@
1
+ from typing import List, Optional
2
+
3
+ from letta.agent import Agent, AgentState
4
+ from letta.interface import AgentInterface
5
+ from letta.orm import User
6
+ from letta.schemas.letta_message_content import TextContent
7
+ from letta.schemas.message import Message, MessageCreate
8
+ from letta.schemas.openai.chat_completion_response import UsageStatistics
9
+ from letta.schemas.usage import LettaUsageStatistics
10
+
11
+
12
+ class RoundRobinMultiAgent(Agent):
13
+ def __init__(
14
+ self,
15
+ interface: AgentInterface,
16
+ agent_state: AgentState,
17
+ user: User = None,
18
+ # custom
19
+ group_id: str = "",
20
+ agent_ids: List[str] = [],
21
+ description: str = "",
22
+ max_turns: Optional[int] = None,
23
+ ):
24
+ super().__init__(interface, agent_state, user)
25
+ self.group_id = group_id
26
+ self.agent_ids = agent_ids
27
+ self.description = description
28
+ self.max_turns = max_turns or len(agent_ids)
29
+
30
+ def step(
31
+ self,
32
+ messages: List[MessageCreate],
33
+ chaining: bool = True,
34
+ max_chaining_steps: Optional[int] = None,
35
+ put_inner_thoughts_first: bool = True,
36
+ **kwargs,
37
+ ) -> LettaUsageStatistics:
38
+ total_usage = UsageStatistics()
39
+ step_count = 0
40
+
41
+ token_streaming = self.interface.streaming_mode if hasattr(self.interface, "streaming_mode") else False
42
+ metadata = self.interface.metadata if hasattr(self.interface, "metadata") else None
43
+
44
+ agents = {}
45
+ for agent_id in self.agent_ids:
46
+ agents[agent_id] = self.load_participant_agent(agent_id=agent_id)
47
+
48
+ message_index = {}
49
+ chat_history: List[Message] = []
50
+ new_messages = messages
51
+ speaker_id = None
52
+ try:
53
+ for i in range(self.max_turns):
54
+ speaker_id = self.agent_ids[i % len(self.agent_ids)]
55
+ # initialize input messages
56
+ start_index = message_index[speaker_id] if speaker_id in message_index else 0
57
+ for message in chat_history[start_index:]:
58
+ message.id = Message.generate_id()
59
+ message.agent_id = speaker_id
60
+
61
+ for message in new_messages:
62
+ chat_history.append(
63
+ Message(
64
+ agent_id=speaker_id,
65
+ role=message.role,
66
+ content=[TextContent(text=message.content)],
67
+ name=message.name,
68
+ model=None,
69
+ tool_calls=None,
70
+ tool_call_id=None,
71
+ group_id=self.group_id,
72
+ )
73
+ )
74
+
75
+ # load agent and perform step
76
+ participant_agent = agents[speaker_id]
77
+ usage_stats = participant_agent.step(
78
+ messages=chat_history[start_index:],
79
+ chaining=chaining,
80
+ max_chaining_steps=max_chaining_steps,
81
+ stream=token_streaming,
82
+ skip_verify=True,
83
+ metadata=metadata,
84
+ put_inner_thoughts_first=put_inner_thoughts_first,
85
+ )
86
+
87
+ # parse new messages for next step
88
+ responses = Message.to_letta_messages_from_list(participant_agent.last_response_messages)
89
+ assistant_messages = [response for response in responses if response.message_type == "assistant_message"]
90
+ new_messages = [
91
+ MessageCreate(
92
+ role="system",
93
+ content=message.content,
94
+ name=participant_agent.agent_state.name,
95
+ )
96
+ for message in assistant_messages
97
+ ]
98
+ message_index[speaker_id] = len(chat_history) + len(new_messages)
99
+
100
+ # sum usage
101
+ total_usage.prompt_tokens += usage_stats.prompt_tokens
102
+ total_usage.completion_tokens += usage_stats.completion_tokens
103
+ total_usage.total_tokens += usage_stats.total_tokens
104
+ step_count += 1
105
+
106
+ # persist remaining chat history
107
+ for message in new_messages:
108
+ chat_history.append(
109
+ Message(
110
+ agent_id=agent_id,
111
+ role=message.role,
112
+ content=[TextContent(text=message.content)],
113
+ name=message.name,
114
+ model=None,
115
+ tool_calls=None,
116
+ tool_call_id=None,
117
+ group_id=self.group_id,
118
+ )
119
+ )
120
+ for agent_id, index in message_index.items():
121
+ if agent_id == speaker_id:
122
+ continue
123
+ for message in chat_history[index:]:
124
+ message.id = Message.generate_id()
125
+ message.agent_id = agent_id
126
+ self.message_manager.create_many_messages(chat_history[index:], actor=self.user)
127
+
128
+ except Exception as e:
129
+ raise e
130
+ finally:
131
+ self.interface.step_yield()
132
+
133
+ self.interface.step_complete()
134
+
135
+ return LettaUsageStatistics(**total_usage.model_dump(), step_count=step_count)
136
+
137
+ def load_participant_agent(self, agent_id: str) -> Agent:
138
+ agent_state = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=self.user)
139
+ persona_block = agent_state.memory.get_block(label="persona")
140
+ group_chat_participant_persona = (
141
+ "\n\n====Group Chat Contex===="
142
+ f"\nYou are speaking in a group chat with {len(self.agent_ids) - 1} other "
143
+ "agents and one user. Respond to new messages in the group chat when prompted. "
144
+ f"Description of the group: {self.description}"
145
+ )
146
+ agent_state.memory.update_block_value(label="persona", value=persona_block.value + group_chat_participant_persona)
147
+ return Agent(
148
+ agent_state=agent_state,
149
+ interface=self.interface,
150
+ user=self.user,
151
+ save_last_response=True,
152
+ )
letta/schemas/agent.py CHANGED
@@ -7,6 +7,7 @@ from letta.constants import DEFAULT_EMBEDDING_CHUNK_SIZE
7
7
  from letta.schemas.block import CreateBlock
8
8
  from letta.schemas.embedding_config import EmbeddingConfig
9
9
  from letta.schemas.environment_variables import AgentEnvironmentVariable
10
+ from letta.schemas.group import Group
10
11
  from letta.schemas.letta_base import OrmMetadataBase
11
12
  from letta.schemas.llm_config import LLMConfig
12
13
  from letta.schemas.memory import Memory
@@ -90,6 +91,8 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
90
91
  description="If set to True, the agent will not remember previous messages (though the agent will still retain state via core memory blocks and archival/recall memory). Not recommended unless you have an advanced use case.",
91
92
  )
92
93
 
94
+ multi_agent_group: Optional[Group] = Field(None, description="The multi-agent group that this agent manages")
95
+
93
96
  def get_agent_env_vars_as_dict(self) -> Dict[str, str]:
94
97
  # Get environment variables for this agent specifically
95
98
  per_agent_env_vars = {}
letta/schemas/enums.py CHANGED
@@ -9,10 +9,6 @@ class MessageRole(str, Enum):
9
9
  system = "system"
10
10
 
11
11
 
12
- class MessageContentType(str, Enum):
13
- text = "text"
14
-
15
-
16
12
  class OptionState(str, Enum):
17
13
  """Useful for kwargs that are bool + default option"""
18
14
 
letta/schemas/group.py ADDED
@@ -0,0 +1,65 @@
1
+ from enum import Enum
2
+ from typing import Annotated, List, Literal, Optional, Union
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ from letta.schemas.letta_base import LettaBase
7
+
8
+
9
+ class ManagerType(str, Enum):
10
+ round_robin = "round_robin"
11
+ supervisor = "supervisor"
12
+ dynamic = "dynamic"
13
+ swarm = "swarm"
14
+
15
+
16
+ class GroupBase(LettaBase):
17
+ __id_prefix__ = "group"
18
+
19
+
20
+ class Group(GroupBase):
21
+ id: str = Field(..., description="The id of the group. Assigned by the database.")
22
+ manager_type: ManagerType = Field(..., description="")
23
+ agent_ids: List[str] = Field(..., description="")
24
+ description: str = Field(..., description="")
25
+ # Pattern fields
26
+ manager_agent_id: Optional[str] = Field(None, description="")
27
+ termination_token: Optional[str] = Field(None, description="")
28
+ max_turns: Optional[int] = Field(None, description="")
29
+
30
+
31
+ class ManagerConfig(BaseModel):
32
+ manager_type: ManagerType = Field(..., description="")
33
+
34
+
35
+ class RoundRobinManager(ManagerConfig):
36
+ manager_type: Literal[ManagerType.round_robin] = Field(ManagerType.round_robin, description="")
37
+ max_turns: Optional[int] = Field(None, description="")
38
+
39
+
40
+ class SupervisorManager(ManagerConfig):
41
+ manager_type: Literal[ManagerType.supervisor] = Field(ManagerType.supervisor, description="")
42
+ manager_agent_id: str = Field(..., description="")
43
+
44
+
45
+ class DynamicManager(ManagerConfig):
46
+ manager_type: Literal[ManagerType.dynamic] = Field(ManagerType.dynamic, description="")
47
+ manager_agent_id: str = Field(..., description="")
48
+ termination_token: Optional[str] = Field("DONE!", description="")
49
+ max_turns: Optional[int] = Field(None, description="")
50
+
51
+
52
+ # class SwarmGroup(ManagerConfig):
53
+ # manager_type: Literal[ManagerType.swarm] = Field(ManagerType.swarm, description="")
54
+
55
+
56
+ ManagerConfigUnion = Annotated[
57
+ Union[RoundRobinManager, SupervisorManager, DynamicManager],
58
+ Field(discriminator="manager_type"),
59
+ ]
60
+
61
+
62
+ class GroupCreate(BaseModel):
63
+ agent_ids: List[str] = Field(..., description="")
64
+ description: str = Field(..., description="")
65
+ manager_config: Optional[ManagerConfigUnion] = Field(None, description="")
@@ -4,109 +4,132 @@ from typing import Annotated, List, Literal, Optional, Union
4
4
 
5
5
  from pydantic import BaseModel, Field, field_serializer, field_validator
6
6
 
7
- from letta.schemas.enums import MessageContentType
7
+ from letta.schemas.letta_message_content import (
8
+ LettaAssistantMessageContentUnion,
9
+ LettaUserMessageContentUnion,
10
+ get_letta_assistant_message_content_union_str_json_schema,
11
+ get_letta_user_message_content_union_str_json_schema,
12
+ )
8
13
 
9
- # Letta API style responses (intended to be easier to use vs getting true Message types)
14
+ # ---------------------------
15
+ # Letta API Messaging Schemas
16
+ # ---------------------------
10
17
 
11
18
 
12
19
  class LettaMessage(BaseModel):
13
20
  """
14
- Base class for simplified Letta message response type. This is intended to be used for developers who want the internal monologue, tool calls, and tool returns in a simplified format that does not include additional information other than the content and timestamp.
21
+ Base class for simplified Letta message response type. This is intended to be used for developers
22
+ who want the internal monologue, tool calls, and tool returns in a simplified format that does not
23
+ include additional information other than the content and timestamp.
15
24
 
16
- Attributes:
25
+ Args:
17
26
  id (str): The ID of the message
18
27
  date (datetime): The date the message was created in ISO format
19
-
28
+ name (Optional[str]): The name of the sender of the message
20
29
  """
21
30
 
22
- # NOTE: use Pydantic's discriminated unions feature: https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions
23
- # see `message_type` attribute
24
-
25
31
  id: str
26
32
  date: datetime
33
+ name: Optional[str] = None
27
34
 
28
35
  @field_serializer("date")
29
36
  def serialize_datetime(self, dt: datetime, _info):
37
+ """
38
+ Remove microseconds since it seems like we're inconsistent with getting them
39
+ TODO: figure out why we don't always get microseconds (get_utc_time() does)
40
+ """
30
41
  if dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None:
31
42
  dt = dt.replace(tzinfo=timezone.utc)
32
- # Remove microseconds since it seems like we're inconsistent with getting them
33
- # TODO figure out why we don't always get microseconds (get_utc_time() does)
34
43
  return dt.isoformat(timespec="seconds")
35
44
 
36
45
 
37
- class MessageContent(BaseModel):
38
- type: MessageContentType = Field(..., description="The type of the message.")
39
-
40
-
41
- class TextContent(MessageContent):
42
- type: Literal[MessageContentType.text] = Field(MessageContentType.text, description="The type of the message.")
43
- text: str = Field(..., description="The text content of the message.")
44
-
45
-
46
- MessageContentUnion = Annotated[
47
- Union[TextContent],
48
- Field(discriminator="type"),
49
- ]
50
-
51
-
52
46
  class SystemMessage(LettaMessage):
53
47
  """
54
48
  A message generated by the system. Never streamed back on a response, only used for cursor pagination.
55
49
 
56
- Attributes:
57
- content (Union[str, List[MessageContentUnion]]): The message content sent by the user (can be a string or an array of content parts)
50
+ Args:
58
51
  id (str): The ID of the message
59
52
  date (datetime): The date the message was created in ISO format
53
+ name (Optional[str]): The name of the sender of the message
54
+ content (str): The message content sent by the system
60
55
  """
61
56
 
62
57
  message_type: Literal["system_message"] = "system_message"
63
- content: Union[str, List[MessageContentUnion]]
58
+ content: str = Field(..., description="The message content sent by the system")
64
59
 
65
60
 
66
61
  class UserMessage(LettaMessage):
67
62
  """
68
63
  A message sent by the user. Never streamed back on a response, only used for cursor pagination.
69
64
 
70
- Attributes:
71
- content (Union[str, List[MessageContentUnion]]): The message content sent by the user (can be a string or an array of content parts)
65
+ Args:
72
66
  id (str): The ID of the message
73
67
  date (datetime): The date the message was created in ISO format
68
+ name (Optional[str]): The name of the sender of the message
69
+ content (Union[str, List[LettaUserMessageContentUnion]]): The message content sent by the user (can be a string or an array of multi-modal content parts)
74
70
  """
75
71
 
76
72
  message_type: Literal["user_message"] = "user_message"
77
- content: Union[str, List[MessageContentUnion]]
73
+ content: Union[str, List[LettaUserMessageContentUnion]] = Field(
74
+ ...,
75
+ description="The message content sent by the user (can be a string or an array of multi-modal content parts)",
76
+ json_schema_extra=get_letta_user_message_content_union_str_json_schema(),
77
+ )
78
78
 
79
79
 
80
80
  class ReasoningMessage(LettaMessage):
81
81
  """
82
82
  Representation of an agent's internal reasoning.
83
83
 
84
- Attributes:
84
+ Args:
85
+ id (str): The ID of the message
86
+ date (datetime): The date the message was created in ISO format
87
+ name (Optional[str]): The name of the sender of the message
88
+ source (Literal["reasoner_model", "non_reasoner_model"]): Whether the reasoning
89
+ content was generated natively by a reasoner model or derived via prompting
85
90
  reasoning (str): The internal reasoning of the agent
91
+ """
92
+
93
+ message_type: Literal["reasoning_message"] = "reasoning_message"
94
+ source: Literal["reasoner_model", "non_reasoner_model"] = "non_reasoner_model"
95
+ reasoning: str
96
+
97
+
98
+ class HiddenReasoningMessage(LettaMessage):
99
+ """
100
+ Representation of an agent's internal reasoning where reasoning content
101
+ has been hidden from the response.
102
+
103
+ Args:
86
104
  id (str): The ID of the message
87
105
  date (datetime): The date the message was created in ISO format
106
+ name (Optional[str]): The name of the sender of the message
107
+ state (Literal["redacted", "omitted"]): Whether the reasoning
108
+ content was redacted by the provider or simply omitted by the API
109
+ reasoning (str): The internal reasoning of the agent
88
110
  """
89
111
 
90
112
  message_type: Literal["reasoning_message"] = "reasoning_message"
113
+ state: Literal["redacted", "omitted"]
91
114
  reasoning: str
92
115
 
93
116
 
94
117
  class ToolCall(BaseModel):
95
-
96
118
  name: str
97
119
  arguments: str
98
120
  tool_call_id: str
99
121
 
100
122
 
101
123
  class ToolCallDelta(BaseModel):
102
-
103
124
  name: Optional[str]
104
125
  arguments: Optional[str]
105
126
  tool_call_id: Optional[str]
106
127
 
107
- # NOTE: this is a workaround to exclude None values from the JSON dump,
108
- # since the OpenAI style of returning chunks doesn't include keys with null values
109
128
  def model_dump(self, *args, **kwargs):
129
+ """
130
+ This is a workaround to exclude None values from the JSON dump since the
131
+ OpenAI style of returning chunks doesn't include keys with null values.
132
+ """
110
133
  kwargs["exclude_none"] = True
111
134
  return super().model_dump(*args, **kwargs)
112
135
 
@@ -118,17 +141,20 @@ class ToolCallMessage(LettaMessage):
118
141
  """
119
142
  A message representing a request to call a tool (generated by the LLM to trigger tool execution).
120
143
 
121
- Attributes:
122
- tool_call (Union[ToolCall, ToolCallDelta]): The tool call
144
+ Args:
123
145
  id (str): The ID of the message
124
146
  date (datetime): The date the message was created in ISO format
147
+ name (Optional[str]): The name of the sender of the message
148
+ tool_call (Union[ToolCall, ToolCallDelta]): The tool call
125
149
  """
126
150
 
127
151
  message_type: Literal["tool_call_message"] = "tool_call_message"
128
152
  tool_call: Union[ToolCall, ToolCallDelta]
129
153
 
130
- # NOTE: this is required for the ToolCallDelta exclude_none to work correctly
131
154
  def model_dump(self, *args, **kwargs):
155
+ """
156
+ Handling for the ToolCallDelta exclude_none to work correctly
157
+ """
132
158
  kwargs["exclude_none"] = True
133
159
  data = super().model_dump(*args, **kwargs)
134
160
  if isinstance(data["tool_call"], dict):
@@ -141,12 +167,14 @@ class ToolCallMessage(LettaMessage):
141
167
  ToolCall: lambda v: v.model_dump(exclude_none=True),
142
168
  }
143
169
 
144
- # NOTE: this is required to cast dicts into ToolCallMessage objects
145
- # Without this extra validator, Pydantic will throw an error if 'name' or 'arguments' are None
146
- # (instead of properly casting to ToolCallDelta instead of ToolCall)
147
170
  @field_validator("tool_call", mode="before")
148
171
  @classmethod
149
172
  def validate_tool_call(cls, v):
173
+ """
174
+ Casts dicts into ToolCallMessage objects. Without this extra validator, Pydantic will throw
175
+ an error if 'name' or 'arguments' are None instead of properly casting to ToolCallDelta
176
+ instead of ToolCall.
177
+ """
150
178
  if isinstance(v, dict):
151
179
  if "name" in v and "arguments" in v and "tool_call_id" in v:
152
180
  return ToolCall(name=v["name"], arguments=v["arguments"], tool_call_id=v["tool_call_id"])
@@ -161,11 +189,12 @@ class ToolReturnMessage(LettaMessage):
161
189
  """
162
190
  A message representing the return value of a tool call (generated by Letta executing the requested tool).
163
191
 
164
- Attributes:
165
- tool_return (str): The return value of the tool
166
- status (Literal["success", "error"]): The status of the tool call
192
+ Args:
167
193
  id (str): The ID of the message
168
194
  date (datetime): The date the message was created in ISO format
195
+ name (Optional[str]): The name of the sender of the message
196
+ tool_return (str): The return value of the tool
197
+ status (Literal["success", "error"]): The status of the tool call
169
198
  tool_call_id (str): A unique identifier for the tool call that generated this message
170
199
  stdout (Optional[List(str)]): Captured stdout (e.g. prints, logs) from the tool invocation
171
200
  stderr (Optional[List(str)]): Captured stderr from the tool invocation
@@ -179,12 +208,100 @@ class ToolReturnMessage(LettaMessage):
179
208
  stderr: Optional[List[str]] = None
180
209
 
181
210
 
182
- # Legacy Letta API had an additional type "assistant_message" and the "function_call" was a formatted string
211
+ class AssistantMessage(LettaMessage):
212
+ """
213
+ A message sent by the LLM in response to user input. Used in the LLM context.
183
214
 
215
+ Args:
216
+ id (str): The ID of the message
217
+ date (datetime): The date the message was created in ISO format
218
+ name (Optional[str]): The name of the sender of the message
219
+ content (Union[str, List[LettaAssistantMessageContentUnion]]): The message content sent by the agent (can be a string or an array of content parts)
220
+ """
184
221
 
185
- class AssistantMessage(LettaMessage):
186
222
  message_type: Literal["assistant_message"] = "assistant_message"
187
- content: Union[str, List[MessageContentUnion]]
223
+ content: Union[str, List[LettaAssistantMessageContentUnion]] = Field(
224
+ ...,
225
+ description="The message content sent by the agent (can be a string or an array of content parts)",
226
+ json_schema_extra=get_letta_assistant_message_content_union_str_json_schema(),
227
+ )
228
+
229
+
230
+ # NOTE: use Pydantic's discriminated unions feature: https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions
231
+ LettaMessageUnion = Annotated[
232
+ Union[SystemMessage, UserMessage, ReasoningMessage, ToolCallMessage, ToolReturnMessage, AssistantMessage],
233
+ Field(discriminator="message_type"),
234
+ ]
235
+
236
+
237
+ def create_letta_message_union_schema():
238
+ return {
239
+ "oneOf": [
240
+ {"$ref": "#/components/schemas/SystemMessage"},
241
+ {"$ref": "#/components/schemas/UserMessage"},
242
+ {"$ref": "#/components/schemas/ReasoningMessage"},
243
+ {"$ref": "#/components/schemas/ToolCallMessage"},
244
+ {"$ref": "#/components/schemas/ToolReturnMessage"},
245
+ {"$ref": "#/components/schemas/AssistantMessage"},
246
+ ],
247
+ "discriminator": {
248
+ "propertyName": "message_type",
249
+ "mapping": {
250
+ "system_message": "#/components/schemas/SystemMessage",
251
+ "user_message": "#/components/schemas/UserMessage",
252
+ "reasoning_message": "#/components/schemas/ReasoningMessage",
253
+ "tool_call_message": "#/components/schemas/ToolCallMessage",
254
+ "tool_return_message": "#/components/schemas/ToolReturnMessage",
255
+ "assistant_message": "#/components/schemas/AssistantMessage",
256
+ },
257
+ },
258
+ }
259
+
260
+
261
+ # --------------------------
262
+ # Message Update API Schemas
263
+ # --------------------------
264
+
265
+
266
+ class UpdateSystemMessage(BaseModel):
267
+ message_type: Literal["system_message"] = "system_message"
268
+ content: str = Field(
269
+ ..., description="The message content sent by the system (can be a string or an array of multi-modal content parts)"
270
+ )
271
+
272
+
273
+ class UpdateUserMessage(BaseModel):
274
+ message_type: Literal["user_message"] = "user_message"
275
+ content: Union[str, List[LettaUserMessageContentUnion]] = Field(
276
+ ...,
277
+ description="The message content sent by the user (can be a string or an array of multi-modal content parts)",
278
+ json_schema_extra=get_letta_user_message_content_union_str_json_schema(),
279
+ )
280
+
281
+
282
+ class UpdateReasoningMessage(BaseModel):
283
+ reasoning: str
284
+ message_type: Literal["reasoning_message"] = "reasoning_message"
285
+
286
+
287
+ class UpdateAssistantMessage(BaseModel):
288
+ message_type: Literal["assistant_message"] = "assistant_message"
289
+ content: Union[str, List[LettaAssistantMessageContentUnion]] = Field(
290
+ ...,
291
+ description="The message content sent by the assistant (can be a string or an array of content parts)",
292
+ json_schema_extra=get_letta_assistant_message_content_union_str_json_schema(),
293
+ )
294
+
295
+
296
+ LettaMessageUpdateUnion = Annotated[
297
+ Union[UpdateSystemMessage, UpdateUserMessage, UpdateReasoningMessage, UpdateAssistantMessage],
298
+ Field(discriminator="message_type"),
299
+ ]
300
+
301
+
302
+ # --------------------------
303
+ # Deprecated Message Schemas
304
+ # --------------------------
188
305
 
189
306
 
190
307
  class LegacyFunctionCallMessage(LettaMessage):
@@ -195,7 +312,7 @@ class LegacyFunctionReturn(LettaMessage):
195
312
  """
196
313
  A message representing the return value of a function call (generated by Letta executing the requested function).
197
314
 
198
- Attributes:
315
+ Args:
199
316
  function_return (str): The return value of the function
200
317
  status (Literal["success", "error"]): The status of the function call
201
318
  id (str): The ID of the message
@@ -217,7 +334,7 @@ class LegacyInternalMonologue(LettaMessage):
217
334
  """
218
335
  Representation of an agent's internal monologue.
219
336
 
220
- Attributes:
337
+ Args:
221
338
  internal_monologue (str): The internal monologue of the agent
222
339
  id (str): The ID of the message
223
340
  date (datetime): The date the message was created in ISO format
@@ -228,59 +345,3 @@ class LegacyInternalMonologue(LettaMessage):
228
345
 
229
346
 
230
347
  LegacyLettaMessage = Union[LegacyInternalMonologue, AssistantMessage, LegacyFunctionCallMessage, LegacyFunctionReturn]
231
-
232
-
233
- LettaMessageUnion = Annotated[
234
- Union[SystemMessage, UserMessage, ReasoningMessage, ToolCallMessage, ToolReturnMessage, AssistantMessage],
235
- Field(discriminator="message_type"),
236
- ]
237
-
238
-
239
- class UpdateSystemMessage(BaseModel):
240
- content: Union[str, List[MessageContentUnion]]
241
- message_type: Literal["system_message"] = "system_message"
242
-
243
-
244
- class UpdateUserMessage(BaseModel):
245
- content: Union[str, List[MessageContentUnion]]
246
- message_type: Literal["user_message"] = "user_message"
247
-
248
-
249
- class UpdateReasoningMessage(BaseModel):
250
- reasoning: Union[str, List[MessageContentUnion]]
251
- message_type: Literal["reasoning_message"] = "reasoning_message"
252
-
253
-
254
- class UpdateAssistantMessage(BaseModel):
255
- content: Union[str, List[MessageContentUnion]]
256
- message_type: Literal["assistant_message"] = "assistant_message"
257
-
258
-
259
- LettaMessageUpdateUnion = Annotated[
260
- Union[UpdateSystemMessage, UpdateUserMessage, UpdateReasoningMessage, UpdateAssistantMessage],
261
- Field(discriminator="message_type"),
262
- ]
263
-
264
-
265
- def create_letta_message_union_schema():
266
- return {
267
- "oneOf": [
268
- {"$ref": "#/components/schemas/SystemMessage"},
269
- {"$ref": "#/components/schemas/UserMessage"},
270
- {"$ref": "#/components/schemas/ReasoningMessage"},
271
- {"$ref": "#/components/schemas/ToolCallMessage"},
272
- {"$ref": "#/components/schemas/ToolReturnMessage"},
273
- {"$ref": "#/components/schemas/AssistantMessage"},
274
- ],
275
- "discriminator": {
276
- "propertyName": "message_type",
277
- "mapping": {
278
- "system_message": "#/components/schemas/SystemMessage",
279
- "user_message": "#/components/schemas/UserMessage",
280
- "reasoning_message": "#/components/schemas/ReasoningMessage",
281
- "tool_call_message": "#/components/schemas/ToolCallMessage",
282
- "tool_return_message": "#/components/schemas/ToolReturnMessage",
283
- "assistant_message": "#/components/schemas/AssistantMessage",
284
- },
285
- },
286
- }