letta-nightly 0.6.14.dev20250123104106__py3-none-any.whl → 0.6.15.dev20250124104035__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/client/client.py +144 -68
- letta/client/streaming.py +1 -1
- letta/functions/function_sets/extras.py +8 -3
- letta/functions/function_sets/multi_agent.py +1 -1
- letta/functions/helpers.py +2 -2
- letta/llm_api/llm_api_tools.py +2 -2
- letta/llm_api/openai.py +30 -138
- letta/memory.py +4 -4
- letta/offline_memory_agent.py +10 -10
- letta/orm/agent.py +10 -2
- letta/orm/block.py +14 -3
- letta/orm/job.py +2 -1
- letta/orm/message.py +12 -1
- letta/orm/passage.py +6 -2
- letta/orm/source.py +6 -1
- letta/orm/sqlalchemy_base.py +80 -32
- letta/orm/tool.py +5 -2
- letta/schemas/embedding_config_overrides.py +3 -0
- letta/schemas/enums.py +4 -0
- letta/schemas/job.py +1 -1
- letta/schemas/letta_message.py +22 -5
- letta/schemas/llm_config.py +5 -0
- letta/schemas/llm_config_overrides.py +38 -0
- letta/schemas/message.py +61 -15
- letta/schemas/openai/chat_completions.py +1 -1
- letta/schemas/passage.py +1 -1
- letta/schemas/providers.py +24 -8
- letta/schemas/source.py +1 -1
- letta/server/rest_api/app.py +12 -3
- letta/server/rest_api/interface.py +5 -7
- letta/server/rest_api/routers/v1/agents.py +7 -12
- letta/server/rest_api/routers/v1/blocks.py +19 -0
- letta/server/rest_api/routers/v1/organizations.py +2 -2
- letta/server/rest_api/routers/v1/providers.py +2 -2
- letta/server/rest_api/routers/v1/runs.py +15 -7
- letta/server/rest_api/routers/v1/sandbox_configs.py +4 -4
- letta/server/rest_api/routers/v1/sources.py +2 -2
- letta/server/rest_api/routers/v1/tags.py +2 -2
- letta/server/rest_api/routers/v1/tools.py +2 -2
- letta/server/rest_api/routers/v1/users.py +2 -2
- letta/server/server.py +62 -34
- letta/services/agent_manager.py +80 -33
- letta/services/block_manager.py +15 -2
- letta/services/helpers/agent_manager_helper.py +11 -4
- letta/services/job_manager.py +19 -9
- letta/services/message_manager.py +14 -8
- letta/services/organization_manager.py +8 -4
- letta/services/provider_manager.py +8 -4
- letta/services/sandbox_config_manager.py +16 -8
- letta/services/source_manager.py +4 -4
- letta/services/tool_manager.py +3 -3
- letta/services/user_manager.py +9 -5
- {letta_nightly-0.6.14.dev20250123104106.dist-info → letta_nightly-0.6.15.dev20250124104035.dist-info}/METADATA +2 -1
- {letta_nightly-0.6.14.dev20250123104106.dist-info → letta_nightly-0.6.15.dev20250124104035.dist-info}/RECORD +58 -57
- letta/orm/job_usage_statistics.py +0 -30
- {letta_nightly-0.6.14.dev20250123104106.dist-info → letta_nightly-0.6.15.dev20250124104035.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.14.dev20250123104106.dist-info → letta_nightly-0.6.15.dev20250124104035.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.14.dev20250123104106.dist-info → letta_nightly-0.6.15.dev20250124104035.dist-info}/entry_points.txt +0 -0
letta/orm/sqlalchemy_base.py
CHANGED
|
@@ -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_,
|
|
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
|
-
|
|
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
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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)
|
|
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
|
-
#
|
|
154
|
-
if
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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(),
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
454
|
-
|
|
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__ = (
|
|
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(
|
letta/schemas/enums.py
CHANGED
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
|
|
letta/schemas/letta_message.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
+
content: Union[str, List[MessageContentUnion]]
|
|
171
188
|
|
|
172
189
|
|
|
173
190
|
class LegacyFunctionCallMessage(LettaMessage):
|
letta/schemas/llm_config.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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):
|