feat: 先进智能体功能上线

- 基于 agent-runtime 打造,默认 ReAct agent
- 支持 agent 能力对接,已对接工作流、插件、知识库等 tool 能力
- 全新 agent 编排界面,支持可视化便捷配置 agent
- 全新 agent 聊天界面,支持快捷操作、额外知识库选择等
This commit is contained in:
2026-05-28 11:29:18 +08:00
parent 11e595b088
commit 1c205c3720
39 changed files with 3546 additions and 217 deletions

View File

@@ -0,0 +1,53 @@
package tech.easyflow.agent.runtime;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
/**
* Agent 聊天临时能力请求项。
*/
public class AgentChatCapability implements Serializable {
private static final long serialVersionUID = 1L;
private String type;
private List<BigInteger> resourceIds = new ArrayList<>();
/**
* 获取能力类型。
*
* @return 能力类型
*/
public String getType() {
return type;
}
/**
* 设置能力类型。
*
* @param type 能力类型
*/
public void setType(String type) {
this.type = type;
}
/**
* 获取资源 ID 列表。
*
* @return 资源 ID 列表
*/
public List<BigInteger> getResourceIds() {
return resourceIds;
}
/**
* 设置资源 ID 列表。
*
* @param resourceIds 资源 ID 列表
*/
public void setResourceIds(List<BigInteger> resourceIds) {
this.resourceIds = resourceIds == null ? new ArrayList<>() : new ArrayList<>(resourceIds);
}
}

View File

@@ -0,0 +1,171 @@
package tech.easyflow.agent.runtime;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.service.ResourceAccessService;
import java.math.BigInteger;
import java.util.*;
/**
* Agent 聊天临时能力编排服务。
*/
@Service
public class AgentChatCapabilityService {
private static final String KNOWLEDGE_CAPABILITY_TYPE = "KNOWLEDGE";
private static final String DEFAULT_RETRIEVAL_MODE = "HYBRID";
private static final int MAX_EXTRA_KNOWLEDGE_COUNT = 3;
private final DocumentCollectionService documentCollectionService;
private final ResourceAccessService resourceAccessService;
private final ObjectMapper objectMapper;
/**
* 创建 Agent 聊天临时能力编排服务。
*
* @param documentCollectionService 知识库服务
* @param resourceAccessService 资源访问服务
* @param objectMapper 对象复制工具
*/
public AgentChatCapabilityService(DocumentCollectionService documentCollectionService,
ResourceAccessService resourceAccessService,
ObjectMapper objectMapper) {
this.documentCollectionService = documentCollectionService;
this.resourceAccessService = resourceAccessService;
this.objectMapper = objectMapper;
}
/**
* 将临时聊天能力合并到运行时 Agent 定义中。
*
* @param agent 已发布 Agent 运行视图
* @param capabilities 临时能力请求
* @param account 当前登录账号
* @return 能力解析结果
*/
public AgentChatCapabilityResolution apply(Agent agent,
List<AgentChatCapability> capabilities,
LoginAccount account) {
List<BigInteger> extraKnowledgeIds = resolveKnowledgeIds(capabilities);
boolean knowledgeCapabilityProvided = hasKnowledgeCapability(capabilities);
if (agent == null || extraKnowledgeIds.isEmpty()) {
return new AgentChatCapabilityResolution(agent, extraKnowledgeIds, knowledgeCapabilityProvided);
}
Agent runtimeAgent = objectMapper.convertValue(agent, Agent.class);
List<AgentKnowledgeBinding> mergedBindings = new ArrayList<>();
Set<BigInteger> existingKnowledgeIds = new LinkedHashSet<>();
if (runtimeAgent.getKnowledgeBindings() != null) {
for (AgentKnowledgeBinding binding : runtimeAgent.getKnowledgeBindings()) {
if (binding == null) {
continue;
}
mergedBindings.add(binding);
if (Boolean.TRUE.equals(binding.getEnabled()) && binding.getKnowledgeId() != null) {
existingKnowledgeIds.add(binding.getKnowledgeId());
}
}
}
int sortNo = mergedBindings.size();
for (BigInteger knowledgeId : extraKnowledgeIds) {
if (existingKnowledgeIds.contains(knowledgeId)) {
continue;
}
DocumentCollection knowledge = documentCollectionService.getById(knowledgeId);
validateKnowledge(knowledge);
resourceAccessService.assertAccess(
CategoryResourceType.KNOWLEDGE,
knowledge,
ResourceAction.USE,
"无权限使用所选知识库"
);
AgentKnowledgeBinding binding = new AgentKnowledgeBinding();
binding.setTenantId(account == null ? runtimeAgent.getTenantId() : account.getTenantId());
binding.setAgentId(runtimeAgent.getId());
binding.setKnowledgeId(knowledgeId);
binding.setRetrievalMode(DEFAULT_RETRIEVAL_MODE);
binding.setEnabled(true);
binding.setSortNo(sortNo++);
mergedBindings.add(binding);
existingKnowledgeIds.add(knowledgeId);
}
runtimeAgent.setKnowledgeBindings(mergedBindings);
return new AgentChatCapabilityResolution(runtimeAgent, extraKnowledgeIds, knowledgeCapabilityProvided);
}
/**
* 从能力列表提取知识库 ID。
*
* @param capabilities 临时能力列表
* @return 已去重知识库 ID
*/
public List<BigInteger> resolveKnowledgeIds(List<AgentChatCapability> capabilities) {
if (capabilities == null || capabilities.isEmpty()) {
return List.of();
}
LinkedHashSet<BigInteger> ids = new LinkedHashSet<>();
for (AgentChatCapability capability : capabilities) {
if (capability == null || !isKnowledgeCapability(capability.getType())) {
continue;
}
if (capability.getResourceIds() == null) {
continue;
}
for (BigInteger resourceId : capability.getResourceIds()) {
if (resourceId != null) {
ids.add(resourceId);
}
}
}
if (ids.size() > MAX_EXTRA_KNOWLEDGE_COUNT) {
throw new BusinessException("临时知识库最多选择 3 个");
}
return new ArrayList<>(ids);
}
private boolean isKnowledgeCapability(String type) {
return Objects.equals(KNOWLEDGE_CAPABILITY_TYPE, type == null ? null : type.trim().toUpperCase());
}
private boolean hasKnowledgeCapability(List<AgentChatCapability> capabilities) {
if (capabilities == null || capabilities.isEmpty()) {
return false;
}
for (AgentChatCapability capability : capabilities) {
if (capability != null && isKnowledgeCapability(capability.getType())) {
return true;
}
}
return false;
}
private void validateKnowledge(DocumentCollection knowledge) {
if (knowledge == null) {
throw new BusinessException("所选知识库不存在");
}
if (PublishStatus.from(knowledge.getPublishStatus()) != PublishStatus.PUBLISHED) {
throw new BusinessException("所选知识库未发布,无法用于聊天");
}
}
/**
* Agent 聊天临时能力解析结果。
*
* @param agent 合并临时能力后的运行时 Agent
* @param extraKnowledgeIds 本次选择的临时知识库 ID
* @param knowledgeCapabilityProvided 请求是否显式传入知识库能力
*/
public record AgentChatCapabilityResolution(Agent agent,
List<BigInteger> extraKnowledgeIds,
boolean knowledgeCapabilityProvided) {
}
}

View File

@@ -1,6 +1,8 @@
package tech.easyflow.agent.runtime;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
/**
* Agent 管理端运行请求。
@@ -10,6 +12,7 @@ public class AgentChatRequest {
private BigInteger agentId;
private BigInteger sessionId;
private String prompt;
private List<AgentChatCapability> capabilities = new ArrayList<>();
/**
* 获取 Agent ID。
@@ -52,4 +55,22 @@ public class AgentChatRequest {
* @param prompt 用户输入
*/
public void setPrompt(String prompt) { this.prompt = prompt; }
/**
* 获取本次聊天启用的临时能力。
*
* @return 临时能力列表
*/
public List<AgentChatCapability> getCapabilities() {
return capabilities;
}
/**
* 设置本次聊天启用的临时能力。
*
* @param capabilities 临时能力列表
*/
public void setCapabilities(List<AgentChatCapability> capabilities) {
this.capabilities = capabilities == null ? new ArrayList<>() : new ArrayList<>(capabilities);
}
}

View File

@@ -70,6 +70,8 @@ public class AgentRunService {
@Resource
private AgentRuntimeFactory agentRuntimeFactory;
@Resource
private AgentChatCapabilityService agentChatCapabilityService;
@Resource
private AgentSessionStore agentSessionStore;
@Resource
private EasyFlowAgentSessionStore easyFlowAgentSessionStore;
@@ -121,10 +123,16 @@ public class AgentRunService {
ChatSessionSummary existingSession = resolveExistingSession(account, sessionId, chatRequest.getAgentId());
// 获取 Agent 发布快照
Agent agent = agentService.getPublishedView(chatRequest.getAgentId());
AgentChatCapabilityService.AgentChatCapabilityResolution capabilityResolution =
agentChatCapabilityService.apply(agent, chatRequest.getCapabilities(), account);
agent = capabilityResolution.agent();
String requestId = UUID.randomUUID().toString();
String traceId = UUID.randomUUID().toString();
// 组建会话上下文必要信息
ChatRuntimeContext chatContext = buildChatRuntimeContext(agent, sessionId, chatRequest.getPrompt(), account);
if (capabilityResolution.knowledgeCapabilityProvided()) {
chatContext.getExt().put(ChatRuntimeExtKeys.EXTRA_KNOWLEDGE_IDS, capabilityResolution.extraKnowledgeIds());
}
applyFormalSessionTitle(chatContext, chatRequest.getPrompt(), existingSession);
// 执行对话
return run(agent, chatRequest.getPrompt(), requestId, traceId, sessionId.toString(),

View File

@@ -0,0 +1,187 @@
package tech.easyflow.agent.runtime;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.permission.resource.VisibilityResource;
import tech.easyflow.system.service.ResourceAccessService;
import java.lang.reflect.Proxy;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Agent 聊天临时能力编排服务测试。
*/
public class AgentChatCapabilityServiceTest {
@Test
public void applyShouldAppendPublishedKnowledgeAndSkipBoundDuplicate() {
DocumentCollectionService documentService = documentService(
knowledge(1, PublishStatus.PUBLISHED),
knowledge(2, PublishStatus.PUBLISHED)
);
AgentChatCapabilityService service = new AgentChatCapabilityService(
documentService,
new AllowResourceAccessService(),
new ObjectMapper()
);
Agent agent = agentWithBoundKnowledge(1);
AgentChatCapabilityService.AgentChatCapabilityResolution resolution = service.apply(
agent,
List.of(capability(1, 2, 2)),
account()
);
List<AgentKnowledgeBinding> bindings = resolution.agent().getKnowledgeBindings();
Assert.assertEquals(List.of(BigInteger.ONE, BigInteger.valueOf(2)), resolution.extraKnowledgeIds());
Assert.assertEquals(2, bindings.size());
Assert.assertEquals(BigInteger.ONE, bindings.get(0).getKnowledgeId());
Assert.assertEquals(BigInteger.valueOf(2), bindings.get(1).getKnowledgeId());
Assert.assertEquals("HYBRID", bindings.get(1).getRetrievalMode());
Assert.assertTrue(bindings.get(1).getEnabled());
}
@Test
public void applyShouldAppendWhenBoundKnowledgeIsDisabled() {
DocumentCollectionService documentService = documentService(knowledge(2, PublishStatus.PUBLISHED));
AgentChatCapabilityService service = new AgentChatCapabilityService(
documentService,
new AllowResourceAccessService(),
new ObjectMapper()
);
Agent agent = agentWithBoundKnowledge(2);
agent.getKnowledgeBindings().get(0).setEnabled(false);
AgentChatCapabilityService.AgentChatCapabilityResolution resolution = service.apply(
agent,
List.of(capability(2)),
account()
);
List<AgentKnowledgeBinding> bindings = resolution.agent().getKnowledgeBindings();
Assert.assertEquals(2, bindings.size());
Assert.assertFalse(bindings.get(0).getEnabled());
Assert.assertTrue(bindings.get(1).getEnabled());
Assert.assertEquals(BigInteger.valueOf(2), bindings.get(1).getKnowledgeId());
}
@Test
public void resolveKnowledgeIdsShouldRejectTooManyItems() {
AgentChatCapabilityService service = new AgentChatCapabilityService(
documentService(),
new AllowResourceAccessService(),
new ObjectMapper()
);
Assert.assertThrows(BusinessException.class,
() -> service.resolveKnowledgeIds(List.of(capability(1, 2, 3, 4))));
}
@Test
public void applyShouldRejectUnpublishedKnowledge() {
DocumentCollectionService documentService = documentService(knowledge(2, PublishStatus.OFFLINE));
AgentChatCapabilityService service = new AgentChatCapabilityService(
documentService,
new AllowResourceAccessService(),
new ObjectMapper()
);
Assert.assertThrows(BusinessException.class,
() -> service.apply(agentWithBoundKnowledge(1), List.of(capability(2)), account()));
}
@Test
public void applyShouldRejectUnauthorizedKnowledge() {
DocumentCollectionService documentService = documentService(knowledge(2, PublishStatus.PUBLISHED));
AgentChatCapabilityService service = new AgentChatCapabilityService(
documentService,
new DenyResourceAccessService(),
new ObjectMapper()
);
Assert.assertThrows(BusinessException.class,
() -> service.apply(agentWithBoundKnowledge(1), List.of(capability(2)), account()));
}
private static Agent agentWithBoundKnowledge(int knowledgeId) {
Agent agent = new Agent();
agent.setId(BigInteger.TEN);
agent.setTenantId(BigInteger.ONE);
AgentKnowledgeBinding binding = new AgentKnowledgeBinding();
binding.setAgentId(agent.getId());
binding.setKnowledgeId(BigInteger.valueOf(knowledgeId));
binding.setRetrievalMode("HYBRID");
binding.setEnabled(true);
agent.setKnowledgeBindings(List.of(binding));
return agent;
}
private static AgentChatCapability capability(int... ids) {
AgentChatCapability capability = new AgentChatCapability();
capability.setType("KNOWLEDGE");
capability.setResourceIds(java.util.Arrays.stream(ids)
.mapToObj(BigInteger::valueOf)
.toList());
return capability;
}
private static DocumentCollection knowledge(int id, PublishStatus status) {
DocumentCollection collection = new DocumentCollection();
collection.setId(BigInteger.valueOf(id));
collection.setTitle("知识库" + id);
collection.setPublishStatus(status.name());
return collection;
}
private static LoginAccount account() {
LoginAccount account = new LoginAccount();
account.setId(BigInteger.valueOf(100));
account.setTenantId(BigInteger.ONE);
return account;
}
private static DocumentCollectionService documentService(DocumentCollection... collections) {
Map<BigInteger, DocumentCollection> collectionMap = new HashMap<>();
for (DocumentCollection collection : collections) {
collectionMap.put(collection.getId(), collection);
}
return (DocumentCollectionService) Proxy.newProxyInstance(
AgentChatCapabilityServiceTest.class.getClassLoader(),
new Class<?>[]{DocumentCollectionService.class},
(proxy, method, args) -> switch (method.getName()) {
case "getById" -> collectionMap.get(new BigInteger(String.valueOf(args[0])));
case "toPublishedView" -> args[0];
case "listByIds" -> ((java.util.Collection<?>) args[0]).stream()
.map(id -> collectionMap.get(new BigInteger(String.valueOf(id))))
.filter(java.util.Objects::nonNull)
.toList();
default -> throw new UnsupportedOperationException(method.getName());
}
);
}
private static class AllowResourceAccessService implements ResourceAccessService {
@Override public boolean canAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action) { return true; }
@Override public boolean canAccess(LoginAccount loginAccount, CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action) { return true; }
@Override public void assertAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action, String message) {}
}
private static class DenyResourceAccessService extends AllowResourceAccessService {
@Override public void assertAccess(CategoryResourceType resourceType, VisibilityResource resource, ResourceAction action, String message) {
throw new BusinessException(message);
}
}
}