feat: 先进智能体功能上线
- 基于 agent-runtime 打造,默认 ReAct agent - 支持 agent 能力对接,已对接工作流、插件、知识库等 tool 能力 - 全新 agent 编排界面,支持可视化便捷配置 agent - 全新 agent 聊天界面,支持快捷操作、额外知识库选择等
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user