letta-nightly 0.6.14.dev20250123041709__py3-none-any.whl → 0.6.15.dev20250124054224__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 (59) hide show
  1. letta/__init__.py +1 -1
  2. letta/client/client.py +144 -68
  3. letta/client/streaming.py +1 -1
  4. letta/functions/function_sets/extras.py +8 -3
  5. letta/functions/function_sets/multi_agent.py +1 -1
  6. letta/functions/helpers.py +2 -2
  7. letta/llm_api/llm_api_tools.py +2 -2
  8. letta/llm_api/openai.py +30 -138
  9. letta/memory.py +4 -4
  10. letta/offline_memory_agent.py +10 -10
  11. letta/orm/agent.py +10 -2
  12. letta/orm/block.py +14 -3
  13. letta/orm/job.py +2 -1
  14. letta/orm/message.py +12 -1
  15. letta/orm/passage.py +6 -2
  16. letta/orm/source.py +6 -1
  17. letta/orm/sqlalchemy_base.py +80 -32
  18. letta/orm/tool.py +5 -2
  19. letta/schemas/embedding_config_overrides.py +3 -0
  20. letta/schemas/enums.py +4 -0
  21. letta/schemas/job.py +1 -1
  22. letta/schemas/letta_message.py +22 -5
  23. letta/schemas/llm_config.py +5 -0
  24. letta/schemas/llm_config_overrides.py +38 -0
  25. letta/schemas/message.py +61 -15
  26. letta/schemas/openai/chat_completions.py +1 -1
  27. letta/schemas/passage.py +1 -1
  28. letta/schemas/providers.py +24 -8
  29. letta/schemas/source.py +1 -1
  30. letta/server/rest_api/app.py +12 -3
  31. letta/server/rest_api/interface.py +5 -7
  32. letta/server/rest_api/routers/v1/agents.py +7 -12
  33. letta/server/rest_api/routers/v1/blocks.py +19 -0
  34. letta/server/rest_api/routers/v1/organizations.py +2 -2
  35. letta/server/rest_api/routers/v1/providers.py +2 -2
  36. letta/server/rest_api/routers/v1/runs.py +15 -7
  37. letta/server/rest_api/routers/v1/sandbox_configs.py +4 -4
  38. letta/server/rest_api/routers/v1/sources.py +2 -2
  39. letta/server/rest_api/routers/v1/tags.py +2 -2
  40. letta/server/rest_api/routers/v1/tools.py +2 -2
  41. letta/server/rest_api/routers/v1/users.py +2 -2
  42. letta/server/server.py +62 -34
  43. letta/services/agent_manager.py +80 -33
  44. letta/services/block_manager.py +15 -2
  45. letta/services/helpers/agent_manager_helper.py +11 -4
  46. letta/services/job_manager.py +19 -9
  47. letta/services/message_manager.py +14 -8
  48. letta/services/organization_manager.py +8 -4
  49. letta/services/provider_manager.py +8 -4
  50. letta/services/sandbox_config_manager.py +16 -8
  51. letta/services/source_manager.py +4 -4
  52. letta/services/tool_manager.py +3 -3
  53. letta/services/user_manager.py +9 -5
  54. {letta_nightly-0.6.14.dev20250123041709.dist-info → letta_nightly-0.6.15.dev20250124054224.dist-info}/METADATA +2 -1
  55. {letta_nightly-0.6.14.dev20250123041709.dist-info → letta_nightly-0.6.15.dev20250124054224.dist-info}/RECORD +58 -57
  56. letta/orm/job_usage_statistics.py +0 -30
  57. {letta_nightly-0.6.14.dev20250123041709.dist-info → letta_nightly-0.6.15.dev20250124054224.dist-info}/LICENSE +0 -0
  58. {letta_nightly-0.6.14.dev20250123041709.dist-info → letta_nightly-0.6.15.dev20250124054224.dist-info}/WHEEL +0 -0
  59. {letta_nightly-0.6.14.dev20250123041709.dist-info → letta_nightly-0.6.15.dev20250124054224.dist-info}/entry_points.txt +0 -0
@@ -3,7 +3,7 @@ from enum import Enum
3
3
  from functools import wraps
4
4
  from typing import TYPE_CHECKING, List, Literal, Optional, Tuple, Union
5
5
 
6
- from sqlalchemy import String, and_, desc, func, or_, select
6
+ from sqlalchemy import String, and_, func, or_, select
7
7
  from sqlalchemy.exc import DBAPIError, IntegrityError, TimeoutError
8
8
  from sqlalchemy.orm import Mapped, Session, mapped_column
9
9
 
@@ -52,7 +52,8 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
52
52
  cls,
53
53
  *,
54
54
  db_session: "Session",
55
- cursor: Optional[str] = None,
55
+ before: Optional[str] = None,
56
+ after: Optional[str] = None,
56
57
  start_date: Optional[datetime] = None,
57
58
  end_date: Optional[datetime] = None,
58
59
  limit: Optional[int] = 50,
@@ -69,12 +70,13 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
69
70
  **kwargs,
70
71
  ) -> List["SqlalchemyBase"]:
71
72
  """
72
- List records with cursor-based pagination, ordering by created_at.
73
- Cursor is an ID, but pagination is based on the cursor object's created_at value.
73
+ List records with before/after pagination, ordering by created_at.
74
+ Can use both before and after to fetch a window of records.
74
75
 
75
76
  Args:
76
77
  db_session: SQLAlchemy session
77
- cursor: ID of the last item seen (for pagination)
78
+ before: ID of item to paginate before (upper bound)
79
+ after: ID of item to paginate after (lower bound)
78
80
  start_date: Filter items after this date
79
81
  end_date: Filter items before this date
80
82
  limit: Maximum number of items to return
@@ -89,13 +91,25 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
89
91
  raise ValueError("start_date must be earlier than or equal to end_date")
90
92
 
91
93
  logger.debug(f"Listing {cls.__name__} with kwarg filters {kwargs}")
94
+
92
95
  with db_session as session:
93
- # If cursor provided, get the reference object
94
- cursor_obj = None
95
- if cursor:
96
- cursor_obj = session.get(cls, cursor)
97
- if not cursor_obj:
98
- raise NoResultFound(f"No {cls.__name__} found with id {cursor}")
96
+ # Get the reference objects for pagination
97
+ before_obj = None
98
+ after_obj = None
99
+
100
+ if before:
101
+ before_obj = session.get(cls, before)
102
+ if not before_obj:
103
+ raise NoResultFound(f"No {cls.__name__} found with id {before}")
104
+
105
+ if after:
106
+ after_obj = session.get(cls, after)
107
+ if not after_obj:
108
+ raise NoResultFound(f"No {cls.__name__} found with id {after}")
109
+
110
+ # Validate that before comes after the after object if both are provided
111
+ if before_obj and after_obj and before_obj.created_at < after_obj.created_at:
112
+ raise ValueError("'before' reference must be later than 'after' reference")
99
113
 
100
114
  query = select(cls)
101
115
 
@@ -122,8 +136,8 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
122
136
  else:
123
137
  # Match ANY tag - use join and filter
124
138
  query = (
125
- query.join(cls.tags).filter(cls.tags.property.mapper.class_.tag.in_(tags)).group_by(cls.id) # Deduplicate results
126
- )
139
+ query.join(cls.tags).filter(cls.tags.property.mapper.class_.tag.in_(tags)).group_by(cls.id)
140
+ ) # Deduplicate results
127
141
 
128
142
  # Group by primary key and all necessary columns to avoid JSON comparison
129
143
  query = query.group_by(cls.id)
@@ -150,16 +164,35 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
150
164
  if end_date:
151
165
  query = query.filter(cls.created_at < end_date)
152
166
 
153
- # Cursor-based pagination
154
- if cursor_obj:
155
- if ascending:
156
- query = query.where(cls.created_at >= cursor_obj.created_at).where(
157
- or_(cls.created_at > cursor_obj.created_at, cls.id > cursor_obj.id)
158
- )
167
+ # Handle pagination based on before/after
168
+ if before or after:
169
+ conditions = []
170
+
171
+ if before and after:
172
+ # Window-based query - get records between before and after
173
+ conditions = [
174
+ or_(cls.created_at < before_obj.created_at, and_(cls.created_at == before_obj.created_at, cls.id < before_obj.id)),
175
+ or_(cls.created_at > after_obj.created_at, and_(cls.created_at == after_obj.created_at, cls.id > after_obj.id)),
176
+ ]
159
177
  else:
160
- query = query.where(cls.created_at <= cursor_obj.created_at).where(
161
- or_(cls.created_at < cursor_obj.created_at, cls.id < cursor_obj.id)
162
- )
178
+ # Pure pagination query
179
+ if before:
180
+ conditions.append(
181
+ or_(
182
+ cls.created_at < before_obj.created_at,
183
+ and_(cls.created_at == before_obj.created_at, cls.id < before_obj.id),
184
+ )
185
+ )
186
+ if after:
187
+ conditions.append(
188
+ or_(
189
+ cls.created_at > after_obj.created_at,
190
+ and_(cls.created_at == after_obj.created_at, cls.id > after_obj.id),
191
+ )
192
+ )
193
+
194
+ if conditions:
195
+ query = query.where(and_(*conditions))
163
196
 
164
197
  # Text search
165
198
  if query_text:
@@ -184,7 +217,9 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
184
217
  # SQLite with custom vector type
185
218
  query_embedding_binary = adapt_array(query_embedding)
186
219
  query = query.order_by(
187
- func.cosine_distance(cls.embedding, query_embedding_binary).asc(), cls.created_at.asc(), cls.id.asc()
220
+ func.cosine_distance(cls.embedding, query_embedding_binary).asc(),
221
+ cls.created_at.asc() if ascending else cls.created_at.desc(),
222
+ cls.id.asc(),
188
223
  )
189
224
  is_ordered = True
190
225
 
@@ -195,13 +230,28 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
195
230
  # Apply ordering
196
231
  if not is_ordered:
197
232
  if ascending:
198
- query = query.order_by(cls.created_at, cls.id)
233
+ query = query.order_by(cls.created_at.asc(), cls.id.asc())
199
234
  else:
200
- query = query.order_by(desc(cls.created_at), desc(cls.id))
235
+ query = query.order_by(cls.created_at.desc(), cls.id.desc())
236
+
237
+ # Apply limit, adjusting for both bounds if necessary
238
+ if before and after:
239
+ # When both bounds are provided, we need to fetch enough records to satisfy
240
+ # the limit while respecting both bounds. We'll fetch more and then trim.
241
+ query = query.limit(limit * 2)
242
+ else:
243
+ query = query.limit(limit)
244
+
245
+ results = list(session.execute(query).scalars())
201
246
 
202
- query = query.limit(limit)
247
+ # If we have both bounds, take the middle portion
248
+ if before and after and len(results) > limit:
249
+ middle = len(results) // 2
250
+ start = max(0, middle - limit // 2)
251
+ end = min(len(results), start + limit)
252
+ results = results[start:end]
203
253
 
204
- return list(session.execute(query).scalars())
254
+ return results
205
255
 
206
256
  @classmethod
207
257
  @handle_db_timeout
@@ -449,12 +499,10 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
449
499
 
450
500
  def to_pydantic(self) -> "BaseModel":
451
501
  """converts to the basic pydantic model counterpart"""
502
+ model = self.__pydantic_model__.model_validate(self)
452
503
  if hasattr(self, "metadata_"):
453
- model_dict = {k: v for k, v in self.__dict__.items() if k in self.__pydantic_model__.model_fields}
454
- model_dict["metadata"] = self.metadata_
455
- return self.__pydantic_model__.model_validate(model_dict)
456
-
457
- return self.__pydantic_model__.model_validate(self)
504
+ model.metadata = self.metadata_
505
+ return model
458
506
 
459
507
  def to_record(self) -> "BaseModel":
460
508
  """Deprecated accessor for to_pydantic"""
letta/orm/tool.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from typing import TYPE_CHECKING, List, Optional
2
2
 
3
- from sqlalchemy import JSON, String, UniqueConstraint
3
+ from sqlalchemy import JSON, Index, String, UniqueConstraint
4
4
  from sqlalchemy.orm import Mapped, mapped_column, relationship
5
5
 
6
6
  # TODO everything in functions should live in this model
@@ -26,7 +26,10 @@ class Tool(SqlalchemyBase, OrganizationMixin):
26
26
 
27
27
  # Add unique constraint on (name, _organization_id)
28
28
  # An organization should not have multiple tools with the same name
29
- __table_args__ = (UniqueConstraint("name", "organization_id", name="uix_name_organization"),)
29
+ __table_args__ = (
30
+ UniqueConstraint("name", "organization_id", name="uix_name_organization"),
31
+ Index("ix_tools_created_at_name", "created_at", "name"),
32
+ )
30
33
 
31
34
  name: Mapped[str] = mapped_column(doc="The display name of the tool.")
32
35
  tool_type: Mapped[ToolType] = mapped_column(
@@ -0,0 +1,3 @@
1
+ from typing import Dict
2
+
3
+ EMBEDDING_HANDLE_OVERRIDES: Dict[str, Dict[str, str]] = {}
letta/schemas/enums.py CHANGED
@@ -9,6 +9,10 @@ class MessageRole(str, Enum):
9
9
  system = "system"
10
10
 
11
11
 
12
+ class MessageContentType(str, Enum):
13
+ text = "text"
14
+
15
+
12
16
  class OptionState(str, Enum):
13
17
  """Useful for kwargs that are bool + default option"""
14
18
 
letta/schemas/job.py CHANGED
@@ -12,7 +12,7 @@ class JobBase(OrmMetadataBase):
12
12
  __id_prefix__ = "job"
13
13
  status: JobStatus = Field(default=JobStatus.created, description="The status of the job.")
14
14
  completed_at: Optional[datetime] = Field(None, description="The unix timestamp of when the job was completed.")
15
- metadata: Optional[dict] = Field(None, description="The metadata of the job.")
15
+ metadata: Optional[dict] = Field(None, validation_alias="metadata_", description="The metadata of the job.")
16
16
  job_type: JobType = Field(default=JobType.JOB, description="The type of the job.")
17
17
 
18
18
 
@@ -4,6 +4,8 @@ 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
8
+
7
9
  # Letta API style responses (intended to be easier to use vs getting true Message types)
8
10
 
9
11
 
@@ -32,18 +34,33 @@ class LettaMessage(BaseModel):
32
34
  return dt.isoformat(timespec="seconds")
33
35
 
34
36
 
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
+
35
52
  class SystemMessage(LettaMessage):
36
53
  """
37
54
  A message generated by the system. Never streamed back on a response, only used for cursor pagination.
38
55
 
39
56
  Attributes:
40
- message (str): The message sent by the system
57
+ content (Union[str, List[MessageContentUnion]]): The message content sent by the user (can be a string or an array of content parts)
41
58
  id (str): The ID of the message
42
59
  date (datetime): The date the message was created in ISO format
43
60
  """
44
61
 
45
62
  message_type: Literal["system_message"] = "system_message"
46
- message: str
63
+ content: Union[str, List[MessageContentUnion]]
47
64
 
48
65
 
49
66
  class UserMessage(LettaMessage):
@@ -51,13 +68,13 @@ class UserMessage(LettaMessage):
51
68
  A message sent by the user. Never streamed back on a response, only used for cursor pagination.
52
69
 
53
70
  Attributes:
54
- message (str): The message sent by the user
71
+ content (Union[str, List[MessageContentUnion]]): The message content sent by the user (can be a string or an array of content parts)
55
72
  id (str): The ID of the message
56
73
  date (datetime): The date the message was created in ISO format
57
74
  """
58
75
 
59
76
  message_type: Literal["user_message"] = "user_message"
60
- message: str
77
+ content: Union[str, List[MessageContentUnion]]
61
78
 
62
79
 
63
80
  class ReasoningMessage(LettaMessage):
@@ -167,7 +184,7 @@ class ToolReturnMessage(LettaMessage):
167
184
 
168
185
  class AssistantMessage(LettaMessage):
169
186
  message_type: Literal["assistant_message"] = "assistant_message"
170
- assistant_message: str
187
+ content: Union[str, List[MessageContentUnion]]
171
188
 
172
189
 
173
190
  class LegacyFunctionCallMessage(LettaMessage):
@@ -14,6 +14,7 @@ class LLMConfig(BaseModel):
14
14
  model_wrapper (str): The wrapper for the model. This is used to wrap additional text around the input/output of the model. This is useful for text-to-text completions, such as the Completions API in OpenAI.
15
15
  context_window (int): The context window size for the model.
16
16
  put_inner_thoughts_in_kwargs (bool): Puts `inner_thoughts` as a kwarg in the function call if this is set to True. This helps with function calling performance and also the generation of inner thoughts.
17
+ temperature (float): The temperature to use when generating text with the model. A higher temperature will result in more random text.
17
18
  """
18
19
 
19
20
  # TODO: 🤮 don't default to a vendor! bug city!
@@ -46,6 +47,10 @@ class LLMConfig(BaseModel):
46
47
  description="Puts 'inner_thoughts' as a kwarg in the function call if this is set to True. This helps with function calling performance and also the generation of inner thoughts.",
47
48
  )
48
49
  handle: Optional[str] = Field(None, description="The handle for this config, in the format provider/model-name.")
50
+ temperature: float = Field(
51
+ 0.7,
52
+ description="The temperature to use when generating text with the model. A higher temperature will result in more random text.",
53
+ )
49
54
 
50
55
  # FIXME hack to silence pydantic protected namespace warning
51
56
  model_config = ConfigDict(protected_namespaces=())
@@ -0,0 +1,38 @@
1
+ from typing import Dict
2
+
3
+ LLM_HANDLE_OVERRIDES: Dict[str, Dict[str, str]] = {
4
+ "anthropic": {
5
+ "claude-3-5-haiku-20241022": "claude-3.5-haiku",
6
+ "claude-3-5-sonnet-20241022": "claude-3.5-sonnet",
7
+ "claude-3-opus-20240229": "claude-3-opus",
8
+ },
9
+ "openai": {
10
+ "chatgpt-4o-latest": "chatgpt-4o",
11
+ "gpt-3.5-turbo": "gpt-3.5-turbo",
12
+ "gpt-3.5-turbo-0125": "gpt-3.5-turbo-jan",
13
+ "gpt-3.5-turbo-1106": "gpt-3.5-turbo-nov",
14
+ "gpt-3.5-turbo-16k": "gpt-3.5-turbo-16k",
15
+ "gpt-3.5-turbo-instruct": "gpt-3.5-turbo-instruct",
16
+ "gpt-4-0125-preview": "gpt-4-preview-jan",
17
+ "gpt-4-0613": "gpt-4-june",
18
+ "gpt-4-1106-preview": "gpt-4-preview-nov",
19
+ "gpt-4-turbo-2024-04-09": "gpt-4-turbo-apr",
20
+ "gpt-4o-2024-05-13": "gpt-4o-may",
21
+ "gpt-4o-2024-08-06": "gpt-4o-aug",
22
+ "gpt-4o-mini-2024-07-18": "gpt-4o-mini-jul",
23
+ },
24
+ "together": {
25
+ "Qwen/Qwen2.5-72B-Instruct-Turbo": "qwen-2.5-72b-instruct",
26
+ "meta-llama/Llama-3-70b-chat-hf": "llama-3-70b",
27
+ "meta-llama/Meta-Llama-3-70B-Instruct-Turbo": "llama-3-70b-instruct",
28
+ "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo": "llama-3.1-405b-instruct",
29
+ "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo": "llama-3.1-70b-instruct",
30
+ "meta-llama/Llama-3.3-70B-Instruct-Turbo": "llama-3.3-70b-instruct",
31
+ "mistralai/Mistral-7B-Instruct-v0.2": "mistral-7b-instruct-v2",
32
+ "mistralai/Mistral-7B-Instruct-v0.3": "mistral-7b-instruct-v3",
33
+ "mistralai/Mixtral-8x22B-Instruct-v0.1": "mixtral-8x22b-instruct",
34
+ "mistralai/Mixtral-8x7B-Instruct-v0.1": "mixtral-8x7b-instruct",
35
+ "mistralai/Mixtral-8x7B-v0.1": "mixtral-8x7b",
36
+ "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO": "hermes-2-mixtral",
37
+ },
38
+ }
letta/schemas/message.py CHANGED
@@ -2,21 +2,23 @@ import copy
2
2
  import json
3
3
  import warnings
4
4
  from datetime import datetime, timezone
5
- from typing import List, Literal, Optional
5
+ from typing import Any, Dict, List, Literal, Optional, Union
6
6
 
7
7
  from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall
8
8
  from openai.types.chat.chat_completion_message_tool_call import Function as OpenAIFunction
9
- from pydantic import BaseModel, Field, field_validator
9
+ from pydantic import BaseModel, Field, field_validator, model_validator
10
10
 
11
11
  from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, TOOL_CALL_ID_MAX_LEN
12
12
  from letta.local_llm.constants import INNER_THOUGHTS_KWARG
13
- from letta.schemas.enums import MessageRole
13
+ from letta.schemas.enums import MessageContentType, MessageRole
14
14
  from letta.schemas.letta_base import OrmMetadataBase
15
15
  from letta.schemas.letta_message import (
16
16
  AssistantMessage,
17
17
  LettaMessage,
18
+ MessageContentUnion,
18
19
  ReasoningMessage,
19
20
  SystemMessage,
21
+ TextContent,
20
22
  ToolCall,
21
23
  ToolCallMessage,
22
24
  ToolReturnMessage,
@@ -59,7 +61,7 @@ class MessageCreate(BaseModel):
59
61
  MessageRole.user,
60
62
  MessageRole.system,
61
63
  ] = Field(..., description="The role of the participant.")
62
- text: str = Field(..., description="The text of the message.")
64
+ content: Union[str, List[MessageContentUnion]] = Field(..., description="The content of the message.")
63
65
  name: Optional[str] = Field(None, description="The name of the participant.")
64
66
 
65
67
 
@@ -67,7 +69,7 @@ class MessageUpdate(BaseModel):
67
69
  """Request to update a message"""
68
70
 
69
71
  role: Optional[MessageRole] = Field(None, description="The role of the participant.")
70
- text: Optional[str] = Field(None, description="The text of the message.")
72
+ content: Optional[Union[str, List[MessageContentUnion]]] = Field(..., description="The content of the message.")
71
73
  # NOTE: probably doesn't make sense to allow remapping user_id or agent_id (vs creating a new message)
72
74
  # user_id: Optional[str] = Field(None, description="The unique identifier of the user.")
73
75
  # agent_id: Optional[str] = Field(None, description="The unique identifier of the agent.")
@@ -79,6 +81,18 @@ class MessageUpdate(BaseModel):
79
81
  tool_calls: Optional[List[OpenAIToolCall,]] = Field(None, description="The list of tool calls requested.")
80
82
  tool_call_id: Optional[str] = Field(None, description="The id of the tool call.")
81
83
 
84
+ def model_dump(self, to_orm: bool = False, **kwargs) -> Dict[str, Any]:
85
+ data = super().model_dump(**kwargs)
86
+ if to_orm and "content" in data:
87
+ if isinstance(data["content"], str):
88
+ data["text"] = data["content"]
89
+ else:
90
+ for content in data["content"]:
91
+ if content["type"] == "text":
92
+ data["text"] = content["text"]
93
+ del data["content"]
94
+ return data
95
+
82
96
 
83
97
  class Message(BaseMessage):
84
98
  """
@@ -100,7 +114,7 @@ class Message(BaseMessage):
100
114
 
101
115
  id: str = BaseMessage.generate_id_field()
102
116
  role: MessageRole = Field(..., description="The role of the participant.")
103
- text: Optional[str] = Field(None, description="The text of the message.")
117
+ content: Optional[List[MessageContentUnion]] = Field(None, description="The content of the message.")
104
118
  organization_id: Optional[str] = Field(None, description="The unique identifier of the organization.")
105
119
  agent_id: Optional[str] = Field(None, description="The unique identifier of the agent.")
106
120
  model: Optional[str] = Field(None, description="The model used to make the function call.")
@@ -108,6 +122,7 @@ class Message(BaseMessage):
108
122
  tool_calls: Optional[List[OpenAIToolCall,]] = Field(None, description="The list of tool calls requested.")
109
123
  tool_call_id: Optional[str] = Field(None, description="The id of the tool call.")
110
124
  step_id: Optional[str] = Field(None, description="The id of the step that this message was created in.")
125
+
111
126
  # This overrides the optional base orm schema, created_at MUST exist on all messages objects
112
127
  created_at: datetime = Field(default_factory=get_utc_time, description="The timestamp when the object was created.")
113
128
 
@@ -118,6 +133,37 @@ class Message(BaseMessage):
118
133
  assert v in roles, f"Role must be one of {roles}"
119
134
  return v
120
135
 
136
+ @model_validator(mode="before")
137
+ @classmethod
138
+ def convert_from_orm(cls, data: Dict[str, Any]) -> Dict[str, Any]:
139
+ if isinstance(data, dict):
140
+ if "text" in data and "content" not in data:
141
+ data["content"] = [TextContent(text=data["text"])]
142
+ del data["text"]
143
+ return data
144
+
145
+ def model_dump(self, to_orm: bool = False, **kwargs) -> Dict[str, Any]:
146
+ data = super().model_dump(**kwargs)
147
+ if to_orm:
148
+ for content in data["content"]:
149
+ if content["type"] == "text":
150
+ data["text"] = content["text"]
151
+ del data["content"]
152
+ return data
153
+
154
+ @property
155
+ def text(self) -> Optional[str]:
156
+ """
157
+ Retrieve the first text content's text.
158
+
159
+ Returns:
160
+ str: The text content, or None if no text content exists
161
+ """
162
+ if not self.content:
163
+ return None
164
+ text_content = [content.text for content in self.content if content.type == MessageContentType.text]
165
+ return text_content[0] if text_content else None
166
+
121
167
  def to_json(self):
122
168
  json_message = vars(self)
123
169
  if json_message["tool_calls"] is not None:
@@ -165,7 +211,7 @@ class Message(BaseMessage):
165
211
  AssistantMessage(
166
212
  id=self.id,
167
213
  date=self.created_at,
168
- assistant_message=message_string,
214
+ content=message_string,
169
215
  )
170
216
  )
171
217
  else:
@@ -221,7 +267,7 @@ class Message(BaseMessage):
221
267
  UserMessage(
222
268
  id=self.id,
223
269
  date=self.created_at,
224
- message=self.text,
270
+ content=self.text,
225
271
  )
226
272
  )
227
273
  elif self.role == MessageRole.system:
@@ -231,7 +277,7 @@ class Message(BaseMessage):
231
277
  SystemMessage(
232
278
  id=self.id,
233
279
  date=self.created_at,
234
- message=self.text,
280
+ content=self.text,
235
281
  )
236
282
  )
237
283
  else:
@@ -283,7 +329,7 @@ class Message(BaseMessage):
283
329
  model=model,
284
330
  # standard fields expected in an OpenAI ChatCompletion message object
285
331
  role=MessageRole.tool, # NOTE
286
- text=openai_message_dict["content"],
332
+ content=[TextContent(text=openai_message_dict["content"])],
287
333
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
288
334
  tool_calls=openai_message_dict["tool_calls"] if "tool_calls" in openai_message_dict else None,
289
335
  tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
@@ -296,7 +342,7 @@ class Message(BaseMessage):
296
342
  model=model,
297
343
  # standard fields expected in an OpenAI ChatCompletion message object
298
344
  role=MessageRole.tool, # NOTE
299
- text=openai_message_dict["content"],
345
+ content=[TextContent(text=openai_message_dict["content"])],
300
346
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
301
347
  tool_calls=openai_message_dict["tool_calls"] if "tool_calls" in openai_message_dict else None,
302
348
  tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
@@ -328,7 +374,7 @@ class Message(BaseMessage):
328
374
  model=model,
329
375
  # standard fields expected in an OpenAI ChatCompletion message object
330
376
  role=MessageRole(openai_message_dict["role"]),
331
- text=openai_message_dict["content"],
377
+ content=[TextContent(text=openai_message_dict["content"])],
332
378
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
333
379
  tool_calls=tool_calls,
334
380
  tool_call_id=None, # NOTE: None, since this field is only non-null for role=='tool'
@@ -341,7 +387,7 @@ class Message(BaseMessage):
341
387
  model=model,
342
388
  # standard fields expected in an OpenAI ChatCompletion message object
343
389
  role=MessageRole(openai_message_dict["role"]),
344
- text=openai_message_dict["content"],
390
+ content=[TextContent(text=openai_message_dict["content"])],
345
391
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
346
392
  tool_calls=tool_calls,
347
393
  tool_call_id=None, # NOTE: None, since this field is only non-null for role=='tool'
@@ -373,7 +419,7 @@ class Message(BaseMessage):
373
419
  model=model,
374
420
  # standard fields expected in an OpenAI ChatCompletion message object
375
421
  role=MessageRole(openai_message_dict["role"]),
376
- text=openai_message_dict["content"],
422
+ content=[TextContent(text=openai_message_dict["content"])],
377
423
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
378
424
  tool_calls=tool_calls,
379
425
  tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
@@ -386,7 +432,7 @@ class Message(BaseMessage):
386
432
  model=model,
387
433
  # standard fields expected in an OpenAI ChatCompletion message object
388
434
  role=MessageRole(openai_message_dict["role"]),
389
- text=openai_message_dict["content"],
435
+ content=[TextContent(text=openai_message_dict["content"] or "")],
390
436
  name=openai_message_dict["name"] if "name" in openai_message_dict else None,
391
437
  tool_calls=tool_calls,
392
438
  tool_call_id=openai_message_dict["tool_call_id"] if "tool_call_id" in openai_message_dict else None,
@@ -104,7 +104,7 @@ class ChatCompletionRequest(BaseModel):
104
104
  logit_bias: Optional[Dict[str, int]] = None
105
105
  logprobs: Optional[bool] = False
106
106
  top_logprobs: Optional[int] = None
107
- max_tokens: Optional[int] = None
107
+ max_completion_tokens: Optional[int] = None
108
108
  n: Optional[int] = 1
109
109
  presence_penalty: Optional[float] = 0
110
110
  response_format: Optional[ResponseFormat] = None
letta/schemas/passage.py CHANGED
@@ -23,7 +23,7 @@ class PassageBase(OrmMetadataBase):
23
23
 
24
24
  # file association
25
25
  file_id: Optional[str] = Field(None, description="The unique identifier of the file associated with the passage.")
26
- metadata: Optional[Dict] = Field({}, description="The metadata of the passage.")
26
+ metadata: Optional[Dict] = Field({}, validation_alias="metadata_", description="The metadata of the passage.")
27
27
 
28
28
 
29
29
  class Passage(PassageBase):