Commit 3cbc33a7 authored by 徐健's avatar 徐健

看板ai优化

parent 28bcb45b
Pipeline #25194 passed with stages
in 2 minutes and 35 seconds
...@@ -18,14 +18,13 @@ import reactor.core.publisher.Flux; ...@@ -18,14 +18,13 @@ import reactor.core.publisher.Flux;
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/api/workbench/agent") @RequestMapping("/api/workbench/agent")
public class WorkbenchAgentChatController { public class WorkbenchAgentChatController {
private final CareAgentService careAgentService;
private final WorkbenchAIService workbenchAIService; private final WorkbenchAIService workbenchAIService;
/** /**
* 推荐场景的标准 GET 方式的流式聊天入口。 * 推荐场景的标准 GET 方式的流式聊天入口。
*/ */
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamCareAgent(@RequestParam("sessionId") String sessionId, public Flux<ServerSentEvent<String>> streamAgent(@RequestParam("sessionId") String sessionId,
@RequestParam("message") String message) { @RequestParam("message") String message) {
return streamWorkbenchAgentInternal(sessionId, message); return streamWorkbenchAgentInternal(sessionId, message);
} }
......
...@@ -21,17 +21,20 @@ public class WorkbenchAgentConfiguration { ...@@ -21,17 +21,20 @@ public class WorkbenchAgentConfiguration {
"workbenchAgent", "workbenchAgent",
chatModel, chatModel,
""" """
你是员工看板智能体 你是员工看板智能体,专注于基于业务数据提供准确、简洁的查询应答服务
当用户进行通用对话时,保持员工看板智能体身份,自然回复用户。 核心数据结构说明:
1. 头表为部门(department),每个部门对应12个月的执行进度数据,存储于progress中;
2. progress为月度进度汇总数据,record为对应月份的责任人填报原始数据;
3. 单月支持多人填报,系统自动将填报数据合并汇总至对应月份的progress中;
4. 数据支持层级结构,children字段为当前数据的子节点集合。
回答要求: 应答规则:
- 推荐类内容只能依据调用方提供的真实数据作答 - 严格依据调用方提供的真实数据作答,严禁编造、虚构数据;
- 不能编造不存在的数据 - 数据为空时,统一友好提示“查询的数据为空”,不得输出技术类描述;
- 必须使用中文回答 - 必须使用中文回复,语气自然、友好、简洁;
- 保持自然、友好、简洁 - 输出结果仅展示业务数据结论,不提及分页、接口、模型、后台处理等技术细节;
- 绝不能提到分页、页码、后台处理中、接口调用、候选集、模型或任何技术实现细节 - 结果不足时,仅输出实际匹配内容并自然说明。
- 当结果不足时,只输出实际合适的结果,并自然说明
""" """
); );
} }
......
package com.infoepoch.pms.agent.domain.itworkbench.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 查询条件类
* 用于封装年度KPI查询所需的各种条件参数
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class QueryDto {
private String workGroup;
private String managerName;
private String sessionId;
}
...@@ -14,19 +14,48 @@ public record WorkbenchConversationState( ...@@ -14,19 +14,48 @@ public record WorkbenchConversationState(
String sessionId, String sessionId,
WorkbenchIntent lastIntent, WorkbenchIntent lastIntent,
Map<String, Object> conditions, Map<String, Object> conditions,
LocalDateTime updatedAt LocalDateTime updatedAt,
Map<String, TypeData> typeDataMap
) { ) {
public WorkbenchConversationState { public WorkbenchConversationState {
conditions = sanitize(conditions); conditions = sanitize(conditions);
typeDataMap = typeDataMap == null ? new LinkedHashMap<>() : new LinkedHashMap<>(typeDataMap);
updatedAt = updatedAt == null ? LocalDateTime.now() : updatedAt; updatedAt = updatedAt == null ? LocalDateTime.now() : updatedAt;
} }
public WorkbenchConversationState(String sessionId) {
this(sessionId, null, Collections.emptyMap(), LocalDateTime.now(), new LinkedHashMap<>());
}
public static WorkbenchConversationState initial(String sessionId, WorkbenchIntent intent, Map<String, Object> conditions) { public static WorkbenchConversationState initial(String sessionId, WorkbenchIntent intent, Map<String, Object> conditions) {
return new WorkbenchConversationState(sessionId, intent, conditions, LocalDateTime.now()); return new WorkbenchConversationState(sessionId, intent, conditions, LocalDateTime.now(), new LinkedHashMap<>());
}
public static WorkbenchConversationState initial(String sessionId, WorkbenchIntent intent, Map<String, Object> conditions, String dataType, String dataJson) {
WorkbenchConversationState state = new WorkbenchConversationState(sessionId, intent, conditions, LocalDateTime.now(), new LinkedHashMap<>());
state.getTypeDataMap().put(dataType, new TypeData(dataType, intent, dataJson, Collections.emptyMap()));
return state;
} }
public WorkbenchConversationState update(WorkbenchIntent intent, Map<String, Object> conditions) { public WorkbenchConversationState update(WorkbenchIntent intent, Map<String, Object> conditions) {
return new WorkbenchConversationState(sessionId, intent, conditions, LocalDateTime.now()); return new WorkbenchConversationState(sessionId, intent, conditions, LocalDateTime.now(), typeDataMap);
}
public Map<String, TypeData> getTypeDataMap() {
return typeDataMap;
}
public void saveDataForType(String dataType, WorkbenchIntent intent, String dataJson, Map<String, Object> conditions) {
typeDataMap.put(dataType, new TypeData(dataType, intent, dataJson, sanitize(conditions)));
}
public boolean hasDataForType(String dataType) {
TypeData typeData = typeDataMap.get(dataType);
return typeData != null && typeData.hasData();
}
public TypeData getDataForType(String dataType) {
return typeDataMap.get(dataType);
} }
private static Map<String, Object> sanitize(Map<String, Object> conditions) { private static Map<String, Object> sanitize(Map<String, Object> conditions) {
...@@ -41,4 +70,22 @@ public record WorkbenchConversationState( ...@@ -41,4 +70,22 @@ public record WorkbenchConversationState(
}); });
return Map.copyOf(sanitized); return Map.copyOf(sanitized);
} }
/**
* 类型数据记录
*/
public record TypeData(
String dataType,
WorkbenchIntent intent,
String dataJson,
Map<String, Object> conditions
) {
public TypeData {
conditions = conditions == null ? Collections.emptyMap() : Map.copyOf(conditions);
}
public boolean hasData() {
return dataJson != null && !dataJson.isBlank();
}
}
} }
...@@ -164,4 +164,6 @@ public class DepartmentWork { ...@@ -164,4 +164,6 @@ public class DepartmentWork {
private List<DepartmentWork> children; private List<DepartmentWork> children;
private List<DepartmentWorkProgress> monthChildren;
} }
...@@ -6,6 +6,7 @@ import lombok.NoArgsConstructor; ...@@ -6,6 +6,7 @@ import lombok.NoArgsConstructor;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import java.util.List;
/** /**
* 部门工作进度 * 部门工作进度
...@@ -117,4 +118,6 @@ public class DepartmentWorkProgress { ...@@ -117,4 +118,6 @@ public class DepartmentWorkProgress {
private Boolean submitFlag; private Boolean submitFlag;
private Integer lastReFillFlag; private Integer lastReFillFlag;
public List<ManagerMonthRecord> managerMonthRecordList;
} }
package com.infoepoch.pms.agent.domain.itworkbench.model.department;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;
/**
* 责任人月工作记录表
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ManagerMonthRecord {
/**
* 主键
*/
private String id;
/**
* 月工作ID
*/
private String monthId;
/**
* 责任人ID
*/
private String managerId;
/**
* 责任人名称
*/
private String managerName;
/**
* 目标值
*/
private String target;
/**
* 实际值
*/
private String actual;
/**
* 遗留值
*/
private String remain;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
/**
* 完成标识
*/
private Boolean finishFlag;
/**
* 截至目前完成进度
*/
private BigDecimal currentProgress;
/**
* 下月工作计划
*/
private String nextMonthGoal;
}
\ No newline at end of file
...@@ -170,4 +170,6 @@ public class AnnualKpi { ...@@ -170,4 +170,6 @@ public class AnnualKpi {
* 月实际值 * 月实际值
*/ */
private String actual; private String actual;
private List<AnnualKpiProgress> progressList;
} }
\ No newline at end of file
...@@ -87,4 +87,7 @@ public class DepartmentMinutesProgress { ...@@ -87,4 +87,7 @@ public class DepartmentMinutesProgress {
private String commentContent; private String commentContent;
List<ManagerMinutesRecord> managerMinutesRecords;
} }
\ No newline at end of file
...@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; ...@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.infoepoch.pms.agent.common.utils.LogHelper; import com.infoepoch.pms.agent.common.utils.LogHelper;
import com.infoepoch.pms.agent.config.JsonUtils; import com.infoepoch.pms.agent.config.JsonUtils;
import com.infoepoch.pms.agent.domain.itworkbench.model.QueryDto;
import com.infoepoch.pms.agent.domain.itworkbench.model.department.DepartmentWork; import com.infoepoch.pms.agent.domain.itworkbench.model.department.DepartmentWork;
import com.infoepoch.pms.agent.domain.itworkbench.model.kpi.AnnualKpi; import com.infoepoch.pms.agent.domain.itworkbench.model.kpi.AnnualKpi;
import com.infoepoch.pms.agent.domain.itworkbench.model.kpi.AnnualKpiDto; import com.infoepoch.pms.agent.domain.itworkbench.model.kpi.AnnualKpiDto;
...@@ -21,6 +22,7 @@ import org.springframework.web.util.UriBuilder; ...@@ -21,6 +22,7 @@ import org.springframework.web.util.UriBuilder;
import java.net.URI; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
...@@ -175,4 +177,18 @@ public class WorkbenchRemoteService { ...@@ -175,4 +177,18 @@ public class WorkbenchRemoteService {
} }
return uriBuilder.build(); return uriBuilder.build();
} }
public JsonNode queryValidAuthority(QueryDto dto) {
try {
String url = properties.getAuthorityPath();
LogHelper.info(this, "业务接口地址: " + url);
JsonNode root = postForJson(url, null, dto);
if (!root.isObject()) {
throw new IllegalStateException("业务接口返回结构不正确: " + url);
}
return root;
} catch (Exception e) {
return null;
}
}
} }
\ No newline at end of file
...@@ -20,21 +20,17 @@ public class WorkbenchConversationStateService { ...@@ -20,21 +20,17 @@ public class WorkbenchConversationStateService {
private static final String KEY_PREFIX = "workbench:state:"; private static final String KEY_PREFIX = "workbench:state:";
private static final Duration TTL = Duration.ofMinutes(120); private static final Duration TTL = Duration.ofMinutes(120);
private final RedisTemplate<String, Object> redisTemplate; private final RedisTemplate<String, String> redisTemplate;
public Optional<WorkbenchConversationState> load(String sessionId) { public Optional<WorkbenchConversationState> load(String sessionId) {
if (!StringUtils.hasText(sessionId)) { if (!StringUtils.hasText(sessionId)) {
return Optional.empty(); return Optional.empty();
} }
Object cached = redisTemplate.opsForValue().get(KEY_PREFIX + sessionId); String json = redisTemplate.opsForValue().get(KEY_PREFIX + sessionId);
if (cached == null) {
return Optional.empty();
}
try {
String json = JsonUtils.objectToJson(cached);
if (!StringUtils.hasText(json)) { if (!StringUtils.hasText(json)) {
return Optional.empty(); return Optional.empty();
} }
try {
return Optional.ofNullable(JsonUtils.jsonToObject(json, WorkbenchConversationState.class)); return Optional.ofNullable(JsonUtils.jsonToObject(json, WorkbenchConversationState.class));
} catch (Exception e) { } catch (Exception e) {
return Optional.empty(); return Optional.empty();
...@@ -45,6 +41,7 @@ public class WorkbenchConversationStateService { ...@@ -45,6 +41,7 @@ public class WorkbenchConversationStateService {
if (state == null || !StringUtils.hasText(state.sessionId())) { if (state == null || !StringUtils.hasText(state.sessionId())) {
return; return;
} }
redisTemplate.opsForValue().set(KEY_PREFIX + state.sessionId(), state, TTL); String json = JsonUtils.objectToJson(state);
redisTemplate.opsForValue().set(KEY_PREFIX + state.sessionId(), json, TTL);
} }
} }
...@@ -28,9 +28,6 @@ public class WorkbenchIntentRecognizer { ...@@ -28,9 +28,6 @@ public class WorkbenchIntentRecognizer {
this.observationSupport = observationSupport; this.observationSupport = observationSupport;
} }
public WorkbenchIntent recognize(String traceId, String sessionId, String message) {
return recognize(traceId, sessionId, message, null);
}
public WorkbenchIntent recognize(String traceId, String sessionId, String message, WorkbenchConversationState state) { public WorkbenchIntent recognize(String traceId, String sessionId, String message, WorkbenchConversationState state) {
String trimMsg = StringUtils.hasText(message) ? message.trim() : ""; String trimMsg = StringUtils.hasText(message) ? message.trim() : "";
...@@ -47,38 +44,48 @@ public class WorkbenchIntentRecognizer { ...@@ -47,38 +44,48 @@ public class WorkbenchIntentRecognizer {
return doClassify(traceId, sessionId, trimMsg); return doClassify(traceId, sessionId, trimMsg);
} }
// ====================== 只有【筛选词】才走继承 ====================== // ====================== 多轮对话:沿用上次意图 ======================
if (state != null && state.lastIntent() != null) { if (state != null && state.lastIntent() != null) {
WorkbenchIntent lastIntent = state.lastIntent(); WorkbenchIntent lastIntent = state.lastIntent();
if (!WorkbenchIntent.GENERAL_CHAT.equals(lastIntent)) { if (!WorkbenchIntent.GENERAL_CHAT.equals(lastIntent)) {
boolean isIrrelevant = isIrrelevantInput(trimMsg);
if (!isIrrelevant) {
LogHelper.info(this, CareTraceLogSupport.format( LogHelper.info(this, CareTraceLogSupport.format(
traceId, sessionId, "意图识别(多轮)", "筛选条件,沿用意图:" + lastIntent traceId, sessionId, "意图识别 (多轮)",
"消息=[" + trimMsg + "] isNewQuery=" + isNewQuery + " 沿用意图:" + lastIntent
)); ));
return lastIntent; return lastIntent;
} else { } else {
LogHelper.info(this, CareTraceLogSupport.format( LogHelper.info(this, CareTraceLogSupport.format(
traceId, sessionId, "意图识别(多轮)", "无关输入,不继承" traceId, sessionId, "意图识别 (多轮)",
"消息=[" + trimMsg + "] 上次是 GENERAL_CHAT,重新识别"
)); ));
return WorkbenchIntent.GENERAL_CHAT;
}
} }
} }
// 全新会话 // 全新会话 或 上次是 GENERAL_CHAT
LogHelper.info(this, CareTraceLogSupport.format(
traceId, sessionId, "意图识别",
"消息=[" + trimMsg + "] state=" + (state == null ? "null" : "exist") + " 重新识别"
));
return doClassify(traceId, sessionId, trimMsg); return doClassify(traceId, sessionId, trimMsg);
} }
/** /**
* 【关键判断】 * 【关键判断】
* 只要包含这些词 = 用户要查【新业务】,必须清空上下文! * 只要包含这些词 = 用户要查【新业务】,必须清空上下文!
* 排除全量查询关键词(全量/全部/所有/重置/清除)
*/ */
public boolean isNewBusinessQuery(String message) { public boolean isNewBusinessQuery(String message) {
if (!StringUtils.hasText(message)) return false; if (!StringUtils.hasText(message)) return false;
String m = message.toLowerCase(); String m = message.toLowerCase();
return m.contains("重点工作")
|| m.contains("kpi") // 如果包含全量查询关键词,不视为新业务(避免误判)
if (m.contains("全量") || m.contains("全部") || m.contains("所有")
|| m.contains("重置") || m.contains("清除")) {
return false;
}
return
m.contains("kpi")
|| m.contains("绩效") || m.contains("绩效")
|| m.contains("指标") || m.contains("指标")
|| m.contains("工作") || m.contains("工作")
...@@ -137,21 +144,4 @@ public class WorkbenchIntentRecognizer { ...@@ -137,21 +144,4 @@ public class WorkbenchIntentRecognizer {
return WorkbenchIntent.GENERAL_CHAT; return WorkbenchIntent.GENERAL_CHAT;
} }
} }
private boolean isIrrelevantInput(String message) {
if (!StringUtils.hasText(message)) return true;
boolean isPureNumber = message.matches("^\\d+$");
boolean isTooShort = message.length() <= 1;
boolean hasBizKeyword = message.contains("室")
|| message.contains("部门")
|| message.contains("完成")
|| message.contains("填报")
|| message.contains("年度")
|| message.contains("月度")
|| message.contains("重点")
|| message.contains("a")
|| message.contains("b")
|| message.contains("c");
return isPureNumber || isTooShort || !hasBizKeyword;
}
} }
...@@ -15,4 +15,6 @@ public class ITProperties { ...@@ -15,4 +15,6 @@ public class ITProperties {
private String minutesPath; private String minutesPath;
private String workPath; private String workPath;
private String authorityPath;
} }
...@@ -62,6 +62,7 @@ pms: ...@@ -62,6 +62,7 @@ pms:
kpi-path: /it-workbench/api/functionCallTools/queryKpiList kpi-path: /it-workbench/api/functionCallTools/queryKpiList
work-path: /it-workbench/api/functionCallTools/queryWorkList work-path: /it-workbench/api/functionCallTools/queryWorkList
minutes-path: /it-workbench/api/functionCallTools/queryMinutesList minutes-path: /it-workbench/api/functionCallTools/queryMinutesList
authority-path: /it-workbench/api/functionCallTools/queryValidAuthority
sso: sso:
cookie-name: pms_agent_sso_token cookie-name: pms_agent_sso_token
session-cookie-name: pms_agent_session session-cookie-name: pms_agent_session
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment