Skip to content

Commit 646b1a4

Browse files
authored
chore: bump v0.11.2 (#2757)
2 parents e141a40 + 9b53e3e commit 646b1a4

File tree

8 files changed

+162
-23
lines changed

8 files changed

+162
-23
lines changed

letta/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
__version__ = version("letta")
66
except PackageNotFoundError:
77
# Fallback for development installations
8-
__version__ = "0.11.0"
8+
__version__ = "0.11.2"
99

1010
if os.environ.get("LETTA_VERSION"):
1111
__version__ = os.environ["LETTA_VERSION"]

letta/schemas/llm_config.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,12 @@ def pretty_print(self) -> str:
187187

188188
@classmethod
189189
def apply_reasoning_setting_to_config(cls, config: "LLMConfig", reasoning: bool):
190-
if reasoning:
191-
config.enable_reasoner = True
190+
if not reasoning:
191+
config.put_inner_thoughts_in_kwargs = False
192+
config.enable_reasoner = False
192193

194+
else:
195+
config.enable_reasoner = True
193196
if (
194197
config.model_endpoint_type == "anthropic"
195198
and ("claude-opus-4" in config.model or "claude-sonnet-4" in config.model or "claude-3-7-sonnet" in config.model)
@@ -207,9 +210,6 @@ def apply_reasoning_setting_to_config(cls, config: "LLMConfig", reasoning: bool)
207210
config.reasoning_effort = "medium"
208211
else:
209212
config.put_inner_thoughts_in_kwargs = True
210-
211-
else:
212-
config.enable_reasoner = False
213-
config.put_inner_thoughts_in_kwargs = False
213+
config.enable_reasoner = False
214214

215215
return config

letta/serialize_schemas/marshmallow_agent.py

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from typing import Dict
1+
from typing import Dict, Optional
22

33
from marshmallow import fields, post_dump, pre_load
4+
from sqlalchemy import func
45
from sqlalchemy.orm import sessionmaker
56

67
import letta
@@ -15,6 +16,7 @@
1516
from letta.serialize_schemas.marshmallow_message import SerializedMessageSchema
1617
from letta.serialize_schemas.marshmallow_tag import SerializedAgentTagSchema
1718
from letta.serialize_schemas.marshmallow_tool import SerializedToolSchema
19+
from letta.settings import DatabaseChoice, settings
1820

1921

2022
class MarshmallowAgentSchema(BaseSchema):
@@ -41,9 +43,10 @@ class MarshmallowAgentSchema(BaseSchema):
4143
tool_exec_environment_variables = fields.List(fields.Nested(SerializedAgentEnvironmentVariableSchema))
4244
tags = fields.List(fields.Nested(SerializedAgentTagSchema))
4345

44-
def __init__(self, *args, session: sessionmaker, actor: User, **kwargs):
46+
def __init__(self, *args, session: sessionmaker, actor: User, max_steps: Optional[int] = None, **kwargs):
4547
super().__init__(*args, actor=actor, **kwargs)
4648
self.session = session
49+
self.max_steps = max_steps
4750

4851
# Propagate session and actor to nested schemas automatically
4952
for field in self.fields.values():
@@ -64,16 +67,103 @@ def attach_messages(self, data: Dict, **kwargs):
6467

6568
with db_registry.session() as session:
6669
agent_id = data.get("id")
67-
msgs = (
68-
session.query(MessageModel)
69-
.filter(
70-
MessageModel.agent_id == agent_id,
71-
MessageModel.organization_id == self.actor.organization_id,
70+
71+
if self.max_steps is not None:
72+
# first, always get the system message
73+
system_msg = (
74+
session.query(MessageModel)
75+
.filter(
76+
MessageModel.agent_id == agent_id,
77+
MessageModel.organization_id == self.actor.organization_id,
78+
MessageModel.role == "system",
79+
)
80+
.order_by(MessageModel.sequence_id.asc())
81+
.first()
82+
)
83+
84+
if settings.database_engine is DatabaseChoice.POSTGRES:
85+
# efficient PostgreSQL approach using subquery
86+
user_msg_subquery = (
87+
session.query(MessageModel.sequence_id)
88+
.filter(
89+
MessageModel.agent_id == agent_id,
90+
MessageModel.organization_id == self.actor.organization_id,
91+
MessageModel.role == "user",
92+
)
93+
.order_by(MessageModel.sequence_id.desc())
94+
.limit(self.max_steps)
95+
.subquery()
96+
)
97+
98+
# get the minimum sequence_id from the subquery
99+
cutoff_sequence_id = session.query(func.min(user_msg_subquery.c.sequence_id)).scalar()
100+
101+
if cutoff_sequence_id:
102+
# get messages from cutoff, excluding system message to avoid duplicates
103+
step_msgs = (
104+
session.query(MessageModel)
105+
.filter(
106+
MessageModel.agent_id == agent_id,
107+
MessageModel.organization_id == self.actor.organization_id,
108+
MessageModel.sequence_id >= cutoff_sequence_id,
109+
MessageModel.role != "system",
110+
)
111+
.order_by(MessageModel.sequence_id.asc())
112+
.all()
113+
)
114+
# combine system message with step messages
115+
msgs = [system_msg] + step_msgs if system_msg else step_msgs
116+
else:
117+
# no user messages, just return system message
118+
msgs = [system_msg] if system_msg else []
119+
else:
120+
# sqlite approach: get all user messages first, then get messages from cutoff
121+
user_messages = (
122+
session.query(MessageModel.sequence_id)
123+
.filter(
124+
MessageModel.agent_id == agent_id,
125+
MessageModel.organization_id == self.actor.organization_id,
126+
MessageModel.role == "user",
127+
)
128+
.order_by(MessageModel.sequence_id.desc())
129+
.limit(self.max_steps)
130+
.all()
131+
)
132+
133+
if user_messages:
134+
# get the minimum sequence_id
135+
cutoff_sequence_id = min(msg.sequence_id for msg in user_messages)
136+
137+
# get messages from cutoff, excluding system message to avoid duplicates
138+
step_msgs = (
139+
session.query(MessageModel)
140+
.filter(
141+
MessageModel.agent_id == agent_id,
142+
MessageModel.organization_id == self.actor.organization_id,
143+
MessageModel.sequence_id >= cutoff_sequence_id,
144+
MessageModel.role != "system",
145+
)
146+
.order_by(MessageModel.sequence_id.asc())
147+
.all()
148+
)
149+
# combine system message with step messages
150+
msgs = [system_msg] + step_msgs if system_msg else step_msgs
151+
else:
152+
# no user messages, just return system message
153+
msgs = [system_msg] if system_msg else []
154+
else:
155+
# if no limit, get all messages in ascending order
156+
msgs = (
157+
session.query(MessageModel)
158+
.filter(
159+
MessageModel.agent_id == agent_id,
160+
MessageModel.organization_id == self.actor.organization_id,
161+
)
162+
.order_by(MessageModel.sequence_id.asc())
163+
.all()
72164
)
73-
.order_by(MessageModel.sequence_id.asc())
74-
.all()
75-
)
76-
# overwrite the “messages” key with a fully serialized list
165+
166+
# overwrite the "messages" key with a fully serialized list
77167
data[self.FIELD_MESSAGES] = [SerializedMessageSchema(session=self.session, actor=self.actor).dump(m) for m in msgs]
78168

79169
return data

letta/server/rest_api/routers/v1/agents.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ def render(self, content: Any) -> bytes:
146146
@router.get("/{agent_id}/export", response_class=IndentedORJSONResponse, operation_id="export_agent_serialized")
147147
def export_agent_serialized(
148148
agent_id: str,
149+
max_steps: int = 100,
149150
server: "SyncServer" = Depends(get_letta_server),
150151
actor_id: str | None = Header(None, alias="user_id"),
151152
# do not remove, used to autogeneration of spec
@@ -158,7 +159,7 @@ def export_agent_serialized(
158159
actor = server.user_manager.get_user_or_default(user_id=actor_id)
159160

160161
try:
161-
agent = server.agent_manager.serialize(agent_id=agent_id, actor=actor)
162+
agent = server.agent_manager.serialize(agent_id=agent_id, actor=actor, max_steps=max_steps)
162163
return agent.model_dump()
163164
except NoResultFound:
164165
raise HTTPException(status_code=404, detail=f"Agent with id={agent_id} not found for user_id={actor.id}.")

letta/services/agent_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,10 +1446,10 @@ async def delete_agent_async(self, agent_id: str, actor: PydanticUser) -> None:
14461446

14471447
@enforce_types
14481448
@trace_method
1449-
def serialize(self, agent_id: str, actor: PydanticUser) -> AgentSchema:
1449+
def serialize(self, agent_id: str, actor: PydanticUser, max_steps: Optional[int] = None) -> AgentSchema:
14501450
with db_registry.session() as session:
14511451
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
1452-
schema = MarshmallowAgentSchema(session=session, actor=actor)
1452+
schema = MarshmallowAgentSchema(session=session, actor=actor, max_steps=max_steps)
14531453
data = schema.dump(agent)
14541454
return AgentSchema(**data)
14551455

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "letta"
3-
version = "0.11.0"
3+
version = "0.11.2"
44
packages = [
55
{include = "letta"},
66
]

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def get_weather(location: str) -> str:
111111
"""
112112
Fetches the current weather for a given location.
113113
114-
Parameters:
114+
Args:
115115
location (str): The location to get the weather for.
116116
117117
Returns:

tests/test_agent_serialization.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,3 +711,51 @@ def test_upload_agentfile_from_disk(server, server_url, disable_e2b_api_key, oth
711711
agent_id=copied_agent_id,
712712
input_messages=[MessageCreate(role=MessageRole.user, content="Hello there!")],
713713
)
714+
715+
716+
def test_serialize_with_max_steps(server, server_url, default_user, other_user):
717+
"""Test that max_steps parameter correctly limits messages by conversation steps."""
718+
# load agent from file with pre-populated messages
719+
file_path = os.path.join(os.path.dirname(__file__), "test_agent_files", "max_messages.af")
720+
721+
with open(file_path, "rb") as f:
722+
files = {"file": ("max_messages.af", f, "application/json")}
723+
724+
form_data = {
725+
"append_copy_suffix": "false",
726+
"override_existing_tools": "false",
727+
}
728+
729+
response = requests.post(
730+
f"{server_url}/v1/agents/import",
731+
headers={"user_id": default_user.id},
732+
files=files,
733+
data=form_data,
734+
)
735+
736+
assert response.status_code == 200, f"Failed to upload agent: {response.text}"
737+
agent_data = response.json()
738+
agent_id = agent_data["id"]
739+
740+
# test with default max_steps (should use None, returning all messages)
741+
full_result = server.agent_manager.serialize(agent_id=agent_id, actor=default_user)
742+
total_messages = len(full_result.messages)
743+
assert total_messages == 31, f"Expected 31 messages, got {total_messages}"
744+
745+
# test with max_steps=2 (should return messages from the last 2 user messages onward)
746+
limited_result = server.agent_manager.serialize(agent_id=agent_id, actor=default_user, max_steps=2)
747+
limited_user_count = sum(1 for msg in limited_result.messages if msg.role == "user")
748+
assert limited_user_count == 2, f"Expected 2 user messages (steps), got {limited_user_count}"
749+
assert len(limited_result.messages) == 2 * 3 + 1
750+
751+
# verify agent can still receive messages after being deserialized with limited steps
752+
agent_copy = server.agent_manager.deserialize(limited_result, actor=other_user, append_copy_suffix=True)
753+
response = server.send_messages(
754+
actor=other_user, agent_id=agent_copy.id, input_messages=[MessageCreate(role=MessageRole.user, content="Hello!")]
755+
)
756+
assert response is not None and response.step_count > 0, "Agent should be able to receive and respond to messages"
757+
758+
# test with max_steps=0 (should return only system message)
759+
empty_result = server.agent_manager.serialize(agent_id=agent_id, actor=default_user, max_steps=0)
760+
assert len(empty_result.messages) == 1, f"Expected 1 message (system), got {len(empty_result.messages)}"
761+
assert empty_result.messages[0].role == "system", "The only message should be the system message"

0 commit comments

Comments
 (0)