diff --git a/letta/helpers/message_helper.py b/letta/helpers/message_helper.py index 84d8cd0b..2ab76833 100644 --- a/letta/helpers/message_helper.py +++ b/letta/helpers/message_helper.py @@ -1,6 +1,7 @@ import asyncio import base64 import mimetypes +from typing import Optional from urllib.parse import unquote, urlparse import httpx @@ -63,7 +64,7 @@ async def _fetch_image_from_url(url: str, max_retries: int = 1, timeout_seconds: async def convert_message_creates_to_messages( message_creates: list[MessageCreate], agent_id: str, - timezone: str, + timezone: Optional[str], run_id: str, wrap_user_message: bool = True, wrap_system_message: bool = True, @@ -86,7 +87,7 @@ async def convert_message_creates_to_messages( async def _convert_message_create_to_message( message_create: MessageCreate, agent_id: str, - timezone: str, + timezone: Optional[str], run_id: str, wrap_user_message: bool = True, wrap_system_message: bool = True, diff --git a/letta/server/rest_api/utils.py b/letta/server/rest_api/utils.py index 66e15572..b04402e1 100644 --- a/letta/server/rest_api/utils.py +++ b/letta/server/rest_api/utils.py @@ -156,7 +156,7 @@ def capture_sentry_exception(e: BaseException): async def create_input_messages( - input_messages: List[MessageCreate], agent_id: str, timezone: str, run_id: str, actor: User + input_messages: List[MessageCreate], agent_id: str, timezone: Optional[str], run_id: str, actor: User ) -> List[Message]: """ Converts a user input message into the internal structured format. @@ -164,9 +164,9 @@ async def create_input_messages( TODO (cliandy): this effectively duplicates the functionality of `convert_message_creates_to_messages`, we should unify this when it's clear what message attributes we need. """ - + wrap_user_message = timezone is not None messages = await convert_message_creates_to_messages( - input_messages, agent_id, timezone, run_id, wrap_user_message=False, wrap_system_message=False + input_messages, agent_id, timezone, run_id, wrap_user_message=wrap_user_message, wrap_system_message=False ) return messages diff --git a/letta/services/agent_manager.py b/letta/services/agent_manager.py index 61ef77ca..211cb06a 100644 --- a/letta/services/agent_manager.py +++ b/letta/services/agent_manager.py @@ -521,7 +521,7 @@ class AgentManager: response_format=agent_create.response_format, created_by_id=actor.id, last_updated_by_id=actor.id, - timezone=agent_create.timezone if agent_create.timezone else DEFAULT_TIMEZONE, + timezone=agent_create.timezone, max_files_open=agent_create.max_files_open, per_file_view_window_char_limit=agent_create.per_file_view_window_char_limit, ) diff --git a/letta/services/helpers/agent_manager_helper.py b/letta/services/helpers/agent_manager_helper.py index 1bcf6683..6554d0e8 100644 --- a/letta/services/helpers/agent_manager_helper.py +++ b/letta/services/helpers/agent_manager_helper.py @@ -472,16 +472,19 @@ async def initialize_message_sequence_async( def package_initial_message_sequence( - agent_id: str, initial_message_sequence: List[MessageCreate], model: str, timezone: str, actor: User + agent_id: str, initial_message_sequence: List[MessageCreate], model: str, timezone: Optional[str], actor: User ) -> List[Message]: # create the agent object init_messages = [] for message_create in initial_message_sequence: if message_create.role == MessageRole.user: - packed_message = system.package_user_message( - user_message=message_create.content, - timezone=timezone, - ) + if timezone is not None: + packed_message = system.package_user_message( + user_message=message_create.content, + timezone=timezone, + ) + else: + packed_message = message_create.content init_messages.append( Message( role=message_create.role, diff --git a/letta/system.py b/letta/system.py index e766420b..6737e3c2 100644 --- a/letta/system.py +++ b/letta/system.py @@ -125,7 +125,7 @@ def get_login_event(timezone, last_login="Never (first login)", include_location def package_user_message( user_message: str, - timezone: str, + timezone: Optional[str], include_location: bool = False, location_name: Optional[str] = "San Francisco, CA, USA", name: Optional[str] = None, diff --git a/tests/managers/test_agent_manager.py b/tests/managers/test_agent_manager.py index a1170d19..fa609f05 100644 --- a/tests/managers/test_agent_manager.py +++ b/tests/managers/test_agent_manager.py @@ -699,6 +699,109 @@ async def test_update_agent(server: SyncServer, comprehensive_test_agent_fixture assert updated_agent.updated_at > last_updated_timestamp +@pytest.mark.asyncio +async def test_create_agent_without_timezone_no_message_packing(server: SyncServer, default_user, default_block): + """Test that agent created without timezone has raw user messages (not JSON wrapped).""" + test_message = "hello world without timezone" + memory_blocks = [CreateBlock(label="human", value="TestUser"), CreateBlock(label="persona", value="I am a helpful assistant")] + create_agent_request = CreateAgent( + agent_type="memgpt_v2_agent", + system="test system", + memory_blocks=memory_blocks, + llm_config=LLMConfig.default_config("gpt-4o-mini"), + embedding_config=EmbeddingConfig.default_config(provider="openai"), + block_ids=[default_block.id], + initial_message_sequence=[MessageCreate(role=MessageRole.user, content=test_message)], + include_base_tools=False, + # No timezone specified - should be None + ) + agent_state = await server.agent_manager.create_agent_async( + create_agent_request, + actor=default_user, + ) + + try: + # Verify timezone is None + assert agent_state.timezone is None, f"Expected timezone to be None, got {agent_state.timezone}" + + # Get the messages + init_messages = await server.message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=default_user) + + # Find the user message + user_messages = [m for m in init_messages if m.role == MessageRole.user] + assert len(user_messages) == 1, f"Expected 1 user message, got {len(user_messages)}" + + # Check that the raw content is the plain text (not JSON wrapped) + raw_content = user_messages[0].content[0].text + assert raw_content == test_message, f"Expected raw message '{test_message}', got '{raw_content}'" + + # Verify it's NOT JSON + try: + parsed = json.loads(raw_content) + # If we get here, the content is JSON - that's wrong for no-timezone case + assert False, f"Expected raw text but got JSON: {parsed}" + except json.JSONDecodeError: + # Expected - the content should not be valid JSON + pass + finally: + await server.agent_manager.delete_agent_async(agent_id=agent_state.id, actor=default_user) + + +@pytest.mark.asyncio +async def test_create_agent_with_timezone_message_packing(server: SyncServer, default_user, default_block): + """Test that agent created with timezone has JSON wrapped user messages with timestamp.""" + test_message = "hello world with timezone" + memory_blocks = [CreateBlock(label="human", value="TestUser"), CreateBlock(label="persona", value="I am a helpful assistant")] + create_agent_request = CreateAgent( + agent_type="memgpt_v2_agent", + system="test system", + memory_blocks=memory_blocks, + llm_config=LLMConfig.default_config("gpt-4o-mini"), + embedding_config=EmbeddingConfig.default_config(provider="openai"), + block_ids=[default_block.id], + initial_message_sequence=[MessageCreate(role=MessageRole.user, content=test_message)], + include_base_tools=False, + timezone="America/Los_Angeles", + ) + agent_state = await server.agent_manager.create_agent_async( + create_agent_request, + actor=default_user, + ) + + try: + # Verify timezone is set + assert agent_state.timezone == "America/Los_Angeles", f"Expected timezone 'America/Los_Angeles', got {agent_state.timezone}" + + # Get the messages + init_messages = await server.message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=default_user) + + # Find the user message + user_messages = [m for m in init_messages if m.role == MessageRole.user] + assert len(user_messages) == 1, f"Expected 1 user message, got {len(user_messages)}" + + # Check that the raw content is JSON wrapped + raw_content = user_messages[0].content[0].text + + # Verify it IS valid JSON with expected structure + parsed = json.loads(raw_content) + assert isinstance(parsed, dict), f"Expected JSON object, got {type(parsed)}" + assert "type" in parsed, "Expected 'type' field in packed message" + assert "message" in parsed, "Expected 'message' field in packed message" + assert "time" in parsed, "Expected 'time' field in packed message" + + # Verify the inner message content + assert parsed["type"] == "user_message", f"Expected type 'user_message', got '{parsed['type']}'" + assert parsed["message"] == test_message, f"Expected inner message '{test_message}', got '{parsed['message']}'" + + # Verify the time contains timezone info (PST or PDT for America/Los_Angeles) + time_str = parsed["time"] + assert any(tz in time_str for tz in ["PST", "PDT", "-0800", "-0700"]), ( + f"Expected Pacific timezone indicator in time string '{time_str}'" + ) + finally: + await server.agent_manager.delete_agent_async(agent_id=agent_state.id, actor=default_user) + + @pytest.mark.asyncio async def test_create_agent_with_compaction_settings(server: SyncServer, default_user, default_block): """Test that agents can be created with custom compaction_settings""" diff --git a/tests/test_client.py b/tests/test_client.py index 7c3b0d08..811241a9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -789,6 +789,81 @@ def test_initial_sequence(client: Letta): # assert agent.timezone == "America/New_York" +def test_agent_timezone_none_no_message_packing(client: Letta): + """Test that agent created without timezone has timezone=None and messages are not JSON wrapped.""" + agent = client.agents.create( + memory_blocks=[{"label": "human", "value": ""}, {"label": "persona", "value": ""}], + model="anthropic/claude-haiku-4-5-20251001", + embedding="openai/text-embedding-3-small", + # No timezone specified + ) + + try: + # Verify timezone is None + retrieved_agent = client.agents.retrieve(agent_id=agent.id) + assert retrieved_agent.timezone is None, f"Expected timezone to be None, got {retrieved_agent.timezone}" + + # Send a message + test_message = "Hello, this is a test message without timezone" + client.agents.messages.create( + agent_id=agent.id, + messages=[MessageCreateParam(role="user", content=test_message)], + ) + + # List messages and find the user message + messages = client.agents.messages.list(agent_id=agent.id).items + user_messages = [m for m in messages if m.message_type == "user_message"] + assert len(user_messages) > 0, "Expected at least one user message" + + # The user message content should be the raw text (not JSON wrapped) + # When timezone is None, the message is stored as-is and retrieved as-is + latest_user_message = user_messages[0] + assert latest_user_message.content == test_message, f"Expected raw message '{test_message}', got '{latest_user_message.content}'" + finally: + client.agents.delete(agent_id=agent.id) + + +def test_agent_timezone_set_message_packing(client: Letta): + """Test that agent created with timezone has messages JSON wrapped with timestamp.""" + agent = client.agents.create( + memory_blocks=[{"label": "human", "value": ""}, {"label": "persona", "value": ""}], + model="anthropic/claude-haiku-4-5-20251001", + embedding="openai/text-embedding-3-small", + timezone="America/Los_Angeles", + ) + + try: + # Verify timezone is set + retrieved_agent = client.agents.retrieve(agent_id=agent.id) + assert retrieved_agent.timezone == "America/Los_Angeles", f"Expected timezone 'America/Los_Angeles', got {retrieved_agent.timezone}" + + # Send a message + test_message = "Hello, this is a test message with timezone" + client.agents.messages.create( + agent_id=agent.id, + messages=[MessageCreateParam(role="user", content=test_message)], + ) + + # List messages and find the user message + messages = client.agents.messages.list(agent_id=agent.id).items + user_messages = [m for m in messages if m.message_type == "user_message"] + assert len(user_messages) > 0, "Expected at least one user message" + + # The user message content should be unpacked to just the message text + # (The API unpacks the JSON wrapper before returning) + latest_user_message = user_messages[0] + assert latest_user_message.content == test_message, ( + f"Expected unpacked message '{test_message}', got '{latest_user_message.content}'" + ) + + # Test that updating timezone works + client.agents.update(agent_id=agent.id, timezone="America/New_York") + updated_agent = client.agents.retrieve(agent_id=agent.id) + assert updated_agent.timezone == "America/New_York", f"Expected updated timezone 'America/New_York', got {updated_agent.timezone}" + finally: + client.agents.delete(agent_id=agent.id) + + def test_attach_sleeptime_block(client: Letta): agent = client.agents.create( memory_blocks=[{"label": "human", "value": ""}, {"label": "persona", "value": ""}],