From 2e06feafbf557e7ce0136125e92e1421de84e311 Mon Sep 17 00:00:00 2001 From: Sarah Wooders Date: Wed, 25 Jun 2025 14:51:37 -0700 Subject: [PATCH] fix: add more timezone fixes (#3025) --- letta/agent.py | 1 + letta/agents/base_agent.py | 1 + letta/agents/voice_agent.py | 1 + letta/constants.py | 1 + letta/helpers/datetime_helpers.py | 39 ++++++++----------- letta/orm/agent.py | 2 - letta/services/agent_manager.py | 5 ++- .../services/helpers/agent_manager_helper.py | 10 +++-- 8 files changed, 31 insertions(+), 29 deletions(-) diff --git a/letta/agent.py b/letta/agent.py index d6ce90fc..90f5494f 100644 --- a/letta/agent.py +++ b/letta/agent.py @@ -1246,6 +1246,7 @@ class Agent(BaseAgent): message_manager_size = self.message_manager.size(actor=self.user, agent_id=self.agent_state.id) external_memory_summary = compile_memory_metadata_block( memory_edit_timestamp=get_utc_time(), + timezone=self.agent_state.timezone, previous_message_count=self.message_manager.size(actor=self.user, agent_id=self.agent_state.id), archival_memory_size=self.agent_manager.passage_size(actor=self.user, agent_id=self.agent_state.id), ) diff --git a/letta/agents/base_agent.py b/letta/agents/base_agent.py index bf451329..7caa3d12 100644 --- a/letta/agents/base_agent.py +++ b/letta/agents/base_agent.py @@ -120,6 +120,7 @@ class BaseAgent(ABC): system_prompt=agent_state.system, in_context_memory=agent_state.memory, in_context_memory_last_edit=memory_edit_timestamp, + timezone=agent_state.timezone, previous_message_count=num_messages - len(in_context_messages), archival_memory_size=num_archival_memories, tool_rules_solver=tool_rules_solver, diff --git a/letta/agents/voice_agent.py b/letta/agents/voice_agent.py index edc18fd2..164a88e7 100644 --- a/letta/agents/voice_agent.py +++ b/letta/agents/voice_agent.py @@ -150,6 +150,7 @@ class VoiceAgent(BaseAgent): system_prompt=agent_state.system, in_context_memory=agent_state.memory, in_context_memory_last_edit=memory_edit_timestamp, + timezone=agent_state.timezone, previous_message_count=self.num_messages, archival_memory_size=self.num_archival_memories, ) diff --git a/letta/constants.py b/letta/constants.py index 95854808..b3f568aa 100644 --- a/letta/constants.py +++ b/letta/constants.py @@ -6,6 +6,7 @@ LETTA_DIR = os.path.join(os.path.expanduser("~"), ".letta") LETTA_TOOL_EXECUTION_DIR = os.path.join(LETTA_DIR, "tool_execution_dir") LETTA_MODEL_ENDPOINT = "https://inference.letta.com" +DEFAULT_TIMEZONE = "UTC" ADMIN_PREFIX = "/v1/admin" API_PREFIX = "/v1" diff --git a/letta/helpers/datetime_helpers.py b/letta/helpers/datetime_helpers.py index 9c5cd825..0633add3 100644 --- a/letta/helpers/datetime_helpers.py +++ b/letta/helpers/datetime_helpers.py @@ -2,11 +2,12 @@ import re import time from datetime import datetime, timedelta from datetime import timezone as dt_timezone -from time import strftime from typing import Callable import pytz +from letta.constants import DEFAULT_TIMEZONE + def parse_formatted_time(formatted_time): # parse times returned by letta.utils.get_formatted_time() @@ -18,33 +19,22 @@ def datetime_to_timestamp(dt): return int(dt.timestamp()) -def get_local_time_military(): - # Get the current time in UTC +def get_local_time_fast(timezone): + # Get current UTC time and convert to the specified timezone + if not timezone: + return datetime.now().strftime("%Y-%m-%d %I:%M:%S %p %Z%z") current_time_utc = datetime.now(pytz.utc) - - # Convert to San Francisco's time zone (PST/PDT) - sf_time_zone = pytz.timezone("America/Los_Angeles") - local_time = current_time_utc.astimezone(sf_time_zone) - - # You may format it as you desire - formatted_time = local_time.strftime("%Y-%m-%d %H:%M:%S %Z%z") + local_time = current_time_utc.astimezone(pytz.timezone(timezone)) + formatted_time = local_time.strftime("%Y-%m-%d %I:%M:%S %p %Z%z") return formatted_time -def get_local_time_fast(): - formatted_time = strftime("%Y-%m-%d %I:%M:%S %p %Z%z") - - return formatted_time - - -def get_local_time_timezone(timezone="America/Los_Angeles"): +def get_local_time_timezone(timezone=DEFAULT_TIMEZONE): # Get the current time in UTC current_time_utc = datetime.now(pytz.utc) - # Convert to San Francisco's time zone (PST/PDT) - sf_time_zone = pytz.timezone(timezone) - local_time = current_time_utc.astimezone(sf_time_zone) + local_time = current_time_utc.astimezone(pytz.timezone(timezone)) # You may format it as you desire, including AM/PM formatted_time = local_time.strftime("%Y-%m-%d %I:%M:%S %p %Z%z") @@ -52,7 +42,7 @@ def get_local_time_timezone(timezone="America/Los_Angeles"): return formatted_time -def get_local_time(timezone=None): +def get_local_time(timezone=DEFAULT_TIMEZONE): if timezone is not None: time_str = get_local_time_timezone(timezone) else: @@ -89,8 +79,11 @@ def timestamp_to_datetime(timestamp_seconds: int) -> datetime: return datetime.fromtimestamp(timestamp_seconds, tz=dt_timezone.utc) -def format_datetime(dt): - return dt.strftime("%Y-%m-%d %I:%M:%S %p %Z%z") +def format_datetime(dt, timezone): + if not timezone: + # use local timezone + return dt.strftime("%Y-%m-%d %I:%M:%S %p %Z%z") + return dt.astimezone(pytz.timezone(timezone)).strftime("%Y-%m-%d %I:%M:%S %p %Z%z") def validate_date_format(date_str): diff --git a/letta/orm/agent.py b/letta/orm/agent.py index 219070f2..4a5dae96 100644 --- a/letta/orm/agent.py +++ b/letta/orm/agent.py @@ -190,7 +190,6 @@ class Agent(SqlalchemyBase, OrganizationMixin, AsyncAttrs): "response_format": self.response_format, "last_run_completion": self.last_run_completion, "last_run_duration_ms": self.last_run_duration_ms, - "timezone": self.timezone, # optional field defaults "tags": [], "tools": [], @@ -269,7 +268,6 @@ class Agent(SqlalchemyBase, OrganizationMixin, AsyncAttrs): "response_format": self.response_format, "last_run_completion": self.last_run_completion, "last_run_duration_ms": self.last_run_duration_ms, - "timezone": self.timezone, } optional_fields = { "tags": [], diff --git a/letta/services/agent_manager.py b/letta/services/agent_manager.py index 0542f3c1..1627bde9 100644 --- a/letta/services/agent_manager.py +++ b/letta/services/agent_manager.py @@ -15,6 +15,7 @@ from letta.constants import ( BASE_TOOLS, BASE_VOICE_SLEEPTIME_CHAT_TOOLS, BASE_VOICE_SLEEPTIME_TOOLS, + DEFAULT_TIMEZONE, FILES_TOOLS, MULTI_AGENT_TOOLS, ) @@ -481,7 +482,7 @@ class AgentManager: response_format=agent_create.response_format, created_by_id=actor.id, last_updated_by_id=actor.id, - timezone=agent_create.timezone, + timezone=agent_create.timezone if agent_create.timezone else DEFAULT_TIMEZONE, ) if _test_only_force_id: @@ -1429,6 +1430,7 @@ class AgentManager: system_prompt=agent_state.system, in_context_memory=agent_state.memory, in_context_memory_last_edit=memory_edit_timestamp, + timezone=agent_state.timezone, previous_message_count=num_messages - len(agent_state.message_ids), archival_memory_size=num_archival_memories, ) @@ -1503,6 +1505,7 @@ class AgentManager: system_prompt=agent_state.system, in_context_memory=agent_state.memory, in_context_memory_last_edit=memory_edit_timestamp, + timezone=agent_state.timezone, previous_message_count=num_messages - len(agent_state.message_ids), archival_memory_size=num_archival_memories, tool_rules_solver=tool_rules_solver, diff --git a/letta/services/helpers/agent_manager_helper.py b/letta/services/helpers/agent_manager_helper.py index 43ff09e3..2dae7dbd 100644 --- a/letta/services/helpers/agent_manager_helper.py +++ b/letta/services/helpers/agent_manager_helper.py @@ -9,7 +9,7 @@ from letta import system from letta.constants import IN_CONTEXT_MEMORY_KEYWORD, MAX_EMBEDDING_DIM, STRUCTURED_OUTPUT_MODELS from letta.embeddings import embedding_model from letta.helpers import ToolRulesSolver -from letta.helpers.datetime_helpers import get_local_time, get_local_time_fast +from letta.helpers.datetime_helpers import format_datetime, get_local_time, get_local_time_fast from letta.orm import AgentPassage, SourcePassage, SourcesAgents from letta.orm.agent import Agent as AgentModel from letta.orm.agents_tags import AgentsTags @@ -179,17 +179,18 @@ def derive_system_message(agent_type: AgentType, enable_sleeptime: Optional[bool # TODO: This code is kind of wonky and deserves a rewrite def compile_memory_metadata_block( memory_edit_timestamp: datetime, + timezone: str, previous_message_count: int = 0, archival_memory_size: int = 0, ) -> str: # Put the timestamp in the local timezone (mimicking get_local_time()) - timestamp_str = memory_edit_timestamp.astimezone().strftime("%Y-%m-%d %I:%M:%S %p %Z%z").strip() + timestamp_str = format_datetime(memory_edit_timestamp, timezone) # Create a metadata block of info so the agent knows about the metadata of out-of-context memories memory_metadata_block = "\n".join( [ "", - f"- The current time is: {get_local_time_fast()}", + f"- The current time is: {get_local_time_fast(timezone)}", f"- Memory blocks were last modified: {timestamp_str}", f"- {previous_message_count} previous messages between you and the user are stored in recall memory (use tools to access them)", f"- {archival_memory_size} total memories you created are stored in archival memory (use tools to access them)", @@ -224,6 +225,7 @@ def compile_system_message( system_prompt: str, in_context_memory: Memory, in_context_memory_last_edit: datetime, # TODO move this inside of BaseMemory? + timezone: str, user_defined_variables: Optional[dict] = None, append_icm_if_missing: bool = True, template_format: Literal["f-string", "mustache", "jinja2"] = "f-string", @@ -258,6 +260,7 @@ def compile_system_message( memory_edit_timestamp=in_context_memory_last_edit, previous_message_count=previous_message_count, archival_memory_size=archival_memory_size, + timezone=timezone, ) full_memory_string = in_context_memory.compile(tool_usage_rules=tool_constraint_block) + "\n\n" + memory_metadata_string @@ -303,6 +306,7 @@ def initialize_message_sequence( system_prompt=agent_state.system, in_context_memory=agent_state.memory, in_context_memory_last_edit=memory_edit_timestamp, + timezone=agent_state.timezone, user_defined_variables=None, append_icm_if_missing=True, previous_message_count=previous_message_count,