日期
This commit is contained in:
parent
0d813d73f0
commit
577d2b51d7
@ -16,8 +16,22 @@ import java.util.regex.Pattern;
|
|||||||
public class SemanticDateRangeResolver {
|
public class SemanticDateRangeResolver {
|
||||||
private static final Pattern EXPLICIT_DATE = Pattern.compile(
|
private static final Pattern EXPLICIT_DATE = Pattern.compile(
|
||||||
"(?<!\\d)(\\d{4})[-/.年](\\d{1,2})[-/.月](\\d{1,2})日?");
|
"(?<!\\d)(\\d{4})[-/.年](\\d{1,2})[-/.月](\\d{1,2})日?");
|
||||||
|
private static final Pattern EXPLICIT_YEAR_MONTH = Pattern.compile(
|
||||||
|
"(?<!\\d)(\\d{4})[-/.年](\\d{1,2})月?");
|
||||||
|
private static final Pattern ARABIC_MONTH = Pattern.compile("(?<!\\d)(1[0-2]|0?[1-9])月");
|
||||||
|
private static final Pattern CHINESE_MONTH = Pattern.compile("(十二|十一|十|九|八|七|六|五|四|三|二|一)月");
|
||||||
|
|
||||||
|
public DateRange resolve(SemanticRequestContext context, SemanticRecognition recognition) {
|
||||||
|
return resolve(context.userText(), recognition.parameters(), context.requestTime().toLocalDate());
|
||||||
|
}
|
||||||
|
|
||||||
public DateRange resolve(String userText, Map<String, String> parameters, LocalDate today) {
|
public DateRange resolve(String userText, Map<String, String> parameters, LocalDate today) {
|
||||||
|
String directText = userText == null ? "" : userText;
|
||||||
|
DateRange textRange = resolveUserText(directText, today);
|
||||||
|
if (textRange != null) {
|
||||||
|
return textRange;
|
||||||
|
}
|
||||||
|
|
||||||
LocalDate parameterStart = parse(parameters.get("startDate"));
|
LocalDate parameterStart = parse(parameters.get("startDate"));
|
||||||
LocalDate parameterEnd = parse(parameters.get("endDate"));
|
LocalDate parameterEnd = parse(parameters.get("endDate"));
|
||||||
if (parameterStart != null || parameterEnd != null) {
|
if (parameterStart != null || parameterEnd != null) {
|
||||||
@ -26,33 +40,83 @@ public class SemanticDateRangeResolver {
|
|||||||
return ordered(start, end);
|
return ordered(start, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<LocalDate> explicitDates = explicitDates(userText);
|
|
||||||
if (!explicitDates.isEmpty()) {
|
|
||||||
return ordered(explicitDates.get(0), explicitDates.size() > 1 ? explicitDates.get(1) : explicitDates.get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
String preset = parameters.getOrDefault("datePreset", "");
|
String preset = parameters.getOrDefault("datePreset", "");
|
||||||
String text = (userText == null ? "" : userText) + " " + preset;
|
if (containsAny(preset, "YESTERDAY")) {
|
||||||
if (containsAny(text, "昨日", "昨天", "YESTERDAY")) {
|
|
||||||
return new DateRange(today.minusDays(1), today.minusDays(1));
|
return new DateRange(today.minusDays(1), today.minusDays(1));
|
||||||
}
|
}
|
||||||
if (containsAny(text, "上周", "LAST_WEEK")) {
|
if (containsAny(preset, "LAST_WEEK")) {
|
||||||
LocalDate start = today.minusWeeks(1).with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
|
LocalDate start = today.minusWeeks(1).with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
|
||||||
return new DateRange(start, start.plusDays(6));
|
return new DateRange(start, start.plusDays(6));
|
||||||
}
|
}
|
||||||
if (containsAny(text, "本周", "这周", "THIS_WEEK")) {
|
if (containsAny(preset, "THIS_WEEK")) {
|
||||||
return new DateRange(today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)), today);
|
return new DateRange(today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)), today);
|
||||||
}
|
}
|
||||||
if (containsAny(text, "上月", "上个月", "LAST_MONTH")) {
|
if (containsAny(preset, "LAST_MONTH")) {
|
||||||
YearMonth month = YearMonth.from(today).minusMonths(1);
|
YearMonth month = YearMonth.from(today).minusMonths(1);
|
||||||
return new DateRange(month.atDay(1), month.atEndOfMonth());
|
return new DateRange(month.atDay(1), month.atEndOfMonth());
|
||||||
}
|
}
|
||||||
if (containsAny(text, "本月", "这个月", "THIS_MONTH")) {
|
if (containsAny(preset, "THIS_MONTH")) {
|
||||||
return new DateRange(today.withDayOfMonth(1), today);
|
return new DateRange(today.withDayOfMonth(1), today);
|
||||||
}
|
}
|
||||||
|
if (containsAny(preset, "LAST_YEAR")) {
|
||||||
|
int year = today.getYear() - 1;
|
||||||
|
return new DateRange(LocalDate.of(year, 1, 1), LocalDate.of(year, 12, 31));
|
||||||
|
}
|
||||||
|
if (containsAny(preset, "THIS_YEAR")) {
|
||||||
|
return new DateRange(today.withDayOfYear(1), today);
|
||||||
|
}
|
||||||
return new DateRange(today, today);
|
return new DateRange(today, today);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean exceedsMaximumDays(String userText, DateRange range, int maximumDays) {
|
||||||
|
if (maximumDays <= 0) {
|
||||||
|
throw new IllegalArgumentException("maximumDays must be positive");
|
||||||
|
}
|
||||||
|
boolean wholeYearRequest = containsAny(userText == null ? "" : userText, "本年", "今年", "全年", "去年", "上年")
|
||||||
|
&& !(userText != null && userText.contains("月"));
|
||||||
|
return wholeYearRequest && maximumDays < 365
|
||||||
|
|| range.startDate().plusDays(maximumDays - 1L).isBefore(range.endDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateRange resolveUserText(String text, LocalDate today) {
|
||||||
|
List<LocalDate> dates = explicitDates(text);
|
||||||
|
if (!dates.isEmpty()) {
|
||||||
|
return ordered(dates.get(0), dates.size() > 1 ? dates.get(1) : dates.get(0));
|
||||||
|
}
|
||||||
|
YearMonth month = requestedMonth(text, today);
|
||||||
|
if (month != null) {
|
||||||
|
return new DateRange(month.atDay(1), month.atEndOfMonth());
|
||||||
|
}
|
||||||
|
if (containsAny(text, "昨日", "昨天")) {
|
||||||
|
return new DateRange(today.minusDays(1), today.minusDays(1));
|
||||||
|
}
|
||||||
|
if (containsAny(text, "上周")) {
|
||||||
|
LocalDate start = today.minusWeeks(1).with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
|
||||||
|
return new DateRange(start, start.plusDays(6));
|
||||||
|
}
|
||||||
|
if (containsAny(text, "本周", "这周")) {
|
||||||
|
return new DateRange(today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)), today);
|
||||||
|
}
|
||||||
|
if (containsAny(text, "上月", "上个月")) {
|
||||||
|
YearMonth lastMonth = YearMonth.from(today).minusMonths(1);
|
||||||
|
return new DateRange(lastMonth.atDay(1), lastMonth.atEndOfMonth());
|
||||||
|
}
|
||||||
|
if (containsAny(text, "本月", "这个月")) {
|
||||||
|
return new DateRange(today.withDayOfMonth(1), today);
|
||||||
|
}
|
||||||
|
if (containsAny(text, "去年", "上年")) {
|
||||||
|
int lastYear = today.getYear() - 1;
|
||||||
|
return new DateRange(LocalDate.of(lastYear, 1, 1), LocalDate.of(lastYear, 12, 31));
|
||||||
|
}
|
||||||
|
if (containsAny(text, "本年", "今年", "全年")) {
|
||||||
|
return new DateRange(today.withDayOfYear(1), today);
|
||||||
|
}
|
||||||
|
if (containsAny(text, "今日", "今天")) {
|
||||||
|
return new DateRange(today, today);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private List<LocalDate> explicitDates(String text) {
|
private List<LocalDate> explicitDates(String text) {
|
||||||
List<LocalDate> dates = new ArrayList<>();
|
List<LocalDate> dates = new ArrayList<>();
|
||||||
Matcher matcher = EXPLICIT_DATE.matcher(text == null ? "" : text);
|
Matcher matcher = EXPLICIT_DATE.matcher(text == null ? "" : text);
|
||||||
@ -77,6 +141,46 @@ public class SemanticDateRangeResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private YearMonth requestedMonth(String text, LocalDate today) {
|
||||||
|
String safeText = text == null ? "" : text;
|
||||||
|
Matcher yearMonthMatcher = EXPLICIT_YEAR_MONTH.matcher(safeText);
|
||||||
|
if (yearMonthMatcher.find()) {
|
||||||
|
try {
|
||||||
|
return YearMonth.of(Integer.parseInt(yearMonthMatcher.group(1)),
|
||||||
|
Integer.parseInt(yearMonthMatcher.group(2)));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Matcher arabicMatcher = ARABIC_MONTH.matcher(safeText);
|
||||||
|
if (arabicMatcher.find()) {
|
||||||
|
return YearMonth.of(today.getYear(), Integer.parseInt(arabicMatcher.group(1)));
|
||||||
|
}
|
||||||
|
Matcher chineseMatcher = CHINESE_MONTH.matcher(safeText);
|
||||||
|
if (chineseMatcher.find()) {
|
||||||
|
return YearMonth.of(today.getYear(), chineseMonth(chineseMatcher.group(1)));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int chineseMonth(String value) {
|
||||||
|
return switch (value) {
|
||||||
|
case "一" -> 1;
|
||||||
|
case "二" -> 2;
|
||||||
|
case "三" -> 3;
|
||||||
|
case "四" -> 4;
|
||||||
|
case "五" -> 5;
|
||||||
|
case "六" -> 6;
|
||||||
|
case "七" -> 7;
|
||||||
|
case "八" -> 8;
|
||||||
|
case "九" -> 9;
|
||||||
|
case "十" -> 10;
|
||||||
|
case "十一" -> 11;
|
||||||
|
case "十二" -> 12;
|
||||||
|
default -> throw new IllegalArgumentException("invalid Chinese month: " + value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private DateRange ordered(LocalDate first, LocalDate second) {
|
private DateRange ordered(LocalDate first, LocalDate second) {
|
||||||
return first.isAfter(second) ? new DateRange(second, first) : new DateRange(first, second);
|
return first.isAfter(second) ? new DateRange(second, first) : new DateRange(first, second);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -108,7 +108,7 @@ public class SemanticRecognitionService {
|
|||||||
%s
|
%s
|
||||||
|
|
||||||
只输出一个 JSON 对象,禁止 Markdown、解释或额外文字:
|
只输出一个 JSON 对象,禁止 Markdown、解释或额外文字:
|
||||||
{"intent":"意图编码或UNKNOWN","confidence":0到1,"parameters":{"startDate":"yyyy-MM-dd","endDate":"yyyy-MM-dd","datePreset":"TODAY|YESTERDAY|THIS_WEEK|LAST_WEEK|THIS_MONTH|LAST_MONTH"}}
|
{"intent":"意图编码或UNKNOWN","confidence":0到1,"parameters":{"startDate":"yyyy-MM-dd","endDate":"yyyy-MM-dd","datePreset":"TODAY|YESTERDAY|THIS_WEEK|LAST_WEEK|THIS_MONTH|LAST_MONTH|THIS_YEAR|LAST_YEAR"}}
|
||||||
|
|
||||||
规则:
|
规则:
|
||||||
1. 只允许选择上面列出的意图,否则返回 UNKNOWN。
|
1. 只允许选择上面列出的意图,否则返回 UNKNOWN。
|
||||||
|
|||||||
@ -51,8 +51,7 @@ public class CustomerDailyReportIntentHandler implements SemanticIntentHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String handle(SemanticRequestContext context, SemanticRecognition recognition) {
|
public String handle(SemanticRequestContext context, SemanticRecognition recognition) {
|
||||||
SemanticDateRangeResolver.DateRange range = dateRangeResolver.resolve(
|
SemanticDateRangeResolver.DateRange range = dateRangeResolver.resolve(context, recognition);
|
||||||
context.userText(), recognition.parameters(), context.requestTime().toLocalDate());
|
|
||||||
try {
|
try {
|
||||||
JsonNode report = objectMapper.valueToTree(reportService.generate(
|
JsonNode report = objectMapper.valueToTree(reportService.generate(
|
||||||
new CustomerDailyReportRequest(range.startDate(), range.endDate(), context.wecomUserId())));
|
new CustomerDailyReportRequest(range.startDate(), range.endDate(), context.wecomUserId())));
|
||||||
|
|||||||
@ -48,8 +48,10 @@ public class WecomCustomerOperationsReportIntentHandler implements SemanticInten
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String handle(SemanticRequestContext context, SemanticRecognition recognition) {
|
public String handle(SemanticRequestContext context, SemanticRecognition recognition) {
|
||||||
SemanticDateRangeResolver.DateRange range = dateRangeResolver.resolve(
|
SemanticDateRangeResolver.DateRange range = dateRangeResolver.resolve(context, recognition);
|
||||||
context.userText(), recognition.parameters(), context.requestTime().toLocalDate());
|
if (dateRangeResolver.exceedsMaximumDays(context.userText(), range, 31)) {
|
||||||
|
return "企业微信客户运营日报一次只能查询一个月的数据。可输入:一月企业微信客户运营日报。";
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
JsonNode report = objectMapper.valueToTree(reportService.generate(
|
JsonNode report = objectMapper.valueToTree(reportService.generate(
|
||||||
new WecomCustomerOperationsReportRequest(range.startDate(), range.endDate(), context.wecomUserId())));
|
new WecomCustomerOperationsReportRequest(range.startDate(), range.endDate(), context.wecomUserId())));
|
||||||
|
|||||||
@ -25,6 +25,14 @@ class SemanticDateRangeResolverTest {
|
|||||||
resolver.resolve("查看本周客资日报", Map.of(), today));
|
resolver.resolve("查看本周客资日报", Map.of(), today));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolvesThisMonthFromUserTextBeforeModelDates() {
|
||||||
|
assertEquals(
|
||||||
|
new SemanticDateRangeResolver.DateRange(LocalDate.of(2026, 7, 1), today),
|
||||||
|
resolver.resolve("本月企业微信客户运营日报",
|
||||||
|
Map.of("startDate", "2025-06-01", "endDate", "2025-06-30"), today));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolvesExplicitRange() {
|
void resolvesExplicitRange() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
@ -38,4 +46,33 @@ class SemanticDateRangeResolverTest {
|
|||||||
new SemanticDateRangeResolver.DateRange(LocalDate.of(2026, 7, 1), LocalDate.of(2026, 7, 15)),
|
new SemanticDateRangeResolver.DateRange(LocalDate.of(2026, 7, 1), LocalDate.of(2026, 7, 15)),
|
||||||
resolver.resolve("查看客资日报", Map.of("startDate", "2026-07-01", "endDate", "2026-07-15"), today));
|
resolver.resolve("查看客资日报", Map.of("startDate", "2026-07-01", "endDate", "2026-07-15"), today));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolvesChineseNamedMonth() {
|
||||||
|
assertEquals(
|
||||||
|
new SemanticDateRangeResolver.DateRange(LocalDate.of(2026, 1, 1), LocalDate.of(2026, 1, 31)),
|
||||||
|
resolver.resolve("一月企业微信客户运营日报", Map.of(), today));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolvesExplicitYearMonth() {
|
||||||
|
assertEquals(
|
||||||
|
new SemanticDateRangeResolver.DateRange(LocalDate.of(2025, 12, 1), LocalDate.of(2025, 12, 31)),
|
||||||
|
resolver.resolve("查看2025年12月企业微信客户运营日报", Map.of(), today));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolvesThisYearForAllReportHandlers() {
|
||||||
|
assertEquals(
|
||||||
|
new SemanticDateRangeResolver.DateRange(LocalDate.of(2026, 1, 1), today),
|
||||||
|
resolver.resolve("本年运营日报", Map.of(), today));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void appliesReusableMaximumRangePolicy() {
|
||||||
|
SemanticDateRangeResolver.DateRange yearRange = resolver.resolve("本年运营日报", Map.of(), today);
|
||||||
|
assertEquals(true, resolver.exceedsMaximumDays("本年运营日报", yearRange, 31));
|
||||||
|
assertEquals(false, resolver.exceedsMaximumDays("本月运营日报",
|
||||||
|
resolver.resolve("本月运营日报", Map.of(), today), 31));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user