Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<MarkAgentTool> allToolBeans;
private final ModelProperties modelProperties;
Expand Down Expand Up @@ -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)
Expand All @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ public class ModelConfig {
private String baseUrl;

private String apiKey;

private Boolean thinkingEnabled;
}
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
* 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<HolidayCalendar> HOLIDAY_CALENDAR_TYPE =
new TypeReference<>() {};

private final ObjectMapper objectMapper;
private final Clock clock;
private final Map<Integer, Optional<HolidayCalendar>> 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<HolidayCalendar> calendar = calendar(date.getYear());
HolidayInfo holidayInfo = calendar.map(c -> c.days().get(date.toString())).orElse(null);
List<ImportantDay> 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<HolidayCalendar> calendar(int year) {
return calendars.computeIfAbsent(year, this::loadCalendar);
}

private Optional<HolidayCalendar> 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<String, List<ImportantDay>> 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<ImportantDay> importantDays,
boolean holidayDataAvailable) {}

record HolidayCalendar(
int year,
String source,
String sourceUrl,
Map<String, HolidayInfo> days,
Map<String, List<ImportantDay>> events) {}

record HolidayInfo(String type, String name) {}

record ImportantDay(String type, String name, String time, String note) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading