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 aebdb89..e351bef 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 @@ -43,6 +43,8 @@ import io.github.malonetalk.service.DatasourceService; import io.github.malonetalk.web.TraceIdFilter; import jakarta.annotation.PostConstruct; +import java.time.LocalDate; +import java.time.ZoneId; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -56,6 +58,8 @@ @RequiredArgsConstructor public class AgentService { + private static final ZoneId DEFAULT_ZONE = ZoneId.of("Asia/Shanghai"); + private final ModelFactory modelFactory; private final List allToolBeans; private final ModelProperties modelProperties; @@ -165,7 +169,7 @@ private ReActAgent createAgent(ToolCallContext toolCallContext) { ToolExecutionContext.builder().register(toolCallContext).build(); return ReActAgent.builder() .name("DataAgent") - .sysPrompt("你是一个数据助手,可以帮助用户查询数据库中的数据。") + .sysPrompt(systemPrompt()) .model(modelFactory.getInstance(modelProperties)) .toolkit(toolkit) .toolExecutionContext(context) @@ -175,4 +179,16 @@ private ReActAgent createAgent(ToolCallContext toolCallContext) { .enablePendingToolRecovery(true) .build(); } + + private String systemPrompt() { + LocalDate today = LocalDate.now(DEFAULT_ZONE); + return """ + 你是一个数据助手,可以帮助用户查询数据库中的数据。 + 当前时区是 Asia/Shanghai。 + 今天是 %s,昨天是 %s,明天是 %s。 + 当用户提到今天、昨天、明天、上周、本周等相对日期时,必须先按以上当前日期换算成具体日期。 + 查询日期、星期、节假日、节气或重要日子时,优先使用 get_date_info 工具。 + """ + .formatted(today, today.minusDays(1), today.plusDays(1)); + } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/models/DashscopeModelProvider.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/models/DashscopeModelProvider.java index 44b6287..beeaeae 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/models/DashscopeModelProvider.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/models/DashscopeModelProvider.java @@ -31,11 +31,14 @@ public String provider() { @Override public Model createModel(ModelConfig config) { - return DashScopeChatModel.builder() - .apiKey(config.getApiKey()) - .modelName(config.getName()) - .stream(true) - .enableThinking(true) - .build(); + DashScopeChatModel.Builder builder = + DashScopeChatModel.builder() + .apiKey(config.getApiKey()) + .modelName(config.getName()) + .stream(true); + if (Boolean.TRUE.equals(config.getThinkingEnabled())) { + builder.enableThinking(true); + } + return builder.build(); } } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/models/ModelConfig.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/models/ModelConfig.java index c84d7b5..1f0cb85 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/models/ModelConfig.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/models/ModelConfig.java @@ -29,4 +29,6 @@ public class ModelConfig { private String baseUrl; private String apiKey; + + private Boolean thinkingEnabled; } 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 new file mode 100644 index 0000000..6dd6b68 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/DateInfoTool.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.agent.tools; + +import com.fasterxml.jackson.core.type.TypeReference; +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.time.Clock; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.TextStyle; +import java.util.List; +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; + +@Component +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 final ObjectMapper objectMapper; + private final Clock clock; + private final Map> calendars = new ConcurrentHashMap<>(); + + @Autowired + public DateInfoTool(ObjectMapper objectMapper) { + this(objectMapper, Clock.systemDefaultZone()); + } + + DateInfoTool(ObjectMapper objectMapper, Clock clock) { + this.objectMapper = objectMapper; + this.clock = clock; + } + + @Tool( + name = "get_date_info", + 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.\ + """) + public String getDateInfo( + @ToolParam( + name = "date", + description = "Date in yyyy-MM-dd format. Defaults to today.", + required = false) + String date, + @ToolParam( + name = "timezone", + description = + "IANA timezone, e.g. Asia/Shanghai. Defaults to Asia/Shanghai.", + required = false) + String timezone) { + try { + return objectMapper.writeValueAsString(resolve(date, timezone)); + } catch (Exception e) { + return "Error: failed to get date info: " + e.getMessage(); + } + } + + DateInfo resolve(String dateText, 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()); + 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, + dayType(legalHoliday, adjustedWorkday, weekend), + importantDays, + calendar.isPresent()); + } + + private Optional calendar(int year) { + return calendars.computeIfAbsent(year, this::loadCalendar); + } + + private Optional loadCalendar(int year) { + String path = "holidays/cn/" + year + ".json"; + try (InputStream input = + Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) { + if (input == null) { + return Optional.empty(); + } + return Optional.of(objectMapper.readValue(input, HOLIDAY_CALENDAR_TYPE)); + } catch (IOException e) { + throw new IllegalStateException("failed to load holiday data: " + path, e); + } + } + + private String dayType(boolean legalHoliday, boolean adjustedWorkday, boolean weekend) { + if (legalHoliday) { + return "PUBLIC_HOLIDAY"; + } + if (adjustedWorkday) { + return "ADJUSTED_WORKDAY"; + } + 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) {} + + record HolidayCalendar( + int year, + String source, + String sourceUrl, + Map days, + Map> events) {} + + record HolidayInfo(String type, String name) {} + + record ImportantDay(String type, String name, String time, String note) {} +} diff --git a/data-agent-backend/src/main/resources/application.properties b/data-agent-backend/src/main/resources/application.properties index 084e531..1c3a1ba 100644 --- a/data-agent-backend/src/main/resources/application.properties +++ b/data-agent-backend/src/main/resources/application.properties @@ -25,6 +25,7 @@ io.github.malonetalk.model.provider=dashscope io.github.malonetalk.model.name=qwen3-max io.github.malonetalk.model.base-url= io.github.malonetalk.model.api-key=${IO_GITHUB_MALONETALK_MODEL_API_KEY:} +io.github.malonetalk.model.thinking-enabled=${IO_GITHUB_MALONETALK_MODEL_THINKING_ENABLED:false} spring.config.import=classpath:skill.properties diff --git a/data-agent-backend/src/main/resources/holidays/cn/2026.json b/data-agent-backend/src/main/resources/holidays/cn/2026.json new file mode 100644 index 0000000..0482bee --- /dev/null +++ b/data-agent-backend/src/main/resources/holidays/cn/2026.json @@ -0,0 +1,198 @@ +{ + "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} + ] + } +}