Spaces:
Sleeping
Sleeping
File size: 6,193 Bytes
9fb3ca7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | """DateTime tool — current time, timezone conversion, date calculations.
Pure Python stdlib — no API needed. Uses the `zoneinfo` module (Python 3.9+).
"""
from __future__ import annotations
import logging
from datetime import datetime, timedelta
from typing import Any
from zoneinfo import ZoneInfo, available_timezones
from app.tools.base_tool import BaseTool
logger = logging.getLogger(__name__)
# Common timezone aliases → IANA names
_TZ_ALIASES: dict[str, str] = {
"est": "America/New_York", "edt": "America/New_York",
"cst": "America/Chicago", "cdt": "America/Chicago",
"mst": "America/Denver", "mdt": "America/Denver",
"pst": "America/Los_Angeles", "pdt": "America/Los_Angeles",
"gmt": "Europe/London", "utc": "UTC",
"bst": "Europe/London",
"cet": "Europe/Paris", "cest": "Europe/Paris",
"ist": "Asia/Kolkata",
"jst": "Asia/Tokyo",
"kst": "Asia/Seoul",
"cst_china": "Asia/Shanghai", "cst china": "Asia/Shanghai",
"hkt": "Asia/Hong_Kong",
"sgt": "Asia/Singapore",
"aest": "Australia/Sydney", "aedt": "Australia/Sydney",
"nzst": "Pacific/Auckland", "nzdt": "Pacific/Auckland",
# City shortcuts
"new york": "America/New_York", "nyc": "America/New_York",
"los angeles": "America/Los_Angeles", "la": "America/Los_Angeles",
"chicago": "America/Chicago",
"london": "Europe/London",
"paris": "Europe/Paris",
"berlin": "Europe/Berlin",
"tokyo": "Asia/Tokyo",
"seoul": "Asia/Seoul",
"beijing": "Asia/Shanghai", "shanghai": "Asia/Shanghai",
"hong kong": "Asia/Hong_Kong",
"singapore": "Asia/Singapore",
"sydney": "Australia/Sydney",
"auckland": "Pacific/Auckland",
"dubai": "Asia/Dubai",
"mumbai": "Asia/Kolkata", "delhi": "Asia/Kolkata",
"kolkata": "Asia/Kolkata", "india": "Asia/Kolkata",
"moscow": "Europe/Moscow",
"toronto": "America/Toronto",
"vancouver": "America/Vancouver",
"denver": "America/Denver",
"cairo": "Africa/Cairo",
"nairobi": "Africa/Nairobi",
"lagos": "Africa/Lagos",
"sao paulo": "America/Sao_Paulo",
"buenos aires": "America/Argentina/Buenos_Aires",
"mexico city": "America/Mexico_City",
"bangkok": "Asia/Bangkok",
"jakarta": "Asia/Jakarta",
"kuala lumpur": "Asia/Kuala_Lumpur",
}
class DateTimeTool(BaseTool):
"""Get current date/time and perform date calculations."""
@property
def name(self) -> str:
return "datetime"
@property
def description(self) -> str:
return (
"Get current date and time, convert between timezones, or calculate "
"date differences. Actions: "
"'now' — current time (optionally in a timezone), "
"'convert' — convert time between timezones, "
"'diff' — days between two dates or days until a date. "
"Example: action='now', timezone='tokyo' or "
"action='diff', date='2025-12-25'."
)
@property
def input_schema(self) -> dict[str, Any]:
return {
"action": {
"type": "string",
"description": "One of: 'now', 'convert', 'diff'.",
},
"timezone": {
"type": "string",
"description": "Timezone name or city (e.g. 'tokyo', 'ist', 'America/New_York'). Optional.",
},
"date": {
"type": "string",
"description": "A date in YYYY-MM-DD format for 'diff' calculations. Optional.",
},
}
def run(self, **kwargs: Any) -> str:
action = str(kwargs.get("action", "now")).strip().lower()
timezone = str(kwargs.get("timezone", "")).strip()
date_str = str(kwargs.get("date", "")).strip()
logger.info("[DATETIME] action=%s tz=%s date=%s", action, timezone, date_str)
if action == "now":
return self._now(timezone)
elif action in ("convert", "conversion"):
return self._now(timezone) # show time in requested TZ
elif action == "diff":
return self._diff(date_str, timezone)
else:
return self._now(timezone)
def _now(self, timezone: str) -> str:
"""Get current time, optionally in a specific timezone."""
tz = self._resolve_tz(timezone) if timezone else None
now = datetime.now(tz or ZoneInfo("UTC"))
tz_name = timezone or "UTC"
return (
f"Current date and time in {tz_name}:\n"
f" Date: {now.strftime('%A, %B %d, %Y')}\n"
f" Time: {now.strftime('%I:%M %p')} ({now.strftime('%H:%M')})\n"
f" Timezone: {now.tzname()} (UTC{now.strftime('%z')})"
)
def _diff(self, date_str: str, timezone: str) -> str:
"""Calculate days between now and a target date."""
if not date_str:
return "Error: 'date' is required for diff (format: YYYY-MM-DD)."
try:
target = datetime.strptime(date_str, "%Y-%m-%d")
except ValueError:
return f"Error: Invalid date format '{date_str}'. Use YYYY-MM-DD."
tz = self._resolve_tz(timezone) if timezone else ZoneInfo("UTC")
now = datetime.now(tz).replace(tzinfo=None)
delta = target - now.replace(hour=0, minute=0, second=0, microsecond=0)
days = delta.days
if days > 0:
return f"{days} days from now until {date_str}."
elif days == 0:
return f"{date_str} is today!"
else:
return f"{date_str} was {abs(days)} days ago."
@staticmethod
def _resolve_tz(tz_input: str) -> ZoneInfo:
"""Resolve a timezone string to a ZoneInfo object."""
key = tz_input.lower().strip()
# Check aliases first
if key in _TZ_ALIASES:
return ZoneInfo(_TZ_ALIASES[key])
# Try as IANA timezone
try:
return ZoneInfo(tz_input)
except (KeyError, Exception):
pass
# Fuzzy match against IANA names
for tz in available_timezones():
if key in tz.lower():
return ZoneInfo(tz)
# Fallback to UTC
return ZoneInfo("UTC")
|