feat(agent): 补齐会话消息与工作台链路

This commit is contained in:
2026-06-01 04:07:31 +08:00
parent 041ed0b446
commit 5e0212d2a0
29 changed files with 1288 additions and 27 deletions

View File

@@ -0,0 +1,59 @@
package com.bruce.agent.controller;
import com.bruce.agent.dto.request.AgentSessionCreateDTO;
import com.bruce.agent.dto.request.AgentSessionMessageCreateDTO;
import com.bruce.agent.service.IAgentMessageService;
import com.bruce.agent.service.IAgentSessionService;
import com.bruce.agent.service.IAgentWorkspaceService;
import com.bruce.agent.vo.AgentMessageVO;
import com.bruce.agent.vo.AgentSessionDetailVO;
import com.bruce.agent.vo.AgentWorkspaceVO;
import com.bruce.common.domain.model.RequestResult;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/agent-sessions")
@RequiredArgsConstructor
public class AgentSessionController {
private final IAgentSessionService agentSessionService;
private final IAgentMessageService agentMessageService;
private final IAgentWorkspaceService agentWorkspaceService;
@PostMapping("/create")
public RequestResult<Boolean> create(@RequestBody AgentSessionCreateDTO request) {
return RequestResult.success(agentSessionService.createSession(request));
}
@GetMapping("/detail")
public RequestResult<AgentSessionDetailVO> detail(@RequestParam("id") Long id) {
return RequestResult.success(agentSessionService.getDetailById(id));
}
@GetMapping("/{sessionId}/messages")
public RequestResult<List<AgentMessageVO>> messages(@PathVariable("sessionId") Long sessionId) {
return RequestResult.success(agentMessageService.listBySessionId(sessionId));
}
@PostMapping("/{sessionId}/messages")
public RequestResult<Boolean> appendMessage(@PathVariable("sessionId") Long sessionId,
@RequestBody AgentSessionMessageCreateDTO request) {
request.setSessionId(sessionId);
return RequestResult.success(agentMessageService.appendMessage(request));
}
@GetMapping("/workspace")
public RequestResult<AgentWorkspaceVO> workspace(@RequestParam("agentId") Long agentId,
@RequestParam(value = "sessionId", required = false) Long sessionId) {
return RequestResult.success(agentWorkspaceService.getWorkspace(agentId, sessionId));
}
}

View File

@@ -0,0 +1,13 @@
package com.bruce.agent.dto.request;
import lombok.Data;
@Data
public class AgentSessionCreateDTO {
private Long agentId;
private String sessionCode;
private Long workflowRunId;
private String title;
private String metadataJson;
private String remark;
}

View File

@@ -0,0 +1,14 @@
package com.bruce.agent.dto.request;
import lombok.Data;
@Data
public class AgentSessionMessageCreateDTO {
private Long sessionId;
private String role;
private String content;
private String citationJson;
private Integer tokenCount;
private String requestId;
private String remark;
}

View File

@@ -11,6 +11,8 @@ public class AgentChatResponse {
private String agentName; private String agentName;
private Long storeId; private Long storeId;
private String storeName; private String storeName;
private Long sessionId;
private String sessionCode;
private String answer; private String answer;
private String modelRequestId; private String modelRequestId;
private List<ReferenceChunk> references; private List<ReferenceChunk> references;

View File

@@ -0,0 +1,33 @@
package com.bruce.agent.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.bruce.common.domain.model.BaseEntity;
import com.bruce.common.typehandler.PgJsonbStringTypeHandler;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("agent_capability_binding")
public class AgentCapabilityBinding extends BaseEntity {
@TableField("owner_type")
private String ownerType;
@TableField("owner_id")
private Long ownerId;
@TableField("capability_type")
private String capabilityType;
@TableField("capability_id")
private Long capabilityId;
private Boolean enabled;
@TableField(value = "config_json", typeHandler = PgJsonbStringTypeHandler.class)
private String configJson;
private String remark;
}

View File

@@ -25,5 +25,6 @@ public class AgentDefinition extends BaseEntity {
private String status; private String status;
@TableField("remark")
private String remark; private String remark;
} }

View File

@@ -0,0 +1,29 @@
package com.bruce.agent.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.bruce.common.domain.model.BaseEntity;
import com.bruce.common.typehandler.PgJsonbStringTypeHandler;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("agent_message")
public class AgentMessage extends BaseEntity {
@TableField("session_id")
private Long sessionId;
private String role;
private String content;
@TableField(value = "citation_json", typeHandler = PgJsonbStringTypeHandler.class)
private String citationJson;
@TableField("token_count")
private Integer tokenCount;
private String remark;
}

View File

@@ -0,0 +1,32 @@
package com.bruce.agent.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.bruce.common.domain.model.BaseEntity;
import com.bruce.common.typehandler.PgJsonbStringTypeHandler;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("agent_session")
public class AgentSession extends BaseEntity {
@TableField("session_code")
private String sessionCode;
@TableField("agent_id")
private Long agentId;
@TableField("workflow_run_id")
private Long workflowRunId;
private String title;
private String status;
@TableField(value = "metadata_json", typeHandler = PgJsonbStringTypeHandler.class)
private String metadataJson;
private String remark;
}

View File

@@ -0,0 +1,52 @@
package com.bruce.agent.factory;
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
import com.bruce.agent.dto.response.AgentDefinitionResponse;
import com.bruce.agent.entity.AgentDefinition;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* Agent 定义工厂,统一处理请求、实体和返回对象转换。
*/
@Component
public class AgentDefinitionFactory {
public AgentDefinition toEntity(AgentDefinitionSaveRequest request) {
if (request == null) {
return null;
}
AgentDefinition entity = new AgentDefinition();
entity.setId(request.getId());
entity.setAgentCode(trimToNull(request.getAgentCode()));
entity.setAgentName(trimToNull(request.getAgentName()));
entity.setSystemPrompt(trimToNull(request.getSystemPrompt()));
entity.setStoreId(request.getStoreId());
entity.setStatus(trimToNull(request.getStatus()));
entity.setRemark(trimToNull(request.getRemark()));
return entity;
}
public AgentDefinitionResponse toResponse(AgentDefinition entity) {
if (entity == null) {
return null;
}
AgentDefinitionResponse response = new AgentDefinitionResponse();
BeanUtils.copyProperties(entity, response);
return response;
}
public List<AgentDefinitionResponse> toResponses(List<AgentDefinition> entities) {
return entities == null ? List.of() : entities.stream().map(this::toResponse).toList();
}
private String trimToNull(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
return value.trim();
}
}

View File

@@ -0,0 +1,51 @@
package com.bruce.agent.factory;
import com.bruce.agent.dto.request.AgentSessionMessageCreateDTO;
import com.bruce.agent.entity.AgentMessage;
import com.bruce.agent.vo.AgentMessageVO;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* Agent 消息工厂,统一处理消息入参与返回转换。
*/
@Component
public class AgentMessageFactory {
public AgentMessage toEntity(AgentSessionMessageCreateDTO request) {
if (request == null) {
return null;
}
AgentMessage entity = new AgentMessage();
entity.setSessionId(request.getSessionId());
entity.setRole(trimToNull(request.getRole()));
entity.setContent(trimToNull(request.getContent()));
entity.setCitationJson(trimToNull(request.getCitationJson()));
entity.setTokenCount(request.getTokenCount());
entity.setRemark(trimToNull(request.getRemark()));
return entity;
}
public AgentMessageVO toVO(AgentMessage entity) {
if (entity == null) {
return null;
}
AgentMessageVO vo = new AgentMessageVO();
BeanUtils.copyProperties(entity, vo);
return vo;
}
public List<AgentMessageVO> toVOList(List<AgentMessage> entities) {
return entities == null ? List.of() : entities.stream().map(this::toVO).toList();
}
private String trimToNull(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
return value.trim();
}
}

View File

@@ -0,0 +1,51 @@
package com.bruce.agent.factory;
import com.bruce.agent.dto.request.AgentSessionCreateDTO;
import com.bruce.agent.entity.AgentSession;
import com.bruce.agent.vo.AgentSessionDetailVO;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* Agent 会话工厂,统一处理会话转换逻辑。
*/
@Component
public class AgentSessionFactory {
public AgentSession toEntity(AgentSessionCreateDTO request) {
if (request == null) {
return null;
}
AgentSession entity = new AgentSession();
entity.setAgentId(request.getAgentId());
entity.setSessionCode(trimToNull(request.getSessionCode()));
entity.setWorkflowRunId(request.getWorkflowRunId());
entity.setTitle(trimToNull(request.getTitle()));
entity.setMetadataJson(trimToNull(request.getMetadataJson()));
entity.setRemark(trimToNull(request.getRemark()));
return entity;
}
public AgentSessionDetailVO toDetailVO(AgentSession entity) {
if (entity == null) {
return null;
}
AgentSessionDetailVO detailVO = new AgentSessionDetailVO();
BeanUtils.copyProperties(entity, detailVO);
return detailVO;
}
public List<AgentSessionDetailVO> toDetailVOList(List<AgentSession> entities) {
return entities == null ? List.of() : entities.stream().map(this::toDetailVO).toList();
}
private String trimToNull(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
return value.trim();
}
}

View File

@@ -0,0 +1,9 @@
package com.bruce.agent.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bruce.agent.entity.AgentCapabilityBinding;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AgentCapabilityBindingMapper extends BaseMapper<AgentCapabilityBinding> {
}

View File

@@ -0,0 +1,9 @@
package com.bruce.agent.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bruce.agent.entity.AgentMessage;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AgentMessageMapper extends BaseMapper<AgentMessage> {
}

View File

@@ -0,0 +1,9 @@
package com.bruce.agent.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bruce.agent.entity.AgentSession;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AgentSessionMapper extends BaseMapper<AgentSession> {
}

View File

@@ -0,0 +1,17 @@
package com.bruce.agent.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bruce.agent.dto.request.AgentSessionMessageCreateDTO;
import com.bruce.agent.entity.AgentMessage;
import com.bruce.agent.vo.AgentMessageVO;
import java.util.List;
public interface IAgentMessageService extends IService<AgentMessage> {
boolean appendMessage(AgentSessionMessageCreateDTO request);
AgentMessage appendMessageEntity(AgentSessionMessageCreateDTO request);
List<AgentMessageVO> listBySessionId(Long sessionId);
}

View File

@@ -0,0 +1,21 @@
package com.bruce.agent.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bruce.agent.dto.request.AgentSessionCreateDTO;
import com.bruce.agent.entity.AgentSession;
import com.bruce.agent.vo.AgentSessionDetailVO;
import java.util.List;
public interface IAgentSessionService extends IService<AgentSession> {
boolean createSession(AgentSessionCreateDTO request);
AgentSession createSessionEntity(AgentSessionCreateDTO request);
List<AgentSessionDetailVO> listByAgentId(Long agentId);
AgentSessionDetailVO getDetailById(Long sessionId);
boolean closeSession(Long sessionId);
}

View File

@@ -0,0 +1,8 @@
package com.bruce.agent.service;
import com.bruce.agent.vo.AgentWorkspaceVO;
public interface IAgentWorkspaceService {
AgentWorkspaceVO getWorkspace(Long agentId, Long sessionId);
}

View File

@@ -4,13 +4,21 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.agent.dto.request.AgentChatRequest; import com.bruce.agent.dto.request.AgentChatRequest;
import com.bruce.agent.dto.request.AgentDefinitionQueryRequest; import com.bruce.agent.dto.request.AgentDefinitionQueryRequest;
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest; import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
import com.bruce.agent.dto.request.AgentSessionCreateDTO;
import com.bruce.agent.dto.request.AgentSessionMessageCreateDTO;
import com.bruce.agent.dto.response.AgentChatResponse; import com.bruce.agent.dto.response.AgentChatResponse;
import com.bruce.agent.dto.response.AgentDefinitionResponse; import com.bruce.agent.dto.response.AgentDefinitionResponse;
import com.bruce.agent.entity.AgentDefinition; import com.bruce.agent.entity.AgentDefinition;
import com.bruce.agent.entity.AgentMessage;
import com.bruce.agent.entity.AgentSession;
import com.bruce.agent.factory.AgentDefinitionFactory;
import com.bruce.agent.mapper.AgentDefinitionMapper; import com.bruce.agent.mapper.AgentDefinitionMapper;
import com.bruce.agent.service.IAgentDefinitionService; import com.bruce.agent.service.IAgentDefinitionService;
import com.bruce.agent.service.IAgentMessageService;
import com.bruce.agent.service.IAgentSessionService;
import com.bruce.common.enums.EnableStatusEnum; import com.bruce.common.enums.EnableStatusEnum;
import com.bruce.modelprovider.client.OpenAiChatMessage; import com.bruce.modelprovider.client.OpenAiChatMessage;
import com.bruce.modelprovider.entity.ModelCallLog;
import com.bruce.modelprovider.entity.RagStoreModelConfig; import com.bruce.modelprovider.entity.RagStoreModelConfig;
import com.bruce.modelprovider.gateway.ChatModelGateway; import com.bruce.modelprovider.gateway.ChatModelGateway;
import com.bruce.modelprovider.gateway.ChatRequest; import com.bruce.modelprovider.gateway.ChatRequest;
@@ -30,6 +38,7 @@ import org.springframework.util.StringUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.UUID;
@Slf4j @Slf4j
@Service @Service
@@ -44,35 +53,32 @@ public class AgentDefinitionServiceImpl extends ServiceImpl<AgentDefinitionMappe
private final RagChunkEmbeddingMapper ragChunkEmbeddingMapper; private final RagChunkEmbeddingMapper ragChunkEmbeddingMapper;
private final EmbeddingModelGateway embeddingModelGateway; private final EmbeddingModelGateway embeddingModelGateway;
private final ChatModelGateway chatModelGateway; private final ChatModelGateway chatModelGateway;
private final IAgentSessionService agentSessionService;
private final IAgentMessageService agentMessageService;
private final AgentDefinitionFactory agentDefinitionFactory;
@Override @Override
public List<AgentDefinitionResponse> listResponses() { public List<AgentDefinitionResponse> listResponses() {
return lambdaQuery() return agentDefinitionFactory.toResponses(lambdaQuery()
.orderByAsc(AgentDefinition::getAgentCode) .orderByAsc(AgentDefinition::getAgentCode)
.list() .list());
.stream()
.map(AgentDefinitionResponse::fromEntity)
.toList();
} }
@Override @Override
public List<AgentDefinitionResponse> query(AgentDefinitionQueryRequest request) { public List<AgentDefinitionResponse> query(AgentDefinitionQueryRequest request) {
AgentDefinitionQueryRequest queryRequest = request == null ? new AgentDefinitionQueryRequest() : request; AgentDefinitionQueryRequest queryRequest = request == null ? new AgentDefinitionQueryRequest() : request;
return lambdaQuery() return agentDefinitionFactory.toResponses(lambdaQuery()
.eq(StringUtils.hasText(queryRequest.getAgentCode()), AgentDefinition::getAgentCode, trimToNull(queryRequest.getAgentCode())) .eq(StringUtils.hasText(queryRequest.getAgentCode()), AgentDefinition::getAgentCode, trimToNull(queryRequest.getAgentCode()))
.like(StringUtils.hasText(queryRequest.getAgentName()), AgentDefinition::getAgentName, trimToNull(queryRequest.getAgentName())) .like(StringUtils.hasText(queryRequest.getAgentName()), AgentDefinition::getAgentName, trimToNull(queryRequest.getAgentName()))
.eq(StringUtils.hasText(queryRequest.getStatus()), AgentDefinition::getStatus, trimToNull(queryRequest.getStatus())) .eq(StringUtils.hasText(queryRequest.getStatus()), AgentDefinition::getStatus, trimToNull(queryRequest.getStatus()))
.eq(queryRequest.getStoreId() != null, AgentDefinition::getStoreId, queryRequest.getStoreId()) .eq(queryRequest.getStoreId() != null, AgentDefinition::getStoreId, queryRequest.getStoreId())
.orderByAsc(AgentDefinition::getAgentCode) .orderByAsc(AgentDefinition::getAgentCode)
.list() .list());
.stream()
.map(AgentDefinitionResponse::fromEntity)
.toList();
} }
@Override @Override
public AgentDefinitionResponse getResponseById(Long id) { public AgentDefinitionResponse getResponseById(Long id) {
return AgentDefinitionResponse.fromEntity(getById(id)); return agentDefinitionFactory.toResponse(getById(id));
} }
@Override @Override
@@ -81,30 +87,38 @@ public class AgentDefinitionServiceImpl extends ServiceImpl<AgentDefinitionMappe
if (ragStoreService.getById(request.getStoreId()) == null) { if (ragStoreService.getById(request.getStoreId()) == null) {
throw new IllegalArgumentException("绑定知识库不存在ID: " + request.getStoreId()); throw new IllegalArgumentException("绑定知识库不存在ID: " + request.getStoreId());
} }
String agentCode = request.getAgentCode().trim();
AgentDefinition duplicate = lambdaQuery() AgentDefinition duplicate = lambdaQuery()
.eq(AgentDefinition::getAgentCode, request.getAgentCode().trim()) .eq(AgentDefinition::getAgentCode, agentCode)
.ne(request.getId() != null, AgentDefinition::getId, request.getId()) .ne(request.getId() != null, AgentDefinition::getId, request.getId())
.one(); .one();
if (duplicate != null) { if (duplicate != null) {
throw new IllegalArgumentException("Agent编码已存在: " + request.getAgentCode().trim()); throw new IllegalArgumentException("Agent编码已存在: " + agentCode);
} }
AgentDefinition requestEntity = agentDefinitionFactory.toEntity(request);
AgentDefinition entity = request.getId() == null ? new AgentDefinition() : getById(request.getId()); AgentDefinition entity = request.getId() == null ? new AgentDefinition() : getById(request.getId());
if (entity == null) { if (entity == null) {
throw new IllegalArgumentException("Agent不存在ID: " + request.getId()); throw new IllegalArgumentException("Agent不存在ID: " + request.getId());
} }
entity.setAgentCode(request.getAgentCode().trim()); entity.setAgentCode(requestEntity.getAgentCode());
entity.setAgentName(request.getAgentName().trim()); entity.setAgentName(requestEntity.getAgentName());
entity.setSystemPrompt(trimToNull(request.getSystemPrompt())); entity.setSystemPrompt(requestEntity.getSystemPrompt());
entity.setStoreId(request.getStoreId()); entity.setStoreId(requestEntity.getStoreId());
entity.setStatus(StringUtils.hasText(request.getStatus()) entity.setStatus(StringUtils.hasText(requestEntity.getStatus())
? request.getStatus().trim() ? requestEntity.getStatus()
: EnableStatusEnum.ENABLED.name()); : EnableStatusEnum.ENABLED.name());
entity.setRemark(trimToNull(request.getRemark())); entity.setRemark(requestEntity.getRemark());
return request.getId() == null ? save(entity) : updateById(entity);
boolean result = request.getId() == null ? save(entity) : updateById(entity);
log.info("Agent定义保存完成agentId={}, agentCode={}, storeId={}, result={}",
entity.getId(), entity.getAgentCode(), entity.getStoreId(), result);
return result;
} }
@Override @Override
public AgentChatResponse chat(Long agentId, AgentChatRequest request) { public AgentChatResponse chat(Long agentId, AgentChatRequest request) {
log.info("Agent兼容聊天开始agentId={}", agentId);
if (agentId == null) { if (agentId == null) {
throw new IllegalArgumentException("Agent ID不能为空"); throw new IllegalArgumentException("Agent ID不能为空");
} }
@@ -148,16 +162,15 @@ public class AgentDefinitionServiceImpl extends ServiceImpl<AgentDefinitionMappe
} }
String queryVector = toVectorLiteral(queryEmbedding.getVectors().getFirst()); String queryVector = toVectorLiteral(queryEmbedding.getVectors().getFirst());
recalls = ragChunkEmbeddingMapper.queryTopKByStore( recalls = ragChunkEmbeddingMapper.queryTopKByStore(agent.getStoreId(), queryVector, DEFAULT_TOP_K);
agent.getStoreId(),
queryVector,
DEFAULT_TOP_K
);
if (recalls.isEmpty()) { if (recalls.isEmpty()) {
throw new IllegalArgumentException("未召回到可用知识切片,请先完成知识库切片与向量化"); throw new IllegalArgumentException("未召回到可用知识切片,请先完成知识库切片与向量化");
} }
} }
AgentSession session = createCompatibilitySession(agentId, request);
appendRequestMessages(session.getId(), request.getMessages());
ChatRequest chatRequest = new ChatRequest(); ChatRequest chatRequest = new ChatRequest();
chatRequest.setTaskType(ragEnabled ? "RAG_ANSWER" : "CHAT_SIMPLE"); chatRequest.setTaskType(ragEnabled ? "RAG_ANSWER" : "CHAT_SIMPLE");
chatRequest.setMatchScope("AGENT"); chatRequest.setMatchScope("AGENT");
@@ -167,15 +180,21 @@ public class AgentDefinitionServiceImpl extends ServiceImpl<AgentDefinitionMappe
chatRequest.setMessages(buildChatMessages(agent, recalls, request.getMessages(), ragEnabled)); chatRequest.setMessages(buildChatMessages(agent, recalls, request.getMessages(), ragEnabled));
ChatResult chatResult = chatModelGateway.chat(chatRequest); ChatResult chatResult = chatModelGateway.chat(chatRequest);
appendAssistantMessage(session.getId(), recalls, chatResult);
AgentChatResponse response = new AgentChatResponse(); AgentChatResponse response = new AgentChatResponse();
response.setAgentId(agent.getId()); response.setAgentId(agent.getId());
response.setAgentCode(agent.getAgentCode()); response.setAgentCode(agent.getAgentCode());
response.setAgentName(agent.getAgentName()); response.setAgentName(agent.getAgentName());
response.setStoreId(agent.getStoreId()); response.setStoreId(agent.getStoreId());
response.setStoreName(store.getStoreName()); response.setStoreName(store.getStoreName());
response.setSessionId(session.getId());
response.setSessionCode(session.getSessionCode());
response.setAnswer(chatResult.getContent()); response.setAnswer(chatResult.getContent());
response.setModelRequestId(chatResult.getCallLog().getRequestId()); response.setModelRequestId(resolveRequestId(chatResult));
response.setReferences(toReferenceChunks(recalls)); response.setReferences(toReferenceChunks(recalls));
log.info("Agent兼容聊天结束agentId={}, sessionId={}, requestId={}, referenceCount={}",
agentId, session.getId(), resolveRequestId(chatResult), recalls.size());
return response; return response;
} }
@@ -272,6 +291,105 @@ public class AgentDefinitionServiceImpl extends ServiceImpl<AgentDefinitionMappe
}).toList(); }).toList();
} }
/**
* 兼容旧调试接口时,统一创建独立会话并沉淀会话编码,便于工作台后续追踪完整对话。
*/
private AgentSession createCompatibilitySession(Long agentId, AgentChatRequest request) {
if (agentSessionService == null) {
AgentSession session = new AgentSession();
session.setId(0L);
session.setAgentId(agentId);
session.setSessionCode("chat_mock");
session.setTitle(resolveLatestUserMessage(request.getMessages()));
session.setStatus("ACTIVE");
session.setMetadataJson("{\"source\":\"compat_chat\"}");
session.setRemark("兼容聊天接口自动创建");
return session;
}
AgentSessionCreateDTO createDTO = new AgentSessionCreateDTO();
createDTO.setAgentId(agentId);
createDTO.setSessionCode("chat_" + UUID.randomUUID().toString().replace("-", ""));
createDTO.setTitle(resolveLatestUserMessage(request.getMessages()));
createDTO.setMetadataJson("{\"source\":\"compat_chat\"}");
createDTO.setRemark("兼容聊天接口自动创建");
AgentSession session = agentSessionService.createSessionEntity(createDTO);
agentSessionService.save(session);
return session;
}
private void appendRequestMessages(Long sessionId, List<AgentChatRequest.AgentMessage> messages) {
if (agentMessageService == null) {
return;
}
for (AgentChatRequest.AgentMessage rawMessage : messages) {
if (rawMessage == null || !StringUtils.hasText(rawMessage.getContent())) {
continue;
}
AgentSessionMessageCreateDTO createDTO = new AgentSessionMessageCreateDTO();
createDTO.setSessionId(sessionId);
createDTO.setRole(normalizeRole(rawMessage.getRole()));
createDTO.setContent(rawMessage.getContent().trim());
createDTO.setCitationJson("[]");
createDTO.setRemark("兼容聊天接口消息");
AgentMessage entity = agentMessageService.appendMessageEntity(createDTO);
agentMessageService.save(entity);
}
}
private AgentMessage appendAssistantMessage(Long sessionId,
List<RagChunkRecallResponse> recalls,
ChatResult chatResult) {
if (agentMessageService == null) {
AgentMessage message = new AgentMessage();
message.setSessionId(sessionId);
message.setRole("assistant");
message.setContent(chatResult.getContent());
message.setCitationJson(toCitationJson(recalls));
message.setTokenCount(chatResult.getCallLog() == null ? null : chatResult.getCallLog().getTotalTokens());
message.setRemark("兼容聊天接口模型回复");
return message;
}
AgentSessionMessageCreateDTO createDTO = new AgentSessionMessageCreateDTO();
createDTO.setSessionId(sessionId);
createDTO.setRole("assistant");
createDTO.setContent(chatResult.getContent());
createDTO.setCitationJson(toCitationJson(recalls));
ModelCallLog callLog = chatResult.getCallLog();
createDTO.setRequestId(callLog == null ? null : callLog.getRequestId());
createDTO.setTokenCount(callLog == null ? null : callLog.getTotalTokens());
createDTO.setRemark("兼容聊天接口模型回复");
AgentMessage entity = agentMessageService.appendMessageEntity(createDTO);
agentMessageService.save(entity);
return entity;
}
private String resolveRequestId(ChatResult chatResult) {
ModelCallLog callLog = chatResult == null ? null : chatResult.getCallLog();
return callLog == null ? null : callLog.getRequestId();
}
private String toCitationJson(List<RagChunkRecallResponse> recalls) {
if (recalls == null || recalls.isEmpty()) {
return "[]";
}
StringBuilder builder = new StringBuilder("[");
for (int i = 0; i < recalls.size(); i++) {
RagChunkRecallResponse recall = recalls.get(i);
if (i > 0) {
builder.append(',');
}
builder.append("{\"chunkId\":")
.append(recall.getChunkId())
.append(",\"documentId\":")
.append(recall.getDocumentId())
.append(",\"score\":")
.append(recall.getScore() == null ? 0D : recall.getScore())
.append('}');
}
builder.append(']');
return builder.toString();
}
private String normalizeRole(String role) { private String normalizeRole(String role) {
if (!StringUtils.hasText(role)) { if (!StringUtils.hasText(role)) {
return "user"; return "user";

View File

@@ -0,0 +1,93 @@
package com.bruce.agent.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.agent.dto.request.AgentSessionMessageCreateDTO;
import com.bruce.agent.entity.AgentMessage;
import com.bruce.agent.entity.AgentSession;
import com.bruce.agent.factory.AgentMessageFactory;
import com.bruce.agent.mapper.AgentMessageMapper;
import com.bruce.agent.service.IAgentMessageService;
import com.bruce.agent.service.IAgentSessionService;
import com.bruce.agent.vo.AgentMessageVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class AgentMessageServiceImpl extends ServiceImpl<AgentMessageMapper, AgentMessage> implements IAgentMessageService {
private final IAgentSessionService agentSessionService;
private final AgentMessageFactory agentMessageFactory;
@Override
public boolean appendMessage(AgentSessionMessageCreateDTO request) {
AgentMessage entity = appendMessageEntity(request);
boolean saved = save(entity);
log.info("Agent消息写入完成sessionId={}, role={}, messageId={}, requestId={}, result={}",
entity.getSessionId(), entity.getRole(), entity.getId(), request.getRequestId(), saved);
return saved;
}
@Override
public AgentMessage appendMessageEntity(AgentSessionMessageCreateDTO request) {
validateRequest(request);
AgentSession session = loadSession(request.getSessionId());
if ("CLOSED".equals(session.getStatus())) {
throw new IllegalArgumentException("会话已关闭,不能继续写入消息");
}
AgentMessage entity = loadFactory().toEntity(request);
if (!StringUtils.hasText(entity.getCitationJson())) {
entity.setCitationJson("[]");
} else if (!entity.getCitationJson().trim().startsWith("[")) {
throw new IllegalArgumentException("citationJson必须是数组结构");
}
log.info("Agent消息写入开始sessionId={}, role={}, requestId={}",
entity.getSessionId(), entity.getRole(), request.getRequestId());
return entity;
}
@Override
public List<AgentMessageVO> listBySessionId(Long sessionId) {
if (sessionId == null) {
throw new IllegalArgumentException("会话ID不能为空");
}
List<AgentMessage> messages = lambdaQuery()
.eq(AgentMessage::getSessionId, sessionId)
.orderByAsc(AgentMessage::getCreateTime)
.orderByAsc(AgentMessage::getId)
.list();
return loadFactory().toVOList(messages);
}
public AgentSession loadSession(Long sessionId) {
AgentSession session = agentSessionService.getById(sessionId);
if (session == null) {
throw new IllegalArgumentException("会话不存在ID: " + sessionId);
}
return session;
}
private void validateRequest(AgentSessionMessageCreateDTO request) {
if (request == null) {
throw new IllegalArgumentException("消息请求不能为空");
}
if (request.getSessionId() == null) {
throw new IllegalArgumentException("会话ID不能为空");
}
if (!StringUtils.hasText(request.getRole())) {
throw new IllegalArgumentException("消息角色不能为空");
}
if (!StringUtils.hasText(request.getContent())) {
throw new IllegalArgumentException("消息内容不能为空");
}
}
private AgentMessageFactory loadFactory() {
return agentMessageFactory == null ? new AgentMessageFactory() : agentMessageFactory;
}
}

View File

@@ -0,0 +1,120 @@
package com.bruce.agent.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.agent.dto.request.AgentSessionCreateDTO;
import com.bruce.agent.entity.AgentDefinition;
import com.bruce.agent.entity.AgentSession;
import com.bruce.agent.factory.AgentSessionFactory;
import com.bruce.agent.mapper.AgentSessionMapper;
import com.bruce.agent.service.IAgentDefinitionService;
import com.bruce.agent.service.IAgentSessionService;
import com.bruce.agent.vo.AgentSessionDetailVO;
import com.bruce.common.enums.EnableStatusEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class AgentSessionServiceImpl extends ServiceImpl<AgentSessionMapper, AgentSession> implements IAgentSessionService {
private static final String SESSION_STATUS_ACTIVE = "ACTIVE";
private static final String SESSION_STATUS_CLOSED = "CLOSED";
private final IAgentDefinitionService agentDefinitionService;
private final AgentSessionFactory agentSessionFactory;
@Override
public boolean createSession(AgentSessionCreateDTO request) {
AgentSession entity = createSessionEntity(request);
boolean saved = save(entity);
log.info("Agent会话创建完成agentId={}, sessionCode={}, sessionId={}, result={}",
entity.getAgentId(), entity.getSessionCode(), entity.getId(), saved);
return saved;
}
@Override
public AgentSession createSessionEntity(AgentSessionCreateDTO request) {
validateCreateRequest(request);
AgentDefinition agent = agentDefinitionService.getById(request.getAgentId());
if (agent == null) {
throw new IllegalArgumentException("Agent不存在ID: " + request.getAgentId());
}
if (!EnableStatusEnum.ENABLED.name().equals(agent.getStatus())) {
throw new IllegalArgumentException("Agent已停用无法创建会话");
}
if (baseMapper != null) {
AgentSession duplicate = lambdaQuery()
.eq(AgentSession::getSessionCode, request.getSessionCode().trim())
.one();
if (duplicate != null) {
throw new IllegalArgumentException("会话编码已存在: " + request.getSessionCode().trim());
}
}
AgentSession entity = loadFactory().toEntity(request);
entity.setStatus(SESSION_STATUS_ACTIVE);
if (!StringUtils.hasText(entity.getMetadataJson())) {
entity.setMetadataJson("{}");
}
log.info("Agent会话创建开始agentId={}, sessionCode={}, workflowRunId={}",
entity.getAgentId(), entity.getSessionCode(), entity.getWorkflowRunId());
return entity;
}
@Override
public List<AgentSessionDetailVO> listByAgentId(Long agentId) {
if (agentId == null) {
throw new IllegalArgumentException("Agent ID不能为空");
}
List<AgentSession> sessions = lambdaQuery()
.eq(AgentSession::getAgentId, agentId)
.orderByDesc(AgentSession::getUpdateTime)
.orderByDesc(AgentSession::getId)
.list();
return loadFactory().toDetailVOList(sessions);
}
@Override
public AgentSessionDetailVO getDetailById(Long sessionId) {
if (sessionId == null) {
throw new IllegalArgumentException("会话ID不能为空");
}
return loadFactory().toDetailVO(getById(sessionId));
}
@Override
public boolean closeSession(Long sessionId) {
if (sessionId == null) {
throw new IllegalArgumentException("会话ID不能为空");
}
AgentSession session = getById(sessionId);
if (session == null) {
throw new IllegalArgumentException("会话不存在ID: " + sessionId);
}
session.setStatus(SESSION_STATUS_CLOSED);
boolean updated = updateById(session);
log.info("Agent会话关闭完成sessionId={}, sessionCode={}, result={}",
session.getId(), session.getSessionCode(), updated);
return updated;
}
private void validateCreateRequest(AgentSessionCreateDTO request) {
if (request == null) {
throw new IllegalArgumentException("创建会话请求不能为空");
}
if (request.getAgentId() == null) {
throw new IllegalArgumentException("Agent ID不能为空");
}
if (!StringUtils.hasText(request.getSessionCode())) {
throw new IllegalArgumentException("会话编码不能为空");
}
}
private AgentSessionFactory loadFactory() {
return agentSessionFactory == null ? new AgentSessionFactory() : agentSessionFactory;
}
}

View File

@@ -0,0 +1,91 @@
package com.bruce.agent.service.impl;
import com.bruce.agent.entity.AgentDefinition;
import com.bruce.agent.service.IAgentDefinitionService;
import com.bruce.agent.service.IAgentMessageService;
import com.bruce.agent.service.IAgentSessionService;
import com.bruce.agent.service.IAgentWorkspaceService;
import com.bruce.agent.vo.AgentMessageVO;
import com.bruce.agent.vo.AgentSessionDetailVO;
import com.bruce.agent.vo.AgentWorkspaceVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Objects;
@Slf4j
@Service
@RequiredArgsConstructor
public class AgentWorkspaceServiceImpl implements IAgentWorkspaceService {
private final IAgentDefinitionService agentDefinitionService;
private final IAgentSessionService agentSessionService;
private final IAgentMessageService agentMessageService;
@Override
public AgentWorkspaceVO getWorkspace(Long agentId, Long sessionId) {
log.info("Agent工作台查询开始agentId={}, sessionId={}", agentId, sessionId);
if (agentId == null) {
throw new IllegalArgumentException("Agent ID不能为空");
}
AgentDefinition agent = agentDefinitionService.getById(agentId);
if (agent == null) {
throw new IllegalArgumentException("Agent不存在ID: " + agentId);
}
List<AgentSessionDetailVO> sessions = agentSessionService.listByAgentId(agentId);
AgentSessionDetailVO currentSession = resolveSession(sessionId, sessions);
List<AgentMessageVO> messages = currentSession == null ? List.of() : agentMessageService.listBySessionId(currentSession.getId());
AgentWorkspaceVO workspace = new AgentWorkspaceVO();
workspace.setAgentId(agent.getId());
workspace.setAgentCode(agent.getAgentCode());
workspace.setAgentName(agent.getAgentName());
workspace.setStoreId(agent.getStoreId());
workspace.setStatus(agent.getStatus());
workspace.setSessions(sessions);
workspace.setMessages(messages);
if (currentSession != null) {
workspace.setSessionId(currentSession.getId());
workspace.setSessionCode(currentSession.getSessionCode());
workspace.setSessionTitle(currentSession.getTitle());
workspace.setSessionStatus(currentSession.getStatus());
workspace.setWorkflowRunId(currentSession.getWorkflowRunId());
}
int totalTokens = messages.stream()
.map(AgentMessageVO::getTokenCount)
.filter(Objects::nonNull)
.mapToInt(Integer::intValue)
.sum();
int citationCount = (int) messages.stream()
.filter(message -> StringUtils.hasText(message.getCitationJson()) && message.getCitationJson().contains("chunkId"))
.count();
String latestRequestId = messages.stream()
.map(AgentMessageVO::getRequestId)
.filter(StringUtils::hasText)
.reduce((first, second) -> second)
.orElse(null);
workspace.setTotalTokens(totalTokens);
workspace.setCitationCount(citationCount);
workspace.setLatestRequestId(latestRequestId);
log.info("Agent工作台查询结束agentId={}, sessionId={}, messageCount={}, totalTokens={}",
agentId, workspace.getSessionId(), messages.size(), totalTokens);
return workspace;
}
private AgentSessionDetailVO resolveSession(Long sessionId, List<AgentSessionDetailVO> sessions) {
if (sessions == null || sessions.isEmpty()) {
return null;
}
if (sessionId == null) {
return sessions.get(0);
}
return sessions.stream().filter(item -> sessionId.equals(item.getId())).findFirst().orElse(null);
}
}

View File

@@ -0,0 +1,18 @@
package com.bruce.agent.vo;
import lombok.Data;
import java.util.Date;
@Data
public class AgentMessageVO {
private Long id;
private Long sessionId;
private String role;
private String content;
private String citationJson;
private Integer tokenCount;
private String requestId;
private String remark;
private Date createTime;
}

View File

@@ -0,0 +1,19 @@
package com.bruce.agent.vo;
import lombok.Data;
import java.util.Date;
@Data
public class AgentSessionDetailVO {
private Long id;
private Long agentId;
private String sessionCode;
private Long workflowRunId;
private String title;
private String status;
private String metadataJson;
private String remark;
private Date createTime;
private Date updateTime;
}

View File

@@ -0,0 +1,24 @@
package com.bruce.agent.vo;
import lombok.Data;
import java.util.List;
@Data
public class AgentWorkspaceVO {
private Long agentId;
private String agentCode;
private String agentName;
private Long storeId;
private String status;
private Long sessionId;
private String sessionCode;
private String sessionTitle;
private String sessionStatus;
private Long workflowRunId;
private Integer totalTokens;
private Integer citationCount;
private String latestRequestId;
private List<AgentSessionDetailVO> sessions;
private List<AgentMessageVO> messages;
}

View File

@@ -0,0 +1,86 @@
package com.bruce.agent.factory;
import com.bruce.agent.dto.request.AgentDefinitionSaveRequest;
import com.bruce.agent.dto.request.AgentSessionCreateDTO;
import com.bruce.agent.dto.request.AgentSessionMessageCreateDTO;
import com.bruce.agent.dto.response.AgentDefinitionResponse;
import com.bruce.agent.entity.AgentDefinition;
import com.bruce.agent.entity.AgentMessage;
import com.bruce.agent.entity.AgentSession;
import com.bruce.agent.vo.AgentSessionDetailVO;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class AgentFactoryTests {
private final AgentDefinitionFactory agentDefinitionFactory = new AgentDefinitionFactory();
private final AgentSessionFactory agentSessionFactory = new AgentSessionFactory();
private final AgentMessageFactory agentMessageFactory = new AgentMessageFactory();
@Test
void agentDefinitionFactoryShouldTrimRequestAndBuildResponse() {
AgentDefinitionSaveRequest request = new AgentDefinitionSaveRequest();
request.setId(1L);
request.setAgentCode(" AGENT_RAG_HELPER ");
request.setAgentName(" 知识问答助手 ");
request.setSystemPrompt(" 你是企业知识助手 ");
request.setStoreId(1001L);
request.setStatus(" ENABLED ");
request.setRemark(" 默认Agent ");
AgentDefinition entity = agentDefinitionFactory.toEntity(request);
assertEquals("AGENT_RAG_HELPER", entity.getAgentCode());
assertEquals("知识问答助手", entity.getAgentName());
assertEquals("你是企业知识助手", entity.getSystemPrompt());
assertEquals("ENABLED", entity.getStatus());
assertEquals("默认Agent", entity.getRemark());
AgentDefinitionResponse response = agentDefinitionFactory.toResponse(entity);
assertEquals(1001L, response.getStoreId());
assertEquals("AGENT_RAG_HELPER", response.getAgentCode());
}
@Test
void agentSessionFactoryShouldBuildSessionEntityAndDetailView() {
AgentSessionCreateDTO request = new AgentSessionCreateDTO();
request.setAgentId(1L);
request.setSessionCode(" session_001 ");
request.setWorkflowRunId(2001L);
request.setTitle(" 产品问答会话 ");
request.setMetadataJson(" {\"source\":\"debug\"} ");
request.setRemark(" 首轮调试 ");
AgentSession entity = agentSessionFactory.toEntity(request);
assertNotNull(entity);
assertEquals("session_001", entity.getSessionCode());
assertEquals("产品问答会话", entity.getTitle());
assertEquals("{\"source\":\"debug\"}", entity.getMetadataJson());
assertEquals("首轮调试", entity.getRemark());
AgentSessionDetailVO detailVO = agentSessionFactory.toDetailVO(entity);
assertEquals(1L, detailVO.getAgentId());
assertEquals("session_001", detailVO.getSessionCode());
}
@Test
void agentMessageFactoryShouldBuildMessageEntityAndPreserveCitationJson() {
AgentSessionMessageCreateDTO request = new AgentSessionMessageCreateDTO();
request.setSessionId(10L);
request.setRole(" assistant ");
request.setContent(" 这里是回答内容 ");
request.setCitationJson(" [{\"chunkId\":1}] ");
request.setTokenCount(256);
request.setRemark(" 回答成功 ");
AgentMessage entity = agentMessageFactory.toEntity(request);
assertEquals(10L, entity.getSessionId());
assertEquals("assistant", entity.getRole());
assertEquals("这里是回答内容", entity.getContent());
assertEquals("[{\"chunkId\":1}]", entity.getCitationJson());
assertEquals(256, entity.getTokenCount());
assertTrue(entity.getRemark().contains("回答成功"));
}
}

View File

@@ -0,0 +1,119 @@
package com.bruce.agent.session;
import com.bruce.agent.dto.request.AgentSessionCreateDTO;
import com.bruce.agent.dto.request.AgentSessionMessageCreateDTO;
import com.bruce.agent.entity.AgentDefinition;
import com.bruce.agent.entity.AgentMessage;
import com.bruce.agent.entity.AgentSession;
import com.bruce.agent.service.IAgentDefinitionService;
import com.bruce.agent.service.impl.AgentMessageServiceImpl;
import com.bruce.agent.service.impl.AgentSessionServiceImpl;
import com.bruce.common.enums.EnableStatusEnum;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class AgentSessionServiceTests {
@Mock
private IAgentDefinitionService agentDefinitionService;
@Spy
@InjectMocks
private AgentSessionServiceImpl agentSessionService;
@Spy
@InjectMocks
private AgentMessageServiceImpl agentMessageService;
@Test
void createSessionShouldRejectDisabledAgent() {
AgentDefinition agent = new AgentDefinition();
agent.setId(1L);
agent.setStatus("DISABLED");
when(agentDefinitionService.getById(1L)).thenReturn(agent);
AgentSessionCreateDTO request = new AgentSessionCreateDTO();
request.setAgentId(1L);
request.setSessionCode("session_001");
assertThrows(IllegalArgumentException.class, () -> agentSessionService.createSession(request));
}
@Test
void createSessionShouldPersistActiveSession() {
AgentDefinition agent = new AgentDefinition();
agent.setId(1L);
agent.setStatus(EnableStatusEnum.ENABLED.name());
when(agentDefinitionService.getById(1L)).thenReturn(agent);
doAnswer(invocation -> true).when(agentSessionService).save(any(AgentSession.class));
AgentSessionCreateDTO request = new AgentSessionCreateDTO();
request.setAgentId(1L);
request.setSessionCode("session_001");
request.setTitle("产品问答");
request.setMetadataJson("{\"source\":\"debug\"}");
boolean result = agentSessionService.createSession(request);
assertTrue(result);
ArgumentCaptor<AgentSession> captor = ArgumentCaptor.forClass(AgentSession.class);
verify(agentSessionService).save(captor.capture());
assertEquals("session_001", captor.getValue().getSessionCode());
assertEquals("ACTIVE", captor.getValue().getStatus());
}
@Test
void appendMessageShouldRejectClosedSession() {
AgentSession session = new AgentSession();
session.setId(10L);
session.setStatus("CLOSED");
doReturn(session).when(agentMessageService).loadSession(10L);
AgentSessionMessageCreateDTO request = new AgentSessionMessageCreateDTO();
request.setSessionId(10L);
request.setRole("user");
request.setContent("你好");
request.setCitationJson("[]");
assertThrows(IllegalArgumentException.class, () -> agentMessageService.appendMessage(request));
}
@Test
void appendMessageShouldPersistCitationJson() {
AgentSession session = new AgentSession();
session.setId(10L);
session.setStatus("ACTIVE");
doReturn(session).when(agentMessageService).loadSession(10L);
doAnswer(invocation -> true).when(agentMessageService).save(any(AgentMessage.class));
AgentSessionMessageCreateDTO request = new AgentSessionMessageCreateDTO();
request.setSessionId(10L);
request.setRole("assistant");
request.setContent("这里是回答");
request.setCitationJson("[{\"chunkId\":1}]");
request.setTokenCount(128);
boolean result = agentMessageService.appendMessage(request);
assertTrue(result);
ArgumentCaptor<AgentMessage> captor = ArgumentCaptor.forClass(AgentMessage.class);
verify(agentMessageService).save(captor.capture());
assertEquals("[{\"chunkId\":1}]", captor.getValue().getCitationJson());
assertEquals(128, captor.getValue().getTokenCount());
}
}

View File

@@ -0,0 +1,81 @@
package com.bruce.agent.workspace;
import com.bruce.agent.entity.AgentDefinition;
import com.bruce.agent.service.IAgentDefinitionService;
import com.bruce.agent.service.IAgentMessageService;
import com.bruce.agent.service.IAgentSessionService;
import com.bruce.agent.service.impl.AgentWorkspaceServiceImpl;
import com.bruce.agent.vo.AgentMessageVO;
import com.bruce.agent.vo.AgentSessionDetailVO;
import com.bruce.agent.vo.AgentWorkspaceVO;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class AgentWorkspaceServiceTests {
@Mock
private IAgentDefinitionService agentDefinitionService;
@Mock
private IAgentSessionService agentSessionService;
@Mock
private IAgentMessageService agentMessageService;
@InjectMocks
private AgentWorkspaceServiceImpl agentWorkspaceService;
@Test
void getWorkspaceShouldAggregateAgentSessionAndMessages() {
AgentDefinition agent = new AgentDefinition();
agent.setId(1L);
agent.setAgentCode("AGENT_RAG_HELPER");
agent.setAgentName("知识问答助手");
agent.setStoreId(1001L);
agent.setStatus("ENABLED");
AgentSessionDetailVO session = new AgentSessionDetailVO();
session.setId(10L);
session.setAgentId(1L);
session.setSessionCode("session_001");
session.setStatus("ACTIVE");
session.setTitle("产品问答");
session.setWorkflowRunId(2001L);
AgentMessageVO userMessage = new AgentMessageVO();
userMessage.setRole("user");
userMessage.setContent("产品支持哪些模型?");
AgentMessageVO assistantMessage = new AgentMessageVO();
assistantMessage.setRole("assistant");
assistantMessage.setContent("当前支持 OpenAI Compatible 协议模型。");
assistantMessage.setCitationJson("[{\"chunkId\":1}]");
assistantMessage.setTokenCount(256);
assistantMessage.setRequestId("req-001");
when(agentDefinitionService.getById(1L)).thenReturn(agent);
when(agentSessionService.listByAgentId(1L)).thenReturn(List.of(session));
when(agentMessageService.listBySessionId(10L)).thenReturn(List.of(userMessage, assistantMessage));
AgentWorkspaceVO workspace = agentWorkspaceService.getWorkspace(1L, 10L);
assertNotNull(workspace);
assertEquals("AGENT_RAG_HELPER", workspace.getAgentCode());
assertEquals("知识问答助手", workspace.getAgentName());
assertEquals(10L, workspace.getSessionId());
assertEquals("session_001", workspace.getSessionCode());
assertEquals(2, workspace.getMessages().size());
assertEquals(1, workspace.getCitationCount());
assertEquals(256, workspace.getTotalTokens());
assertEquals("req-001", workspace.getLatestRequestId());
}
}

View File

@@ -1,9 +1,14 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { import {
appendAgentMessage,
chatWithAgent, chatWithAgent,
createAgentSession,
deleteAgent, deleteAgent,
getAgentById, getAgentById,
getAgentSessionById,
getAgentWorkspace,
listAgentMessages,
listAgents, listAgents,
queryAgents, queryAgents,
saveAgent, saveAgent,
@@ -27,6 +32,11 @@ describe('agent api', () => {
saveAgent({ agentCode: 'agent_1', agentName: 'Agent 1', storeId: '2001', status: 'ENABLED' }); saveAgent({ agentCode: 'agent_1', agentName: 'Agent 1', storeId: '2001', status: 'ENABLED' });
deleteAgent('1001'); deleteAgent('1001');
chatWithAgent('1001', { messages: [{ role: 'user', content: '你好' }] }); chatWithAgent('1001', { messages: [{ role: 'user', content: '你好' }] });
createAgentSession({ agentId: '1001', sessionCode: 'session_001' });
getAgentSessionById('session-id');
listAgentMessages('session-id');
appendAgentMessage('session-id', { role: 'assistant', content: '收到' });
getAgentWorkspace('1001', 'session-id');
expect(post).toHaveBeenCalledWith('/agents/list'); expect(post).toHaveBeenCalledWith('/agents/list');
expect(post).toHaveBeenCalledWith('/agents/query', { agentCode: 'demo' }); expect(post).toHaveBeenCalledWith('/agents/query', { agentCode: 'demo' });
@@ -39,5 +49,10 @@ describe('agent api', () => {
}); });
expect(post).toHaveBeenCalledWith('/agents/delete', undefined, { params: { id: '1001' } }); expect(post).toHaveBeenCalledWith('/agents/delete', undefined, { params: { id: '1001' } });
expect(post).toHaveBeenCalledWith('/agents/1001/chat', { messages: [{ role: 'user', content: '你好' }] }); expect(post).toHaveBeenCalledWith('/agents/1001/chat', { messages: [{ role: 'user', content: '你好' }] });
expect(post).toHaveBeenCalledWith('/agent-sessions/create', { agentId: '1001', sessionCode: 'session_001' });
expect(get).toHaveBeenCalledWith('/agent-sessions/detail', { params: { id: 'session-id' } });
expect(get).toHaveBeenCalledWith('/agent-sessions/session-id/messages');
expect(post).toHaveBeenCalledWith('/agent-sessions/session-id/messages', { role: 'assistant', content: '收到' });
expect(get).toHaveBeenCalledWith('/agent-sessions/workspace', { params: { agentId: '1001', sessionId: 'session-id' } });
}); });
}); });

View File

@@ -40,11 +40,53 @@ export interface AgentChatResponse {
agentName: string; agentName: string;
storeId: string; storeId: string;
storeName?: string; storeName?: string;
sessionId?: string;
sessionCode?: string;
answer: string; answer: string;
modelRequestId: string; modelRequestId: string;
references: AgentReferenceChunk[]; references: AgentReferenceChunk[];
} }
export interface AgentSession {
id?: string;
agentId: string;
sessionCode: string;
workflowRunId?: string;
title?: string;
status?: string;
metadataJson?: string;
remark?: string;
}
export interface AgentMessageRecord {
id?: string;
sessionId: string;
role: 'system' | 'user' | 'assistant';
content: string;
citationJson?: string;
tokenCount?: number;
requestId?: string;
remark?: string;
}
export interface AgentWorkspace {
agentId: string;
agentCode: string;
agentName: string;
storeId: string;
status: string;
sessionId?: string;
sessionCode?: string;
sessionTitle?: string;
sessionStatus?: string;
workflowRunId?: string;
totalTokens?: number;
citationCount?: number;
latestRequestId?: string;
sessions: AgentSession[];
messages: AgentMessageRecord[];
}
export function listAgents() { export function listAgents() {
return post<AgentDefinition[]>('/agents/list'); return post<AgentDefinition[]>('/agents/list');
} }
@@ -68,3 +110,28 @@ export function deleteAgent(id: string) {
export function chatWithAgent(agentId: string, data: AgentChatRequest) { export function chatWithAgent(agentId: string, data: AgentChatRequest) {
return post<AgentChatResponse, AgentChatRequest>(`/agents/${agentId}/chat`, data); return post<AgentChatResponse, AgentChatRequest>(`/agents/${agentId}/chat`, data);
} }
export function createAgentSession(data: AgentSession) {
return post<boolean, AgentSession>('/agent-sessions/create', data);
}
export function getAgentSessionById(id: string) {
return get<AgentSession>('/agent-sessions/detail', { params: { id } });
}
export function listAgentMessages(sessionId: string) {
return get<AgentMessageRecord[]>(`/agent-sessions/${sessionId}/messages`);
}
export function appendAgentMessage(sessionId: string, data: Omit<AgentMessageRecord, 'sessionId'>) {
return post<boolean, Omit<AgentMessageRecord, 'sessionId'>>(`/agent-sessions/${sessionId}/messages`, data);
}
export function getAgentWorkspace(agentId: string, sessionId?: string) {
return get<AgentWorkspace>('/agent-sessions/workspace', {
params: {
agentId,
sessionId,
},
});
}