Skip to content

Commit e3a3d3b

Browse files
Timezone Support for datetime Tool and Normalize Response Time Handling (#660)
## Description <!-- Note: The pull request title will be included in the CHANGELOG. --> <!-- Provide a standalone description of changes in this PR. --> - Added timezone support for the `datetime_tools`, so now it will automatically extract the `x-timezone` from the headers from context, which will be used to format the current time. - If the `x-timezone` is not present in the headers, the fallback behavior is checked - The documentations should also be clear now. - Fixed a small issue in `NeMo-Agent-Toolkit/src/nat/builder/user_interaction_manager.py`. Before the timestamp is in UTC (as seen by the `Z` suffix in the timestamp), but the timestamp is in the local timezone, which is most likely not in UTC timezone. - Made a tiny change for the `NeMo-Agent-Toolkit-UI` repository. So now the front-end also stores the timezone in its header to be sent to the backend. - The change: [https://github.com/NVIDIA/NeMo-Agent-Toolkit-UI/pull/38](https://github.com/NVIDIA/NeMo-Agent-Toolkit-UI/pull/38) Closes #445 ## By Submitting this PR I confirm: - I am familiar with the [Contributing Guidelines](https://github.com/NVIDIA/NeMo-Agent-Toolkit/blob/develop/docs/source/resources/contributing.md). - We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license. - Any contribution which contains commits that are not Signed-Off will not be accepted. - When the PR is ready for review, new or existing tests cover these changes. - When the PR is ready for review, the documentation is up to date with these changes. --------- Signed-off-by: Daniel Wang <daniewang@nvidia.com>
1 parent aec0975 commit e3a3d3b

4 files changed

Lines changed: 65 additions & 13 deletions

File tree

external/nat-ui

src/nat/builder/user_interaction_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,13 @@ async def prompt_user_input(self, content: HumanPrompt) -> InteractionResponse:
6161

6262
uuid_req = str(uuid.uuid4())
6363
status = InteractionStatus.IN_PROGRESS
64-
timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ")
64+
timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
6565
sys_human_interaction = InteractionPrompt(id=uuid_req, status=status, timestamp=timestamp, content=content)
6666

6767
resp = await self._context_state.user_input_callback.get()(sys_human_interaction)
6868

6969
# Rebuild a InteractionResponse object with the response
70-
timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ")
70+
timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
7171
status = InteractionStatus.COMPLETED
7272
sys_human_interaction = InteractionResponse(id=uuid_req, status=status, timestamp=timestamp, content=resp)
7373

src/nat/settings/global_settings.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ class Settings(HashableBaseModel):
4747
# Registry Handeler Configuration
4848
channels: dict[str, RegistryHandlerBaseConfig] = {}
4949

50+
# Timezone fallback behavior
51+
# Options:
52+
# - "utc": default to UTC
53+
# - "system": use the system's local timezone
54+
fallback_timezone: typing.Literal["system", "utc"] = "utc"
55+
5056
_configuration_directory: typing.ClassVar[str]
5157
_settings_changed_hooks: typing.ClassVar[list[Callable[[], None]]] = []
5258
_settings_changed_hooks_active: bool = True
@@ -165,7 +171,11 @@ def from_file():
165171
loaded_config = {}
166172
else:
167173
with open(file_path, mode="r", encoding="utf-8") as f:
168-
loaded_config = json.load(f)
174+
try:
175+
loaded_config = json.load(f)
176+
except Exception as e:
177+
logger.exception("Error loading configuration file %s: %s", file_path, e)
178+
loaded_config = {}
169179

170180
settings = Settings(**loaded_config)
171181
settings.set_configuration_directory(configuration_directory)
@@ -214,6 +224,8 @@ def _revalidate(self, config_dict) -> bool:
214224
match field:
215225
case "channels":
216226
self.channels = validated_data.channels
227+
case "fallback_timezone":
228+
self.fallback_timezone = validated_data.fallback_timezone
217229
case _:
218230
raise ValueError(f"Encountered invalid model field: {field}")
219231

src/nat/tool/datetime_tools.py

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,70 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16+
import datetime
17+
import zoneinfo
18+
19+
from starlette.datastructures import Headers
20+
1621
from nat.builder.builder import Builder
1722
from nat.builder.function_info import FunctionInfo
1823
from nat.cli.register_workflow import register_function
1924
from nat.data_models.function import FunctionBaseConfig
25+
from nat.settings.global_settings import GlobalSettings
2026

2127

2228
class CurrentTimeToolConfig(FunctionBaseConfig, name="current_datetime"):
2329
"""
24-
Simple tool which returns the current date and time in human readable format.
30+
Simple tool which returns the current date and time in human readable format with timezone information. By default,
31+
the timezone is in Etc/UTC. If the user provides a timezone in the header, we will use it. Timezone will be
32+
provided in IANA zone name format. For example, "America/New_York" or "Etc/UTC".
2533
"""
2634
pass
2735

2836

29-
@register_function(config_type=CurrentTimeToolConfig)
30-
async def current_datetime(config: CurrentTimeToolConfig, builder: Builder):
37+
def _get_timezone_obj(headers: Headers | None) -> zoneinfo.ZoneInfo | datetime.tzinfo:
38+
# Default to UTC
39+
timezone_obj = zoneinfo.ZoneInfo("Etc/UTC")
40+
41+
if headers:
42+
# If user has provided a timezone in the header, we will prioritize on using it
43+
timezone_header = headers.get("x-timezone")
44+
if timezone_header:
45+
try:
46+
timezone_obj = zoneinfo.ZoneInfo(timezone_header)
47+
except Exception:
48+
pass
49+
else:
50+
# Only if a timezone is not in the header, we will determine default timezone based on global settings
51+
fallback_tz = GlobalSettings.get().fallback_timezone
52+
53+
if fallback_tz == "system":
54+
# Use the system's local timezone. Avoid requiring external deps.
55+
timezone_obj = datetime.datetime.now().astimezone().tzinfo or zoneinfo.ZoneInfo("Etc/UTC")
56+
57+
return timezone_obj
3158

32-
import datetime
59+
60+
@register_function(config_type=CurrentTimeToolConfig)
61+
async def current_datetime(_config: CurrentTimeToolConfig, _builder: Builder):
3362

3463
async def _get_current_time(unused: str) -> str:
3564

36-
now = datetime.datetime.now() # Get current time
37-
now_human_readable = now.strftime(("%Y-%m-%d %H:%M:%S"))
65+
del unused # Unused parameter to avoid linting error
66+
67+
from nat.builder.context import Context
68+
nat_context = Context.get()
69+
70+
headers: Headers | None = nat_context.metadata.headers
71+
72+
timezone_obj = _get_timezone_obj(headers)
73+
74+
now = datetime.datetime.now(timezone_obj)
75+
now_machine_readable = now.strftime(("%Y-%m-%d %H:%M:%S %z"))
3876

39-
return f"The current time of day is {now_human_readable}" # Format time in H:MM AM/PM format
77+
# Returns the current time in machine readable format with timezone offset.
78+
return f"The current time of day is {now_machine_readable}"
4079

41-
yield FunctionInfo.from_fn(_get_current_time,
42-
description="Returns the current date and time in human readable format.")
80+
yield FunctionInfo.from_fn(
81+
_get_current_time,
82+
description="Returns the current date and time in human readable format with timezone information.")

0 commit comments

Comments
 (0)