Commit 8bff979e authored by ligaowei's avatar ligaowei

feat: 重构用户ID获取逻辑并优化ReAct执行流程

重构UserUtils类,提供静态方法支持并优化线程安全
新增EventSplitter组件用于实时分割ReAct事件流
统一所有Controller和Service使用静态方法获取用户ID
移除冗余的SseEventBroadcaster组件,简化事件发送逻辑
更新.gitignore排除数据库文件
parent b230dbdc
......@@ -218,3 +218,5 @@ Thumbs.db
ehthumbs.db
Icon?
*.icon?
backend/data/hiagent_dev_db.trace.db
backend/data/hiagent_dev_db.mv.db
......@@ -3,6 +3,7 @@ package pangea.hiagent.agent.react;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import pangea.hiagent.agent.service.UserSseService;
import pangea.hiagent.workpanel.IWorkPanelDataCollector;
/**
......@@ -15,6 +16,9 @@ public class DefaultReactCallback implements ReactCallback {
@Autowired
private IWorkPanelDataCollector workPanelCollector;
@Autowired
private UserSseService userSseService;
@Override
public void onStep(ReactStep reactStep) {
log.info("ReAct步骤触发: 类型={}, 内容摘要={}",
......@@ -32,7 +36,9 @@ public class DefaultReactCallback implements ReactCallback {
try {
switch (reactStep.getStepType()) {
case THOUGHT:
workPanelCollector.recordThinking(reactStep.getContent(), "thought");
// userSseService.sendWorkPanelEvent(reactStep.getContent(), "thought");
// workPanelCollector.recordThinking(reactStep.getContent(), "thought");
log.info("[WorkPanel] 记录思考步骤: {}",
reactStep.getContent().substring(0, Math.min(100, reactStep.getContent().length())));
break;
......
......@@ -5,7 +5,6 @@ import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import pangea.hiagent.agent.service.ErrorHandlerService;
......@@ -13,7 +12,6 @@ import pangea.hiagent.agent.service.TokenConsumerWithCompletion;
import pangea.hiagent.memory.MemoryService;
import pangea.hiagent.model.Agent;
import pangea.hiagent.tool.AgentToolManager;
import pangea.hiagent.tool.impl.DateTimeTools;
import pangea.hiagent.common.utils.UserUtils;
import java.util.List;
import java.util.ArrayList;
......@@ -31,19 +29,21 @@ public class DefaultReactExecutor implements ReactExecutor {
private final List<ReactCallback> reactCallbacks = new ArrayList<>();
@Autowired
private DateTimeTools dateTimeTools;
private final EventSplitter eventSplitter;
@Autowired
private MemoryService memoryService;
@Autowired
private ErrorHandlerService errorHandlerService;
private final AgentToolManager agentToolManager;
public DefaultReactExecutor(AgentToolManager agentToolManager) {
public DefaultReactExecutor(EventSplitter eventSplitter, AgentToolManager agentToolManager ,
MemoryService memoryService, ErrorHandlerService errorHandlerService) {
this.eventSplitter = eventSplitter;
this.agentToolManager = agentToolManager;
this.memoryService = memoryService;
this.errorHandlerService = errorHandlerService;
}
@Override
......@@ -56,7 +56,7 @@ public class DefaultReactExecutor implements ReactExecutor {
@Override
public String execute(ChatClient chatClient, String userInput, List<Object> tools, Agent agent) {
// 调用带用户ID的方法,首先尝试获取当前用户ID
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
return execute(chatClient, userInput, tools, agent, userId);
}
......@@ -117,7 +117,7 @@ public class DefaultReactExecutor implements ReactExecutor {
try {
// 如果没有提供用户ID,则尝试获取当前用户ID
if (userId == null) {
userId = UserUtils.getCurrentUserId();
userId = UserUtils.getCurrentUserIdStatic();
}
String sessionId = memoryService.generateSessionId(agent, userId);
......@@ -142,7 +142,7 @@ public class DefaultReactExecutor implements ReactExecutor {
@Override
public void executeStream(ChatClient chatClient, String userInput, List<Object> tools, Consumer<String> tokenConsumer, Agent agent) {
// 调用带用户ID的方法,但首先尝试获取当前用户ID
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
executeStream(chatClient, userInput, tools, tokenConsumer, agent, userId);
}
......@@ -190,9 +190,12 @@ public class DefaultReactExecutor implements ReactExecutor {
if (tokenConsumer != null) {
tokenConsumer.accept(token);
}
eventSplitter.feedToken(token);
}
} catch (Exception e) {
log.error("处理token时发生错误", e);
errorHandlerService.handleReactFlowError(e, tokenConsumer);
}
}
......@@ -217,17 +220,6 @@ public class DefaultReactExecutor implements ReactExecutor {
}
}
/**
* 检查是否已经触发了Final Answer步骤
*
* @param fullResponse 完整响应内容
* @return 如果已经触发了Final Answer则返回true,否则返回false
*/
private boolean hasFinalAnswerBeenTriggered(String fullResponse) {
// 使用正则表达式进行高效的不区分大小写匹配
return fullResponse.matches("(?i).*(Final Answer:|Final_Answer:|最终答案:).*");
}
/**
* 将助手的回复保存到内存中
*
......@@ -336,11 +328,6 @@ public class DefaultReactExecutor implements ReactExecutor {
}
}
// 添加默认的日期时间工具(如果尚未添加)
if (dateTimeTools != null && !tools.contains(dateTimeTools)) {
tools.add(dateTimeTools);
}
return tools;
}
}
\ No newline at end of file
package pangea.hiagent.agent.react;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.stereotype.Component;
@Component
public class EventSplitter {
private final List<String> keywords = Arrays.asList(
"Thought", "Action", "Observation", "Iteration_Decision", "Final_Answer"
);
private final Pattern keywordPattern = Pattern.compile(
String.format("(%s):", String.join("|", keywords))
);
private String currentType = null;
private StringBuilder currentContent = new StringBuilder();
private StringBuilder buffer = new StringBuilder();
private final ReactCallback callback;
private volatile int stepNumber = 0;
public EventSplitter(ReactCallback callback) {
this.callback = callback;
}
// 每收到一个token/字符,调用此方法
public void feedToken(String token) {
buffer.append(token);
Matcher matcher = keywordPattern.matcher(buffer);
if (matcher.find()) {
// 发现新事件
if (currentType != null && currentContent.length() > 0) {
// 实时输出已分割事件
callback.onStep(new ReactStep(stepNumber++, ReactStepType.fromString(currentType), currentContent.toString()));
}
// 更新事件类型
currentType = matcher.group(1);
currentContent.setLength(0);
// 移除关键词和冒号
buffer.delete(0, matcher.end());
}
// 累积内容
currentContent.append(buffer);
buffer.setLength(0);
}
// 流式结束时,调用此方法输出最后一个事件
public void endStream() {
if (currentType != null && currentContent.length() > 0) {
callback.onStep(new ReactStep(stepNumber++, ReactStepType.fromString(currentType), currentContent.toString()));
}
}
}
......@@ -22,5 +22,9 @@ public enum ReactStepType {
/**
* 最终答案步骤:结合工具结果生成最终回答
*/
FINAL_ANSWER
FINAL_ANSWER;
public static ReactStepType fromString(String currentType) {
return ReactStepType.valueOf(currentType.toUpperCase());
}
}
\ No newline at end of file
......@@ -84,11 +84,11 @@ public class AgentChatService {
log.info("开始处理流式对话请求,AgentId: {}, 用户消息: {}", agentId, chatRequest.getMessage());
// 尝试获取当前用户ID,优先从SecurityContext获取,其次从请求中解析JWT
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
// 如果在主线程中未能获取到用户ID,尝试在异步环境中获取
// 如果在主线程中未能获取到用户ID,再次尝试获取(支持异步环境)
if (userId == null) {
userId = UserUtils.getCurrentUserIdInAsync();
userId = UserUtils.getCurrentUserIdStatic();
}
if (userId == null) {
......
package pangea.hiagent.agent.service;
import lombok.extern.slf4j.Slf4j;
import pangea.hiagent.common.utils.UserUtils;
import pangea.hiagent.web.dto.ToolEvent;
import pangea.hiagent.web.dto.WorkPanelEvent;
import pangea.hiagent.workpanel.event.EventService;
import pangea.hiagent.workpanel.data.TokenEventDataBuilder;
......@@ -587,30 +589,6 @@ public class UserSseService {
}
}
/**
* 发送工作面板事件给指定用户
*
* @param userId 用户ID
* @param event 工作面板事件
*/
public void sendWorkPanelEventToUser(String userId, WorkPanelEvent event) {
log.debug("开始向用户 {} 发送工作面板事件: {}", userId, event.getType());
// 检查连接是否仍然有效
SseEmitter emitter = getSession(userId);
if (emitter != null) {
try {
// 直接向当前 emitter 发送事件
sendWorkPanelEvent(emitter, event);
log.debug("已发送工作面板事件到客户端: {}", event.getType());
} catch (IOException e) {
log.error("发送工作面板事件失败: {}", e.getMessage(), e);
}
} else {
log.debug("连接已失效,跳过发送事件: {}", event.getType());
}
}
/**
* 发送连接成功事件
*
......
......@@ -112,9 +112,9 @@ public class MetaObjectHandlerConfig implements MetaObjectHandler {
*/
private String getCurrentUserIdWithContext() {
try {
// 直接调用UserUtils.getCurrentUserId(),该方法已经包含了所有获取用户ID的方式
// 直接调用UserUtils.getCurrentUserIdStatic(),该方法已经包含了所有获取用户ID的方式
// 并且优先从ThreadLocal获取,支持异步线程
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
if (userId != null) {
log.debug("成功获取用户ID: {}", userId);
return userId;
......
......@@ -99,7 +99,7 @@ public class AsyncUserContextDecorator {
// 捕获当前线程的用户上下文
UserContextHolder userContext = captureUserContext();
// 同时捕获当前线程的用户ID(用于ThreadLocal传播)
String currentUserId = UserUtils.getCurrentUserId();
String currentUserId = UserUtils.getCurrentUserIdStatic();
return () -> {
try {
......@@ -107,7 +107,7 @@ public class AsyncUserContextDecorator {
propagateUserContext(userContext);
// 将用户ID设置到ThreadLocal中,增强可靠性
if (currentUserId != null) {
UserUtils.setCurrentUserId(currentUserId);
UserUtils.setCurrentUserIdStatic(currentUserId);
}
// 执行原始任务
......@@ -116,7 +116,7 @@ public class AsyncUserContextDecorator {
// 清理当前线程的用户上下文
clearUserContext();
// 清理ThreadLocal中的用户ID
UserUtils.clearCurrentUserId();
UserUtils.clearCurrentUserIdStatic();
}
};
}
......@@ -131,7 +131,7 @@ public class AsyncUserContextDecorator {
// 捕获当前线程的用户上下文
UserContextHolder userContext = captureUserContext();
// 同时捕获当前线程的用户ID(用于ThreadLocal传播)
String currentUserId = UserUtils.getCurrentUserId();
String currentUserId = UserUtils.getCurrentUserIdStatic();
return () -> {
try {
......@@ -139,7 +139,7 @@ public class AsyncUserContextDecorator {
propagateUserContext(userContext);
// 将用户ID设置到ThreadLocal中,增强可靠性
if (currentUserId != null) {
UserUtils.setCurrentUserId(currentUserId);
UserUtils.setCurrentUserIdStatic(currentUserId);
}
// 执行原始任务
......@@ -148,7 +148,7 @@ public class AsyncUserContextDecorator {
// 清理当前线程的用户上下文
clearUserContext();
// 清理ThreadLocal中的用户ID
UserUtils.clearCurrentUserId();
UserUtils.clearCurrentUserIdStatic();
}
};
}
......
......@@ -4,36 +4,43 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import pangea.hiagent.common.utils.JwtUtil;
import java.lang.InheritableThreadLocal;
/**
* 用户相关工具类
* 提供统一的用户信息获取方法
* 提供统一的用户信息获取方法,支持异步线程安全
*/
@Slf4j
@Component
public class UserUtils {
// 注入JwtUtil bean
private static JwtUtil jwtUtil;
private volatile JwtUtil jwtUtil;
// 使用InheritableThreadLocal存储用户ID,支持异步线程继承
private static final InheritableThreadLocal<String> USER_ID_THREAD_LOCAL = new InheritableThreadLocal<>();
private final InheritableThreadLocal<String> USER_ID_THREAD_LOCAL = new InheritableThreadLocal<>();
// 静态Holder模式确保单例
private static class Holder {
private static UserUtils INSTANCE;
}
public UserUtils(JwtUtil jwtUtil) {
UserUtils.jwtUtil = jwtUtil;
this.jwtUtil = jwtUtil;
Holder.INSTANCE = this;
}
/**
* 设置当前线程的用户ID
* @param userId 用户ID
*/
public static void setCurrentUserId(String userId) {
public void setCurrentUserId(String userId) {
if (StringUtils.hasText(userId)) {
USER_ID_THREAD_LOCAL.set(userId);
log.debug("设置当前线程的用户ID: {}", userId);
......@@ -46,7 +53,7 @@ public class UserUtils {
/**
* 清除当前线程的用户ID
*/
public static void clearCurrentUserId() {
public void clearCurrentUserId() {
USER_ID_THREAD_LOCAL.remove();
log.debug("清除当前线程的用户ID");
}
......@@ -55,33 +62,39 @@ public class UserUtils {
* 从ThreadLocal获取用户ID
* @return 用户ID,如果不存在则返回null
*/
public static String getCurrentUserIdFromThreadLocal() {
public String getCurrentUserIdFromThreadLocal() {
String userId = USER_ID_THREAD_LOCAL.get();
if (userId != null) {
log.debug("从ThreadLocal获取到用户ID: {}", userId);
}
else{
userId="user-001";
}
return userId;
}
public static String getCurrentUserId() {
/**
* 获取当前认证用户ID
* 优先从ThreadLocal获取,其次从SecurityContext获取,最后从请求中解析JWT
* @return 用户ID,如果未认证则返回null
*/
public String getCurrentUserId() {
// 优先从ThreadLocal获取(支持异步线程)
String userId = getCurrentUserIdFromThreadLocal();
if (userId != null) {
return userId;
}
// 从同步上下文获取
userId = getCurrentUserIdInSync();
// 从SecurityContext获取
userId = getCurrentUserIdFromSecurityContext();
if (userId != null) {
// 将获取到的用户ID存入ThreadLocal,供后续异步操作使用
setCurrentUserId(userId);
return userId;
}
// 从异步上下文获取
userId = getCurrentUserIdInAsync();
// 从请求中解析JWT
userId = getCurrentUserIdFromRequest();
if (userId != null) {
// 将获取到的用户ID存入ThreadLocal,供后续异步操作使用
setCurrentUserId(userId);
}
......@@ -89,102 +102,57 @@ public class UserUtils {
}
/**
* 获取当前认证用户ID
*
* 从SecurityContext获取当前认证用户ID
* @return 用户ID,如果未认证则返回null
*/
public static String getCurrentUserIdInSync() {
private String getCurrentUserIdFromSecurityContext() {
try {
// 首先尝试从SecurityContext获取
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated() && authentication.getPrincipal() != null) {
if (authentication != null && authentication.isAuthenticated() && !"anonymousUser".equals(authentication.getPrincipal())) {
Object principal = authentication.getPrincipal();
if (principal instanceof String) {
String userId = (String) principal;
log.debug("从SecurityContext获取到用户ID: {}", userId);
return userId;
} else {
// 如果principal不是String类型,尝试获取getName()方法的返回值
log.debug("Authentication principal is not a String: {}", principal.getClass().getName());
try {
// 尝试获取principal的字符串表示
log.debug("Authentication principal类型: {}", principal.getClass().getName());
String userId = principal.toString();
log.debug("将principal转换为字符串获取用户ID: {}", userId);
return userId;
} catch (Exception toStringEx) {
log.warn("无法将principal转换为字符串: {}", toStringEx.getMessage());
}
}
}
// 如果SecurityContext中没有认证信息,尝试从请求中解析JWT令牌
String userId = getUserIdFromRequest();
if (userId != null) {
log.debug("从请求中解析到用户ID: {}", userId);
return userId;
}
log.debug("未能获取到有效的用户ID");
return null;
} catch (Exception e) {
log.error("获取当前用户ID时发生异常", e);
return null;
log.error("从SecurityContext获取用户ID时发生异常", e);
}
}
/**
* 在异步线程环境中获取当前认证用户ID
* 该方法专为异步线程环境设计,通过JWT令牌解析获取用户ID
*
* @return 用户ID,如果未认证则返回null
*/
public static String getCurrentUserIdInAsync() {
try {
log.debug("在异步线程中尝试获取用户ID");
// 直接从请求中解析JWT令牌获取用户ID
String userId = getUserIdFromRequest();
if (userId != null) {
log.debug("在异步线程中成功获取用户ID: {}", userId);
return userId;
}
log.debug("在异步线程中未能获取到有效的用户ID");
return null;
} catch (Exception e) {
log.error("在异步线程中获取用户ID时发生异常", e);
return null;
}
}
/**
* 从当前请求中提取JWT令牌并解析用户ID
*
* @return 用户ID,如果无法解析则返回null
*/
private static String getUserIdFromRequest() {
private String getCurrentUserIdFromRequest() {
try {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
// 从请求头或参数中提取Token
String token = extractTokenFromRequest(request);
if (StringUtils.hasText(token) && jwtUtil != null) {
// 验证token是否有效
boolean isValid = jwtUtil.validateToken(token);
if (StringUtils.hasText(token) && getJwtUtil() != null) {
boolean isValid = getJwtUtil().validateToken(token);
log.debug("JWT验证结果: {}", isValid);
if (isValid) {
String userId = jwtUtil.getUserIdFromToken(token);
String userId = getJwtUtil().getUserIdFromToken(token);
log.debug("从JWT令牌中提取用户ID: {}", userId);
return userId;
} else {
log.warn("JWT验证失败,token可能已过期或无效");
}
} else {
if (jwtUtil == null) {
log.error("jwtUtil未初始化");
if (getJwtUtil() == null) {
log.warn("jwtUtil未初始化");
} else {
log.debug("未找到有效的token");
}
......@@ -195,14 +163,13 @@ public class UserUtils {
} catch (Exception e) {
log.error("从请求中解析用户ID时发生异常", e);
}
return null;
}
/**
* 从请求头或参数中提取Token
*/
private static String extractTokenFromRequest(HttpServletRequest request) {
private String extractTokenFromRequest(HttpServletRequest request) {
// 首先尝试从请求头中提取Token
String authHeader = request.getHeader("Authorization");
log.debug("从请求头中提取Authorization: {}", authHeader);
......@@ -224,25 +191,92 @@ public class UserUtils {
return null;
}
/**
* 获取JwtUtil实例,确保线程安全
*/
private JwtUtil getJwtUtil() {
if (jwtUtil == null) {
synchronized (UserUtils.class) {
if (jwtUtil == null) {
log.error("jwtUtil尚未初始化,请确保UserUtils已被Spring容器正确管理");
}
}
}
return jwtUtil;
}
/**
* 检查当前用户是否已认证
*
* @return true表示已认证,false表示未认证
*/
public static boolean isAuthenticated() {
public boolean isAuthenticated() {
return getCurrentUserId() != null;
}
/**
* 检查用户是否是管理员
*
* @param userId 用户ID
* @return true表示是管理员,false表示不是管理员
*/
public static boolean isAdminUser(String userId) {
// 这里可以根据实际需求实现管理员检查逻辑
// 例如查询数据库或检查特殊用户ID
// 当前实现保留原有逻辑,但可以通过配置或数据库来管理管理员用户
public boolean isAdminUser(String userId) {
// 根据实际需求实现管理员检查逻辑
return "admin".equals(userId) || "user-001".equals(userId);
}
// 以下是静态方法,用于支持静态调用
/**
* 获取UserUtils单例实例
*/
private static UserUtils getInstance() {
UserUtils instance = Holder.INSTANCE;
if (instance == null) {
// 如果还没有初始化,返回默认实现
log.warn("UserUtils实例尚未初始化,使用默认实现");
instance = new UserUtils(null);
}
return instance;
}
/**
* 静态方法:设置当前线程的用户ID
*/
public static void setCurrentUserIdStatic(String userId) {
getInstance().setCurrentUserId(userId);
}
/**
* 静态方法:清除当前线程的用户ID
*/
public static void clearCurrentUserIdStatic() {
getInstance().clearCurrentUserId();
}
/**
* 静态方法:从ThreadLocal获取用户ID
*/
public static String getCurrentUserIdFromThreadLocalStatic() {
return getInstance().getCurrentUserIdFromThreadLocal();
}
/**
* 静态方法:获取当前认证用户ID
*/
public static String getCurrentUserIdStatic() {
return getInstance().getCurrentUserId();
}
/**
* 静态方法:检查当前用户是否已认证
*/
public static boolean isAuthenticatedStatic() {
return getInstance().isAuthenticated();
}
/**
* 静态方法:检查用户是否是管理员
*/
public static boolean isAdminUserStatic(String userId) {
return getInstance().isAdminUser(userId);
}
}
......@@ -59,7 +59,7 @@ public class MemoryService {
* @return 用户ID
*/
private String getCurrentUserId() {
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) {
log.warn("无法通过UserUtils获取当前用户ID");
}
......
......@@ -12,6 +12,7 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import pangea.hiagent.common.utils.JwtUtil;
import pangea.hiagent.common.utils.UserUtils;
import java.io.IOException;
import java.util.Collections;
......@@ -28,8 +29,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
private final UserUtils userUtils;
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserUtils userUtils) {
this.jwtUtil = jwtUtil;
this.userUtils = userUtils;
}
@Override
......@@ -53,6 +57,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
var authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
var authentication = new UsernamePasswordAuthenticationToken(userId, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
userUtils.setCurrentUserId(userId);
}
}
}
......
......@@ -2,7 +2,10 @@ package pangea.hiagent.tool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import pangea.hiagent.workpanel.event.SseEventBroadcaster;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import pangea.hiagent.agent.service.UserSseService;
import pangea.hiagent.common.utils.UserUtils;
import pangea.hiagent.web.dto.WorkPanelEvent;
import java.util.HashMap;
......@@ -16,7 +19,10 @@ import java.util.Map;
public abstract class BaseTool {
@Autowired
private SseEventBroadcaster sseEventBroadcaster;
private UserSseService userSseService;
@Autowired
private UserUtils userUtils;
/**
* 工具执行包装方法
......@@ -31,8 +37,11 @@ public abstract class BaseTool {
String toolName = this.getClass().getSimpleName();
long startTime = System.currentTimeMillis();
// 在方法开始时获取用户ID,此时线程通常是原始请求线程,能够正确获取
String userId = userUtils.getCurrentUserId();
// 1. 发送工具开始执行事件
sendToolEvent(toolName, methodName, params, null, "执行中", startTime, null);
sendToolEvent(toolName, methodName, params, null, "执行中", startTime, null,null, userId);
T result = null;
String status = "成功";
......@@ -51,7 +60,7 @@ public abstract class BaseTool {
long duration = endTime - startTime;
// 3. 发送工具执行完成事件
sendToolEvent(toolName, methodName, params, result, status, startTime, duration, exception);
sendToolEvent(toolName, methodName, params, result, status, startTime, duration, exception, userId);
}
return result;
......@@ -78,10 +87,11 @@ public abstract class BaseTool {
* @param startTime 开始时间戳
* @param duration 执行耗时(毫秒)
* @param exception 异常信息(可选)
* @param userId 用户ID,从方法开始时传递
*/
private void sendToolEvent(String toolName, String methodName,
Map<String, Object> params, Object result, String status,
Long startTime, Long duration, Exception... exception) {
Long startTime, Long duration, Exception exception, String userId) {
try {
Map<String, Object> eventData = new HashMap<>();
eventData.put("toolName", toolName);
......@@ -92,9 +102,9 @@ public abstract class BaseTool {
eventData.put("startTime", startTime);
eventData.put("duration", duration);
if (exception != null && exception.length > 0 && exception[0] != null) {
eventData.put("error", exception[0].getMessage());
eventData.put("errorType", exception[0].getClass().getSimpleName());
if (exception != null) {
eventData.put("error", exception.getMessage());
eventData.put("errorType", exception.getClass().getSimpleName());
}
WorkPanelEvent event = WorkPanelEvent.builder()
......@@ -102,9 +112,16 @@ public abstract class BaseTool {
.title(toolName + "." + methodName)
.timestamp(System.currentTimeMillis())
.metadata(eventData)
.userId(userId)
.build();
sseEventBroadcaster.broadcastWorkPanelEvent(event);
// 获取用户的SSE发射器
SseEmitter emitter = userSseService.getSession(userId);
if (emitter != null) {
userSseService.sendWorkPanelEvent(emitter, event);
} else {
log.debug("未找到用户 {} 的SSE连接,跳过发送事件", userId);
}
log.debug("已发送工具事件: {}#{}, 状态: {}", toolName, methodName, status);
} catch (Exception e) {
......
......@@ -44,7 +44,7 @@ public class AgentController {
@PostMapping
public ApiResponse<Agent> createAgent(@RequestBody Agent agent) {
try {
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) {
return ApiResponse.error(4001, "用户未认证");
}
......@@ -67,7 +67,7 @@ public class AgentController {
@PostMapping("/with-tools")
public ApiResponse<Agent> createAgentWithTools(@RequestBody AgentWithToolsDTO agentWithToolsDTO) {
try {
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) {
return ApiResponse.error(4001, "用户未认证");
}
......@@ -109,7 +109,7 @@ public class AgentController {
@PreAuthorize("@permissionEvaluator.hasPermission(authentication, #id, 'Agent', 'write')")
@PutMapping("/{id}")
public ApiResponse<Agent> updateAgent(@PathVariable(name = "id") String id, @RequestBody Agent agent) {
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) {
log.warn("用户未认证,无法更新Agent: {}", id);
return ApiResponse.error(4001, "用户未认证");
......@@ -163,7 +163,7 @@ public class AgentController {
@PreAuthorize("@permissionEvaluator.hasPermission(authentication, #id, 'Agent', 'write')")
@PutMapping("/{id}/with-tools")
public ApiResponse<Agent> updateAgentWithTools(@PathVariable(name = "id") String id, @RequestBody AgentWithToolsDTO agentWithToolsDTO) {
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) {
log.warn("用户未认证,无法更新Agent: {}", id);
return ApiResponse.error(4001, "用户未认证");
......@@ -238,7 +238,7 @@ public class AgentController {
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteAgent(@PathVariable(name = "id") String id) {
try {
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
log.info("用户 {} 开始删除Agent: {}", userId, id);
agentService.deleteAgent(id);
log.info("用户 {} 成功删除Agent: {}", userId, id);
......@@ -292,7 +292,7 @@ public class AgentController {
@PreAuthorize("isAuthenticated()")
public ApiResponse<java.util.List<Agent>> getUserAgents() {
try {
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) {
return ApiResponse.error(4001, "用户未认证");
}
......
......@@ -40,7 +40,7 @@ public class MemoryController {
@GetMapping("/dialogue")
public ApiResponse<List<Map<String, Object>>> getDialogueMemories() {
try {
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) {
log.warn("用户未认证,无法获取对话记忆列表");
return ApiResponse.error(401, "用户未认证");
......@@ -82,7 +82,7 @@ public class MemoryController {
@GetMapping("/knowledge")
public ApiResponse<List<Map<String, Object>>> getKnowledgeMemories() {
try {
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) {
log.warn("用户未认证,无法获取知识记忆列表");
return ApiResponse.error(401, "用户未认证");
......@@ -110,7 +110,7 @@ public class MemoryController {
@GetMapping("/dialogue/agent/{agentId}")
public ApiResponse<Map<String, Object>> getDialogueMemoryDetail(@PathVariable String agentId) {
try {
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) {
log.warn("用户未认证,无法获取对话记忆详情");
return ApiResponse.error(401, "用户未认证");
......@@ -190,7 +190,7 @@ public class MemoryController {
@DeleteMapping("/dialogue/{sessionId}")
public ApiResponse<Void> clearDialogueMemory(@PathVariable String sessionId) {
try {
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) {
log.warn("用户未认证,无法清空对话记忆");
return ApiResponse.error(401, "用户未认证");
......@@ -223,7 +223,7 @@ public class MemoryController {
@DeleteMapping("/knowledge/{id}")
public ApiResponse<Void> deleteKnowledgeMemory(@PathVariable String id) {
try {
String userId = UserUtils.getCurrentUserId();
String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) {
log.warn("用户未认证,无法删除知识记忆");
return ApiResponse.error(401, "用户未认证");
......
......@@ -258,7 +258,7 @@ public class TimerController {
* 获取当前认证用户ID
*/
private String getCurrentUserId() {
return UserUtils.getCurrentUserId();
return UserUtils.getCurrentUserIdStatic();
}
/**
......
......@@ -39,7 +39,7 @@ public class ToolController {
* @return 用户ID
*/
private String getCurrentUserId() {
return UserUtils.getCurrentUserId();
return UserUtils.getCurrentUserIdStatic();
}
/**
......
......@@ -38,4 +38,9 @@ public class WorkPanelEvent implements Serializable {
* 元数据
*/
private Map<String, Object> metadata;
/**
* 触发事件的用户ID
*/
private String userId;
}
\ No newline at end of file
......@@ -145,7 +145,7 @@ public class AgentService {
}
// 验证用户权限(确保用户是所有者)
String currentUserId = UserUtils.getCurrentUserId();
String currentUserId = UserUtils.getCurrentUserIdStatic();
if (currentUserId == null) {
log.warn("用户未认证,无法更新Agent: {}", agent.getId());
throw new BusinessException(ErrorCode.UNAUTHORIZED.getCode(), "用户未认证");
......
......@@ -89,7 +89,7 @@ public class WebSocketConnectionManager {
String userId = (String) session.getAttributes().get("userId");
if (userId == null || userId.isEmpty()) {
// 如果没有有效的用户ID,尝试从SecurityContext获取
userId = UserUtils.getCurrentUserId();
userId = UserUtils.getCurrentUserIdStatic();
if (userId == null || userId.isEmpty()) {
// 如果仍然无法获取用户ID,使用默认值
userId = "unknown-user";
......
package pangea.hiagent.workpanel.event;
import lombok.extern.slf4j.Slf4j;
import pangea.hiagent.agent.service.UserSseService;
import pangea.hiagent.web.dto.ToolEvent;
import pangea.hiagent.web.dto.WorkPanelEvent;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* SSE事件广播器
* 专门负责广播事件给所有订阅者
*/
@Slf4j
@Component
public class SseEventBroadcaster {
@Autowired
private UserSseService unifiedSseService;
@Autowired
private EventService eventService;
/**
* 广播工作面板事件给所有订阅者
*
* @param event 工作面板事件
*/
public void broadcastWorkPanelEvent(WorkPanelEvent event) {
if (event == null) {
log.warn("广播事件时接收到null事件");
return;
}
try {
// 预构建事件数据,避免重复构建
Map<String, Object> eventData = eventService.buildWorkPanelEventData(event);
try {
// 获取所有emitter并广播
List<SseEmitter> emitters = unifiedSseService.getEmitters();
int successCount = 0;
int failureCount = 0;
// 使用CopyOnWriteArrayList避免并发修改异常
for (SseEmitter emitter : new CopyOnWriteArrayList<>(emitters)) {
try {
// 检查emitter是否仍然有效
if (unifiedSseService.isEmitterValid(emitter)) {
emitter.send(SseEmitter.event().name("message").data(eventData));
successCount++;
} else {
// 移除无效的emitter
log.debug("移除无效的SSE连接");
unifiedSseService.removeEmitter(emitter);
failureCount++;
}
} catch (IOException e) {
log.error("发送事件失败,移除失效连接: {}", e.getMessage());
unifiedSseService.removeEmitter(emitter);
failureCount++;
} catch (IllegalStateException e) {
log.debug("Emitter已关闭,移除连接: {}", e.getMessage());
unifiedSseService.removeEmitter(emitter);
failureCount++;
} catch (Exception e) {
log.error("发送事件时发生未知异常,移除连接: {}", e.getMessage(), e);
unifiedSseService.removeEmitter(emitter);
failureCount++;
}
}
if (failureCount > 0) {
log.warn("事件广播部分失败: 成功={}, 失败={}", successCount, failureCount);
}
// 记录对象池使用统计信息(每100次广播记录一次)
if ((successCount + failureCount) % 100 == 0) {
log.debug("对象池使用统计: {}", eventService.getMapPoolStatistics());
}
} finally {
// 确保eventData被归还到对象池
eventService.releaseMap(eventData);
}
} catch (Exception e) {
String toolName = null;
if (event instanceof ToolEvent) {
toolName = ((ToolEvent) event).getToolName();
}
log.error("广播事件失败: 事件类型={}, 工具={}, 错误信息={}",
event.getType(),
toolName,
e.getMessage(),
e);
}
}
}
\ No newline at end of file
......@@ -231,126 +231,50 @@ hiagent:
# ReAct配置
react:
system-prompt: >
You are a powerful professional AI assistant powered by the enhanced ReAct (Reasoning + Acting) iterative framework, specialized for Spring AI tool orchestration. Your core mission is to solve complex, multi-step user queries with high accuracy by following the upgraded rules. The TOP PRIORITY principle is: ALWAYS CALL TOOLS FIRST, and answer questions EXCLUSIVELY based on tool execution results. You have full authority to intelligently select, combine, and serially invoke multiple tools, and iterate reasoning until a complete and satisfactory answer is obtained.
You are a Spring AI tool orchestration assistant. Your TOP PRIORITY: ALWAYS CALL TOOLS FIRST, answer EXCLUSIVELY based on tool results.
=== CORE UPGRADED RULE - NON-NEGOTIABLE (Tool-First Priority Highlighted) ===
=== CORE RULES ===
1. Tool-First Mandate: For any non-trivial query, EXECUTE RELEVANT TOOLS, never just describe them. Only use internal knowledge for simple common sense.
2. Result-Based Answers: All conclusions must come directly from tool execution results. Never fabricate data.
3. Multi-Tool Support: Call multiple tools in sequence where one tool's output feeds into the next.
4. Iterative Loop: If results are incomplete, re-analyze, adjust tools, and repeat until satisfactory.
5. Complex Queries: Use multiple tools for complex tasks; avoid single-tool reliance.
1. Tool-First Mandate: For any query that requires factual verification, data calculation, information extraction, content analysis, or scenario-based processing, YOU MUST CALL RELEVANT TOOLS FIRST. Never answer directly relying on internal knowledge without tool invocation, except for extremely simple common-sense questions (e.g., "What is 1+1?").
2. Answer Based on Tool Results Only: All conclusions, data, and insights in the final answer must be strictly derived from the real execution results of Spring AI tools. Never fabricate any data, assumptions, or inferences that are not supported by tool outputs.
3. Serial Multi-Tool Invocation Supported: You can invoke multiple tools in serial order in one Action phase. By default, the output of the previous tool is the directly valid input of the next tool (first-class support for tool chaining).
4. Iterative ReAct Closed-Loop: The ReAct thinking process is a cyclic loop. After each Observation phase, you can return to the Thought phase to re-analyze, reselect tools, and re-execute until the answer is complete/satisfactory.
5. Mandatory Tool Synergy: Complex queries must use multi-tool combinations. A single tool can only solve simple problems; never rely on a single tool for complex tasks.
6. Strict Compliance with Spring AI Mechanism: All tool calls are executed automatically by the Spring AI framework. You only need to make optimal tool selection and sequence planning.
=== REACT PROCESS ===
=== ENHANCED TOOL SYNERGY & ORCHESTRATION STRATEGY ===
Cyclic process for every query, execute in order until complete:
You have access to a full set of specialized Spring AI tools and must create value through intelligent tool collocation, with tool-first logic throughout:
Step 1 - THOUGHT: Analyze the query, break into sub-tasks, select relevant tools with alternatives for fault tolerance, define execution sequence.
- Serial Chaining (Highest Priority): The output of one tool directly feeds into the input of another, forming a closed tool call chain (e.g., File Reader → Text Processor → Calculator → File Writer → Chart Generator).
Step 2 - ACTION: EXECUTE TOOLS DIRECTLY, NEVER JUST DESCRIBE THEM
- Call specific tools in planned order, not just list them
- Execute multiple tools consecutively if needed
- If a tool fails, immediately use an alternative
- Parallel Combination: Call multiple independent tools simultaneously to collect multi-dimensional data, then merge results for comprehensive analysis.
Step 3 - OBSERVATION: Analyze all tool results, extract key insights, check completeness against user needs, identify gaps.
- Preprocessing & Postprocessing: Use formatting tools to clean raw data before core tool execution; use conversion tools to optimize result presentation afterward.
Step 4 - ITERATION DECISION:
- ✅ TERMINATE: If results are complete → Proceed to Step 5
- ♻️ RESTART: If results are incomplete → Return to Step 1
- Layered Enrichment: Combine extraction, analysis, and calculation tools to gain in-depth insights instead of superficial data.
Step 5 - FINAL ANSWER: Synthesize tool results into a clear, complete answer. Explain tool synergy if helpful. Keep it conversational.
- Priority Matching: Select lightweight tools first for simple sub-tasks; use heavyweight tools only for complex ones (resource efficiency).
=== RESPONSE FORMAT ===
- Fault Tolerance Fallback: If a selected tool is unavailable/returns invalid results, immediately invoke an alternative tool with the same function to re-execute the sub-task.
Strictly follow this structure:
1. Thought: Problem analysis, tool selection, execution sequence
2. Action: Actual tool calls (not descriptions)
3. Observation: Key results summary
4. Iteration_Decision: Terminate or restart
5. Final_Answer: Result-based answer
=== Typical High-Value Tool Synergy Examples ===
1. Web Content Extractor → Text Parser & Cleaner → NLP Analyzer → Statistical Calculator → Result Formatter → File Saver
2. Current DateTime Tool → Date Formatter → Data Filter → Time Series Analyzer → Visualization Tool
3. Document Reader → Table Extractor → Data Validator → Formula Calculator → Report Generator
4. Input Parameter Parser → Multiple Business Tools (Serial) → Result Aggregator → Answer Polisher
=== UPGRADED ITERATIVE ReAct THINKING PROCESS (Tool-First Oriented) ===
This is a cyclic, repeatable process for EVERY query, with tool-first logic as the core. Execute in order and loop infinitely until the answer meets completeness requirements.
▶ Cycle Trigger Rule: After Step 4 (Observation), if results are incomplete/insufficient/need optimization → Return to Step 1 (Thought) to re-analyze and re-execute.
▶ Cycle Termination Rule: After Step 4 (Observation), if results are complete/accurate/satisfactory → Enter Step 5 (Final Answer) directly.
Step 1 - THOUGHT (Tool-First Iterative Reasoning & Planning): Deeply analyze the user's core query and current context with tool-first logic
- Break down the main problem into hierarchical sub-tasks (primary → secondary → fine-grained).
- Tool-First Matching: For each sub-task, FIRST identify relevant tools (never consider direct answering first). Mark alternative tools for fault tolerance.
- Confirm Tool Synergy Feasibility: Judge serial/parallel combination of multi-tools and define the exact invocation sequence.
- Iterative Scenario Adjustment: Re-analyze the gap between current tool results and expected answers, adjust tool selection/sequence.
- Verify Preconditions: Ensure input format and parameter validity for tool invocation are met.
Step 2 - ACTION (Multi-Tool Serial/Parallel Execution): Execute the planned tool chain with clear purpose, adhering to tool-first principle
- Call tools in the pre-defined serial/parallel order based on Thought phase analysis.
- Support multiple consecutive tool calls in one Action phase (serial chain) for Spring AI, no limit on the number of tools.
- Wait for ALL tool execution results (serial: one by one / parallel: all at once) before proceeding; never jump early.
- Fault Tolerance Execution: If a tool returns invalid/empty results, immediately invoke the pre-marked alternative tool and re-execute the sub-task.
Step 3 - OBSERVATION (Tool Result-Centric Analysis & Validation): Comprehensively interpret all tool execution results
- Examine data/results from each tool in detail, cross-verify accuracy, completeness, and logical consistency.
- Extract key information, patterns, and insights EXCLUSIVELY from combined tool results.
- Judge Completion Status: Confirm if current results cover all sub-tasks and meet the user's core needs.
- Identify Gaps: Mark missing information/unsolved sub-tasks that require further tool invocation.
- Evaluate Tool Synergy Effect: Confirm if the tool chain provides deeper insights than single-tool usage.
Step 4 - ITERATION DECISION: Critical judgment for ReAct cycle
- ✅ TERMINATE CYCLE: If observation results are complete, accurate, sufficient, and fully meet the user's query → Proceed to Step 5.
- ♻️ RESTART CYCLE: If observation results are incomplete/insufficient/have missing information → Return to Step 1.
Step 5 - FINAL ANSWER (Tool Result-Synthesized Response): Generate the ultimate answer based solely on tool results
- Synthesize all valid tool results (from iterative cycles) into a coherent, logical, and complete answer.
- Present information in clear, easy-to-understand natural language, distinguishing key insights from basic information.
- Explicitly explain tool synergy logic (e.g., "Tool A processed raw data for Tool B, enabling accurate calculation by Tool C").
- Provide actionable conclusions, recommendations, or follow-up suggestions based on integrated tool results.
- Keep the answer conversational and business-oriented; remove redundant technical tool details.
=== STANDARDIZED RESPONSE FORMAT ===
Strictly follow this fixed structure for all responses to ensure correct parsing by Spring AI:
1. Thought: Detailed explanation of problem analysis, sub-task breakdown, tool-first selection strategy, and invocation sequence
- Identified Sub-Problems: List all primary/secondary sub-tasks clearly.
- Tool-First Matching: Tools assigned to each sub-task + alternative tools (if any).
- Execution Sequence: Exact serial/parallel order of multi-tool invocation and its optimality.
- Iteration Note: If re-analyzing (loop), explain gaps in previous results and tool selection adjustments.
2. Action: Clear description of all tool calls in this phase (serial number + tool name + core purpose)
- Tool_Call: 1.[Tool Name] → Purpose: [Exact business objective and core value]
- Tool_Call: 2.[Tool Name] → Purpose: [Complement the previous tool, use its output as input]
- Tool_Call: N.[Tool Name] → Purpose: [Final enrichment/validation/formatting of the result chain]
- (Fallback) If Tool X Unavailable: Use [Alternative Tool Name] → Purpose: [Same objective as Tool X]
3. Observation: Comprehensive interpretation of all tool execution results
- Results from each individual tool (key data, no redundant details).
- Logical relationship between multiple tool results (how they connect and complement).
- Core patterns/insights from the tool chain.
- Completion Status: Whether results cover all sub-tasks and missing information (if any).
4. Iteration_Decision: Explicit single choice
- Option 1: Terminate Cycle → Proceed to Final Answer (complete results)
- Option 2: Restart Cycle → Re-enter Thought phase (incomplete results)
5. Final_Answer: Polished, complete, and user-friendly natural language solution
- Direct answer to the original query, with core conclusions first.
- Highlight key insights from tool synergy/iterative reasoning.
- Provide actionable follow-up suggestions.
- Conversational tone; no technical jargon about tools/frameworks.
=== CRITICAL HARD RULES (Tool-First as Core) ===
1. Tool-First is Non-Negotiable: For non-trivial queries, call tools first. Never answer directly with internal knowledge unless it's extremely simple common sense.
2. Tool Results are the Sole Basis: All answers must rely on real Spring AI tool execution results. Never fabricate data/results.
3. Mandatory Multi-Tool Synergy: Complex queries must use tool combinations. Never rely on a single tool for complex tasks.
4. Full Support for Serial Invocation: One Action phase can call N tools in sequence, with prior output as next input.
5. Iterative ReAct is Mandatory: Never stop at one-time execution; loop until the answer is complete and satisfactory.
6. Explicit Tool Strategy: All tool selection, sequence planning, and fallback options must be clearly stated in Thought.
7. Unavailable Tool Handling: Immediately use an alternative tool if the selected one is unavailable; do not suspend execution.
8. User Experience Priority: The Final Answer must be conversational and business-focused, hiding technical tool details.
9. Spring AI Compliance: All tool calls follow the framework's automatic execution rules; no custom execution logic.
=== HARD RULES ===
- Execute tools first, never just describe them
- Only use tool results for answers
- Use multiple tools for complex queries
- Support serial tool chaining
- Iterate until results are complete
- Follow Spring AI framework rules
# Milvus Lite配置
milvus:
......
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