From eb281fb97ec33abf813c9a04bcf2bb1c3cf706a6 Mon Sep 17 00:00:00 2001 From: Redo-Xz Date: Sat, 27 Jun 2026 23:32:09 +0500 Subject: [PATCH 1/4] feat: Add date and time awareness tool (#20) --- tapio/tools/__init__.py | 4 ++++ tapio/tools/datetime_tool.py | 23 +++++++++++++++++++++++ tests/tools/test_datetime_tool.py | 13 +++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 tapio/tools/__init__.py create mode 100644 tapio/tools/datetime_tool.py create mode 100644 tests/tools/test_datetime_tool.py diff --git a/tapio/tools/__init__.py b/tapio/tools/__init__.py new file mode 100644 index 0000000..f447e00 --- /dev/null +++ b/tapio/tools/__init__.py @@ -0,0 +1,4 @@ +from tapio.tools.datetime_tool import get_current_datetime + +ALL_TOOLS = [get_current_datetime] +__all__ = ["ALL_TOOLS", "get_current_datetime"] \ No newline at end of file diff --git a/tapio/tools/datetime_tool.py b/tapio/tools/datetime_tool.py new file mode 100644 index 0000000..a24447c --- /dev/null +++ b/tapio/tools/datetime_tool.py @@ -0,0 +1,23 @@ +from datetime import datetime, timezone +from zoneinfo import ZoneInfo + +from langchain_core.tools import tool + +# TODO: Get user timezone via UI or setting, since not all users are in Finland +HELSINKI_TZ = ZoneInfo("Europe/Helsinki") + + +@tool +def get_current_datetime() -> str: + """Return the current date and time in Helsinki (Finnish) local time. + + Use this tool whenever the user's question involves today's date, current + deadlines, permit processing windows, office opening hours, or any other + context that requires knowing what day or time it is right now. + + Returns: + A human-readable string with the current weekday, date, and time in + Helsinki local time, e.g. "Tuesday, 27 June 2026, 14:32 EEST". + """ + now = datetime.now(timezone.utc).astimezone(HELSINKI_TZ) + return now.strftime("%A, %d %B %Y, %H:%M %Z") \ No newline at end of file diff --git a/tests/tools/test_datetime_tool.py b/tests/tools/test_datetime_tool.py new file mode 100644 index 0000000..a6d6c00 --- /dev/null +++ b/tests/tools/test_datetime_tool.py @@ -0,0 +1,13 @@ +from tapio.tools.datetime_tool import get_current_datetime + + +def test_returns_string(): + result = get_current_datetime.invoke({}) + assert isinstance(result, str) + assert len(result) > 0 + + +def test_includes_timezone(): + result = get_current_datetime.invoke({}) + # Helsinki is EET (UTC+2) or EEST (UTC+3) depending on DST + assert "EET" in result or "EEST" in result \ No newline at end of file From d4316f53b533d80e59fa70cbe1941fcc857342a6 Mon Sep 17 00:00:00 2001 From: Redo-Xz Date: Sun, 28 Jun 2026 13:52:03 +0500 Subject: [PATCH 2/4] fix: resolve Helsinki timezone with UTC fallback and add tzdata as runtime dependancy --- pyproject.toml | 1 + tapio/tools/datetime_tool.py | 14 ++++++++---- tests/tools/test_datetime_tool.py | 18 ++++++++++++++- uv.lock | 38 ++++++++++++++++--------------- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a0c736c..8dce474 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "pydantic>=2.13.4", "httpx>=0.28.1", "beautifulsoup4>=4.13.4", + "tzdata>=2026.2", ] [project.scripts] diff --git a/tapio/tools/datetime_tool.py b/tapio/tools/datetime_tool.py index a24447c..d35205b 100644 --- a/tapio/tools/datetime_tool.py +++ b/tapio/tools/datetime_tool.py @@ -1,10 +1,14 @@ -from datetime import datetime, timezone -from zoneinfo import ZoneInfo +from datetime import datetime, timezone, tzinfo +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from langchain_core.tools import tool -# TODO: Get user timezone via UI or setting, since not all users are in Finland -HELSINKI_TZ = ZoneInfo("Europe/Helsinki") + +def _get_helsinki_tz() -> tzinfo: + try: + return ZoneInfo("Europe/Helsinki") + except ZoneInfoNotFoundError: + return timezone.utc @tool @@ -19,5 +23,5 @@ def get_current_datetime() -> str: A human-readable string with the current weekday, date, and time in Helsinki local time, e.g. "Tuesday, 27 June 2026, 14:32 EEST". """ - now = datetime.now(timezone.utc).astimezone(HELSINKI_TZ) + now = datetime.now(timezone.utc).astimezone(_get_helsinki_tz()) return now.strftime("%A, %d %B %Y, %H:%M %Z") \ No newline at end of file diff --git a/tests/tools/test_datetime_tool.py b/tests/tools/test_datetime_tool.py index a6d6c00..7c43c73 100644 --- a/tests/tools/test_datetime_tool.py +++ b/tests/tools/test_datetime_tool.py @@ -1,4 +1,7 @@ from tapio.tools.datetime_tool import get_current_datetime +from tapio.tools import datetime_tool +from zoneinfo import ZoneInfoNotFoundError + def test_returns_string(): @@ -10,4 +13,17 @@ def test_returns_string(): def test_includes_timezone(): result = get_current_datetime.invoke({}) # Helsinki is EET (UTC+2) or EEST (UTC+3) depending on DST - assert "EET" in result or "EEST" in result \ No newline at end of file + assert "EET" in result or "EEST" in result + + +def test_when_tzdata_missing(monkeypatch): + """If the IANA timezone database isn't available, the tool should + fall back to UTC.""" + + def raise_zone_info_not_found(name): + raise ZoneInfoNotFoundError(name) + monkeypatch.setattr(datetime_tool, "ZoneInfo", raise_zone_info_not_found) + + result = get_current_datetime.invoke({}) + assert isinstance(result, str) + assert len(result) > 0 \ No newline at end of file diff --git a/uv.lock b/uv.lock index 7075e2d..ea536e1 100644 --- a/uv.lock +++ b/uv.lock @@ -470,7 +470,7 @@ name = "cuda-bindings" version = "13.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "cuda-pathfinder" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/b1/81/bff68ce829999c1e4209c761bbf903b1c06ec570416ddb25020864ad5907/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ab2f74ed65bfef4163ba07a8db16f1085e0729291db12a2423aff84ee8278b8", size = 6013639, upload-time = "2026-05-29T23:12:03.509Z" }, @@ -497,34 +497,34 @@ wheels = [ [package.optional-dependencies] cudart = [ - { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime" }, ] cufft = [ - { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufft" }, ] cufile = [ - { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufile" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti" }, ] curand = [ - { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, + { name = "nvidia-curand" }, ] cusolver = [ - { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusolver" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvtx" }, ] [[package]] @@ -1551,7 +1551,7 @@ name = "nvidia-cublas" version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cuda-nvrtc" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, @@ -1590,7 +1590,7 @@ name = "nvidia-cudnn-cu13" version = "9.20.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cublas" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, @@ -1602,7 +1602,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -1632,9 +1632,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -1646,7 +1646,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, @@ -2775,6 +2775,7 @@ dependencies = [ { name = "python-frontmatter" }, { name = "pyyaml" }, { name = "typer" }, + { name = "tzdata" }, ] [package.dev-dependencies] @@ -2807,6 +2808,7 @@ requires-dist = [ { name = "python-frontmatter", specifier = ">=1.1.0" }, { name = "pyyaml", specifier = ">=6.0.1" }, { name = "typer", specifier = ">=0.9.0" }, + { name = "tzdata", specifier = ">=2026.2" }, ] [package.metadata.requires-dev] From 86a7da78b180e1cc040154e8cde017910fc9b256 Mon Sep 17 00:00:00 2001 From: Redo-Xz Date: Sun, 28 Jun 2026 13:59:00 +0500 Subject: [PATCH 3/4] test: assert UTC fallback explicitly in tzdata-missing test --- tests/tools/test_datetime_tool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_datetime_tool.py b/tests/tools/test_datetime_tool.py index 7c43c73..888e203 100644 --- a/tests/tools/test_datetime_tool.py +++ b/tests/tools/test_datetime_tool.py @@ -26,4 +26,5 @@ def raise_zone_info_not_found(name): result = get_current_datetime.invoke({}) assert isinstance(result, str) - assert len(result) > 0 \ No newline at end of file + assert len(result) > 0 + assert "UTC" in result \ No newline at end of file From 74cfa93686a6819b010d7025be213fb340189845 Mon Sep 17 00:00:00 2001 From: Redo-Xz Date: Sun, 28 Jun 2026 21:14:10 +0500 Subject: [PATCH 4/4] fix: clean formatting and add docstrings --- tapio/tools/__init__.py | 3 ++- tapio/tools/datetime_tool.py | 10 ++++++---- tests/tools/test_datetime_tool.py | 13 +++++++------ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/tapio/tools/__init__.py b/tapio/tools/__init__.py index f447e00..dd0fe57 100644 --- a/tapio/tools/__init__.py +++ b/tapio/tools/__init__.py @@ -1,4 +1,5 @@ +"""Tool registry package for Tapio's agent tools.""" from tapio.tools.datetime_tool import get_current_datetime ALL_TOOLS = [get_current_datetime] -__all__ = ["ALL_TOOLS", "get_current_datetime"] \ No newline at end of file +__all__ = ["ALL_TOOLS", "get_current_datetime"] diff --git a/tapio/tools/datetime_tool.py b/tapio/tools/datetime_tool.py index d35205b..736a09f 100644 --- a/tapio/tools/datetime_tool.py +++ b/tapio/tools/datetime_tool.py @@ -1,4 +1,6 @@ -from datetime import datetime, timezone, tzinfo +"""Date and time awareness tool.""" + +from datetime import UTC, datetime, tzinfo from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from langchain_core.tools import tool @@ -8,7 +10,7 @@ def _get_helsinki_tz() -> tzinfo: try: return ZoneInfo("Europe/Helsinki") except ZoneInfoNotFoundError: - return timezone.utc + return UTC @tool @@ -23,5 +25,5 @@ def get_current_datetime() -> str: A human-readable string with the current weekday, date, and time in Helsinki local time, e.g. "Tuesday, 27 June 2026, 14:32 EEST". """ - now = datetime.now(timezone.utc).astimezone(_get_helsinki_tz()) - return now.strftime("%A, %d %B %Y, %H:%M %Z") \ No newline at end of file + now = datetime.now(UTC).astimezone(_get_helsinki_tz()) + return now.strftime("%A, %d %B %Y, %H:%M %Z") diff --git a/tests/tools/test_datetime_tool.py b/tests/tools/test_datetime_tool.py index 888e203..2c5fb47 100644 --- a/tests/tools/test_datetime_tool.py +++ b/tests/tools/test_datetime_tool.py @@ -1,7 +1,7 @@ -from tapio.tools.datetime_tool import get_current_datetime -from tapio.tools import datetime_tool from zoneinfo import ZoneInfoNotFoundError +from tapio.tools import datetime_tool +from tapio.tools.datetime_tool import get_current_datetime def test_returns_string(): @@ -14,17 +14,18 @@ def test_includes_timezone(): result = get_current_datetime.invoke({}) # Helsinki is EET (UTC+2) or EEST (UTC+3) depending on DST assert "EET" in result or "EEST" in result - - + + def test_when_tzdata_missing(monkeypatch): """If the IANA timezone database isn't available, the tool should fall back to UTC.""" - + def raise_zone_info_not_found(name): raise ZoneInfoNotFoundError(name) + monkeypatch.setattr(datetime_tool, "ZoneInfo", raise_zone_info_not_found) result = get_current_datetime.invoke({}) assert isinstance(result, str) assert len(result) > 0 - assert "UTC" in result \ No newline at end of file + assert "UTC" in result