diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java index e351bef..e8fd589 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java @@ -187,7 +187,7 @@ private String systemPrompt() { 当前时区是 Asia/Shanghai。 今天是 %s,昨天是 %s,明天是 %s。 当用户提到今天、昨天、明天、上周、本周等相对日期时,必须先按以上当前日期换算成具体日期。 - 查询日期、星期、节假日、节气或重要日子时,优先使用 get_date_info 工具。 + 查询日期、星期、节假日、调休或日期差时,优先使用 get_date_info 工具。 """ .formatted(today, today.minusDays(1), today.plusDays(1)); } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/DateInfoTool.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/DateInfoTool.java index 6dd6b68..860d245 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/DateInfoTool.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/DateInfoTool.java @@ -17,21 +17,24 @@ */ package io.github.malonetalk.agent.tools; -import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.ObjectMapper; import io.agentscope.core.tool.Tool; import io.agentscope.core.tool.ToolParam; import java.io.IOException; -import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; import java.time.Clock; +import java.time.Duration; import java.time.LocalDate; import java.time.ZoneId; import java.time.format.TextStyle; -import java.util.List; +import java.time.temporal.ChronoUnit; import java.util.Locale; -import java.util.Map; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -40,23 +43,21 @@ public class DateInfoTool implements MarkAgentTool { private static final String DEFAULT_TIMEZONE = "Asia/Shanghai"; - private static final String PUBLIC_HOLIDAY = "PUBLIC_HOLIDAY"; - private static final String ADJUSTED_WORKDAY = "ADJUSTED_WORKDAY"; - private static final TypeReference HOLIDAY_CALENDAR_TYPE = - new TypeReference<>() {}; + private static final String HOLIDAY_API_BASE_URL = "https://timor.tech/api/holiday/info"; private final ObjectMapper objectMapper; private final Clock clock; - private final Map> calendars = new ConcurrentHashMap<>(); + private final HttpClient httpClient; @Autowired public DateInfoTool(ObjectMapper objectMapper) { - this(objectMapper, Clock.systemDefaultZone()); + this(objectMapper, Clock.systemDefaultZone(), HttpClient.newHttpClient()); } - DateInfoTool(ObjectMapper objectMapper, Clock clock) { + DateInfoTool(ObjectMapper objectMapper, Clock clock, HttpClient httpClient) { this.objectMapper = objectMapper; this.clock = clock; + this.httpClient = httpClient; } @Tool( @@ -64,17 +65,28 @@ public DateInfoTool(ObjectMapper objectMapper) { description = """ Get accurate date information, including weekday, weekend, Chinese public \ - holidays, adjusted workdays, festivals, solar terms and important days. Use \ - this whenever the user asks about today, a date, weekday, whether a day is a \ - holiday, or Chinese holiday schedule. For relative dates, convert them using \ - the system prompt's current date before passing date; omit date for today.\ + holidays, holiday schedule adjustments, and optional difference to another \ + date. Use this whenever the user asks about today, a date, weekday, whether a \ + day is a holiday, Chinese holiday schedule, or date difference. For relative \ + dates, convert them using the system prompt's current date before passing \ + date; omit date for today. Pass end_date only when a calendar-day difference \ + is needed. Date difference counts start date inclusive and end date exclusive.\ """) public String getDateInfo( @ToolParam( name = "date", - description = "Date in yyyy-MM-dd format. Defaults to today.", + description = + "Date in yyyy-MM-dd format. Defaults to today. Also acts as" + + " start date when end_date is provided.", required = false) String date, + @ToolParam( + name = "end_date", + description = + "Optional end date in yyyy-MM-dd format for date difference" + + " calculation.", + required = false) + String endDate, @ToolParam( name = "timezone", description = @@ -82,63 +94,76 @@ public String getDateInfo( required = false) String timezone) { try { - return objectMapper.writeValueAsString(resolve(date, timezone)); + return objectMapper.writeValueAsString(resolve(date, endDate, timezone)); } catch (Exception e) { return "Error: failed to get date info: " + e.getMessage(); } } DateInfo resolve(String dateText, String timezoneText) { + return resolve(dateText, null, timezoneText); + } + + DateInfo resolve(String dateText, String endDateText, String timezoneText) { ZoneId zoneId = ZoneId.of(StringUtils.hasText(timezoneText) ? timezoneText : DEFAULT_TIMEZONE); LocalDate date = StringUtils.hasText(dateText) ? LocalDate.parse(dateText) : LocalDate.now(clock.withZone(zoneId)); - Optional calendar = calendar(date.getYear()); - HolidayInfo holidayInfo = calendar.map(c -> c.days().get(date.toString())).orElse(null); - List importantDays = - calendar.map(c -> events(c).getOrDefault(date.toString(), List.of())) - .orElse(List.of()); - - boolean legalHoliday = holidayInfo != null && PUBLIC_HOLIDAY.equals(holidayInfo.type()); - boolean adjustedWorkday = - holidayInfo != null && ADJUSTED_WORKDAY.equals(holidayInfo.type()); + Optional holidayApiResponse = queryHolidayApi(date); + HolidayDetail holiday = holidayApiResponse.map(HolidayApiResponse::holiday).orElse(null); + + boolean legalHoliday = holiday != null && Boolean.TRUE.equals(holiday.holiday()); + boolean adjustedWorkday = holiday != null && Boolean.FALSE.equals(holiday.holiday()); boolean weekend = date.getDayOfWeek().getValue() >= 6; - boolean dayOff = legalHoliday || (weekend && !adjustedWorkday); return new DateInfo( date.toString(), - zoneId.getId(), date.getDayOfWeek().getDisplayName(TextStyle.FULL, Locale.CHINA), - date.getDayOfWeek().getValue(), - weekend, - dayOff, - legalHoliday, - adjustedWorkday, - holidayInfo != null ? holidayInfo.name() : null, + holiday != null ? holiday.name() : null, dayType(legalHoliday, adjustedWorkday, weekend), - importantDays, - calendar.isPresent()); + holidayApiResponse.isPresent(), + StringUtils.hasText(endDateText) + ? resolveDateDiff(date, LocalDate.parse(endDateText)) + : null); } - private Optional calendar(int year) { - return calendars.computeIfAbsent(year, this::loadCalendar); + DateDiff resolveDateDiff(LocalDate startDate, LocalDate endDate) { + long calendarDays = ChronoUnit.DAYS.between(startDate, endDate); + + return new DateDiff(calendarDays, Math.abs(calendarDays)); } - private Optional loadCalendar(int year) { - String path = "holidays/cn/" + year + ".json"; - try (InputStream input = - Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) { - if (input == null) { + private Optional queryHolidayApi(LocalDate date) { + HttpRequest request = + HttpRequest.newBuilder(holidayInfoUri(date)) + .timeout(Duration.ofSeconds(5)) + .GET() + .build(); + try { + HttpResponse response = + httpClient.send( + request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + if (response.statusCode() != 200) { return Optional.empty(); } - return Optional.of(objectMapper.readValue(input, HOLIDAY_CALENDAR_TYPE)); + + HolidayApiResponse body = + objectMapper.readValue(response.body(), HolidayApiResponse.class); + return body.code() == 0 ? Optional.of(body) : Optional.empty(); } catch (IOException e) { - throw new IllegalStateException("failed to load holiday data: " + path, e); + return Optional.empty(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return Optional.empty(); } } + private URI holidayInfoUri(LocalDate date) { + return URI.create(HOLIDAY_API_BASE_URL + "/" + date); + } + private String dayType(boolean legalHoliday, boolean adjustedWorkday, boolean weekend) { if (legalHoliday) { return "PUBLIC_HOLIDAY"; @@ -149,32 +174,22 @@ private String dayType(boolean legalHoliday, boolean adjustedWorkday, boolean we return weekend ? "WEEKEND" : "WORKDAY"; } - private Map> events(HolidayCalendar calendar) { - return calendar.events() != null ? calendar.events() : Map.of(); - } - record DateInfo( String date, - String timezone, String weekday, - int weekdayIso, - boolean isWeekend, - boolean isDayOff, - boolean isLegalHoliday, - boolean isAdjustedWorkday, String holidayName, String dayType, - List importantDays, - boolean holidayDataAvailable) {} + boolean holidayDataAvailable, + DateDiff dateDiff) {} + + record DateDiff(long calendarDays, long absoluteCalendarDays) {} - record HolidayCalendar( - int year, - String source, - String sourceUrl, - Map days, - Map> events) {} + @JsonIgnoreProperties(ignoreUnknown = true) + record HolidayApiResponse(int code, HolidayType type, HolidayDetail holiday) {} - record HolidayInfo(String type, String name) {} + @JsonIgnoreProperties(ignoreUnknown = true) + record HolidayType(Integer type, String name, Integer week) {} - record ImportantDay(String type, String name, String time, String note) {} + @JsonIgnoreProperties(ignoreUnknown = true) + record HolidayDetail(Boolean holiday, String name, String target, String date) {} } diff --git a/data-agent-backend/src/main/resources/holidays/cn/2026.json b/data-agent-backend/src/main/resources/holidays/cn/2026.json deleted file mode 100644 index 0482bee..0000000 --- a/data-agent-backend/src/main/resources/holidays/cn/2026.json +++ /dev/null @@ -1,198 +0,0 @@ -{ - "year": 2026, - "source": "国务院办公厅关于2026年部分节假日安排的通知", - "sourceUrl": "https://www.gov.cn/gongbao/2025/issue_12406/202511/content_7048922.html", - "days": { - "2026-01-01": {"type": "PUBLIC_HOLIDAY", "name": "元旦"}, - "2026-01-02": {"type": "PUBLIC_HOLIDAY", "name": "元旦"}, - "2026-01-03": {"type": "PUBLIC_HOLIDAY", "name": "元旦"}, - "2026-01-04": {"type": "ADJUSTED_WORKDAY", "name": "元旦调休上班"}, - "2026-02-14": {"type": "ADJUSTED_WORKDAY", "name": "春节调休上班"}, - "2026-02-15": {"type": "PUBLIC_HOLIDAY", "name": "春节"}, - "2026-02-16": {"type": "PUBLIC_HOLIDAY", "name": "春节"}, - "2026-02-17": {"type": "PUBLIC_HOLIDAY", "name": "春节"}, - "2026-02-18": {"type": "PUBLIC_HOLIDAY", "name": "春节"}, - "2026-02-19": {"type": "PUBLIC_HOLIDAY", "name": "春节"}, - "2026-02-20": {"type": "PUBLIC_HOLIDAY", "name": "春节"}, - "2026-02-21": {"type": "PUBLIC_HOLIDAY", "name": "春节"}, - "2026-02-22": {"type": "PUBLIC_HOLIDAY", "name": "春节"}, - "2026-02-23": {"type": "PUBLIC_HOLIDAY", "name": "春节"}, - "2026-02-28": {"type": "ADJUSTED_WORKDAY", "name": "春节调休上班"}, - "2026-04-04": {"type": "PUBLIC_HOLIDAY", "name": "清明节"}, - "2026-04-05": {"type": "PUBLIC_HOLIDAY", "name": "清明节"}, - "2026-04-06": {"type": "PUBLIC_HOLIDAY", "name": "清明节"}, - "2026-05-01": {"type": "PUBLIC_HOLIDAY", "name": "劳动节"}, - "2026-05-02": {"type": "PUBLIC_HOLIDAY", "name": "劳动节"}, - "2026-05-03": {"type": "PUBLIC_HOLIDAY", "name": "劳动节"}, - "2026-05-04": {"type": "PUBLIC_HOLIDAY", "name": "劳动节"}, - "2026-05-05": {"type": "PUBLIC_HOLIDAY", "name": "劳动节"}, - "2026-05-09": {"type": "ADJUSTED_WORKDAY", "name": "劳动节调休上班"}, - "2026-06-19": {"type": "PUBLIC_HOLIDAY", "name": "端午节"}, - "2026-06-20": {"type": "PUBLIC_HOLIDAY", "name": "端午节"}, - "2026-06-21": {"type": "PUBLIC_HOLIDAY", "name": "端午节"}, - "2026-09-20": {"type": "ADJUSTED_WORKDAY", "name": "国庆节调休上班"}, - "2026-09-25": {"type": "PUBLIC_HOLIDAY", "name": "中秋节"}, - "2026-09-26": {"type": "PUBLIC_HOLIDAY", "name": "中秋节"}, - "2026-09-27": {"type": "PUBLIC_HOLIDAY", "name": "中秋节"}, - "2026-10-01": {"type": "PUBLIC_HOLIDAY", "name": "国庆节"}, - "2026-10-02": {"type": "PUBLIC_HOLIDAY", "name": "国庆节"}, - "2026-10-03": {"type": "PUBLIC_HOLIDAY", "name": "国庆节"}, - "2026-10-04": {"type": "PUBLIC_HOLIDAY", "name": "国庆节"}, - "2026-10-05": {"type": "PUBLIC_HOLIDAY", "name": "国庆节"}, - "2026-10-06": {"type": "PUBLIC_HOLIDAY", "name": "国庆节"}, - "2026-10-07": {"type": "PUBLIC_HOLIDAY", "name": "国庆节"}, - "2026-10-10": {"type": "ADJUSTED_WORKDAY", "name": "国庆节调休上班"} - }, - "events": { - "2026-01-01": [ - {"type": "PUBLIC_FESTIVAL", "name": "元旦", "time": null, "note": "公历新年"} - ], - "2026-01-05": [ - {"type": "SOLAR_TERM", "name": "小寒", "time": "16:23", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-01-20": [ - {"type": "SOLAR_TERM", "name": "大寒", "time": "09:45", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-01-26": [ - {"type": "TRADITIONAL_FESTIVAL", "name": "腊八节", "time": null, "note": "农历乙巳年腊月初八"} - ], - "2026-02-04": [ - {"type": "SOLAR_TERM", "name": "立春", "time": "04:02", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-02-10": [ - {"type": "TRADITIONAL_FESTIVAL", "name": "小年", "time": null, "note": "北方,农历乙巳年腊月廿三"} - ], - "2026-02-11": [ - {"type": "TRADITIONAL_FESTIVAL", "name": "小年", "time": null, "note": "南方,农历乙巳年腊月廿四"} - ], - "2026-02-14": [ - {"type": "WESTERN_FESTIVAL", "name": "情人节", "time": null, "note": null} - ], - "2026-02-16": [ - {"type": "TRADITIONAL_FESTIVAL", "name": "除夕", "time": null, "note": "农历乙巳年腊月廿九"} - ], - "2026-02-17": [ - {"type": "TRADITIONAL_FESTIVAL", "name": "春节", "time": null, "note": "农历丙午年正月初一"} - ], - "2026-02-18": [ - {"type": "SOLAR_TERM", "name": "雨水", "time": "23:52", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-03-03": [ - {"type": "TRADITIONAL_FESTIVAL", "name": "元宵节", "time": null, "note": "农历丙午年正月十五"} - ], - "2026-03-05": [ - {"type": "SOLAR_TERM", "name": "惊蛰", "time": "21:59", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-03-08": [ - {"type": "MEMORIAL_DAY", "name": "国际妇女节", "time": null, "note": null} - ], - "2026-03-12": [ - {"type": "MEMORIAL_DAY", "name": "植树节", "time": null, "note": null} - ], - "2026-03-20": [ - {"type": "SOLAR_TERM", "name": "春分", "time": "22:46", "note": "二十四节气,香港时间 UTC+8"}, - {"type": "TRADITIONAL_FESTIVAL", "name": "龙抬头", "time": null, "note": "农历丙午年二月初二"} - ], - "2026-04-05": [ - {"type": "SOLAR_TERM", "name": "清明", "time": "02:40", "note": "二十四节气,香港时间 UTC+8"}, - {"type": "TRADITIONAL_FESTIVAL", "name": "清明节", "time": null, "note": null} - ], - "2026-04-20": [ - {"type": "SOLAR_TERM", "name": "谷雨", "time": "09:39", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-05-01": [ - {"type": "PUBLIC_FESTIVAL", "name": "劳动节", "time": null, "note": "国际劳动节"} - ], - "2026-05-04": [ - {"type": "MEMORIAL_DAY", "name": "青年节", "time": null, "note": null} - ], - "2026-05-05": [ - {"type": "SOLAR_TERM", "name": "立夏", "time": "19:49", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-05-10": [ - {"type": "WESTERN_FESTIVAL", "name": "母亲节", "time": null, "note": "5月第二个星期日"} - ], - "2026-05-21": [ - {"type": "SOLAR_TERM", "name": "小满", "time": "08:37", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-06-01": [ - {"type": "MEMORIAL_DAY", "name": "儿童节", "time": null, "note": null} - ], - "2026-06-05": [ - {"type": "SOLAR_TERM", "name": "芒种", "time": "23:48", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-06-19": [ - {"type": "TRADITIONAL_FESTIVAL", "name": "端午节", "time": null, "note": "农历丙午年五月初五"} - ], - "2026-06-21": [ - {"type": "SOLAR_TERM", "name": "夏至", "time": "16:25", "note": "二十四节气,香港时间 UTC+8"}, - {"type": "WESTERN_FESTIVAL", "name": "父亲节", "time": null, "note": "6月第三个星期日"} - ], - "2026-07-01": [ - {"type": "MEMORIAL_DAY", "name": "建党节", "time": null, "note": null} - ], - "2026-07-07": [ - {"type": "SOLAR_TERM", "name": "小暑", "time": "09:57", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-07-23": [ - {"type": "SOLAR_TERM", "name": "大暑", "time": "03:13", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-08-01": [ - {"type": "MEMORIAL_DAY", "name": "建军节", "time": null, "note": null} - ], - "2026-08-07": [ - {"type": "SOLAR_TERM", "name": "立秋", "time": "19:43", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-08-19": [ - {"type": "TRADITIONAL_FESTIVAL", "name": "七夕节", "time": null, "note": "农历丙午年七月初七"} - ], - "2026-08-23": [ - {"type": "SOLAR_TERM", "name": "处暑", "time": "10:19", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-08-27": [ - {"type": "TRADITIONAL_FESTIVAL", "name": "中元节", "time": null, "note": "农历丙午年七月十五"} - ], - "2026-09-07": [ - {"type": "SOLAR_TERM", "name": "白露", "time": "22:41", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-09-10": [ - {"type": "MEMORIAL_DAY", "name": "教师节", "time": null, "note": null} - ], - "2026-09-23": [ - {"type": "SOLAR_TERM", "name": "秋分", "time": "08:05", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-09-25": [ - {"type": "TRADITIONAL_FESTIVAL", "name": "中秋节", "time": null, "note": "农历丙午年八月十五"} - ], - "2026-10-01": [ - {"type": "PUBLIC_FESTIVAL", "name": "国庆节", "time": null, "note": "中华人民共和国国庆节"} - ], - "2026-10-08": [ - {"type": "SOLAR_TERM", "name": "寒露", "time": "14:29", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-10-18": [ - {"type": "TRADITIONAL_FESTIVAL", "name": "重阳节", "time": null, "note": "农历丙午年九月初九"} - ], - "2026-10-23": [ - {"type": "SOLAR_TERM", "name": "霜降", "time": "17:38", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-11-07": [ - {"type": "SOLAR_TERM", "name": "立冬", "time": "17:52", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-11-22": [ - {"type": "SOLAR_TERM", "name": "小雪", "time": "15:23", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-12-07": [ - {"type": "SOLAR_TERM", "name": "大雪", "time": "10:53", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-12-22": [ - {"type": "SOLAR_TERM", "name": "冬至", "time": "04:50", "note": "二十四节气,香港时间 UTC+8"} - ], - "2026-12-24": [ - {"type": "WESTERN_FESTIVAL", "name": "平安夜", "time": null, "note": null} - ], - "2026-12-25": [ - {"type": "WESTERN_FESTIVAL", "name": "圣诞节", "time": null, "note": null} - ] - } -}