feat: 全新智能体功能
- 基于先进智能体框架,增加智能体编排功能 - 增加智能体聊天,并对接持久化
This commit is contained in:
67
easyflow-modules/easyflow-module-agent/pom.xml
Normal file
67
easyflow-modules/easyflow-module-agent/pom.xml
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-modules</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<name>easyflow-module-agent</name>
|
||||
<artifactId>easyflow-module-agent</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-ai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-chatlog</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-approval</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-module-system</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-chat-protocol</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-cache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tech.easyflow</groupId>
|
||||
<artifactId>easyflow-common-satoken</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mybatis-flex</groupId>
|
||||
<artifactId>mybatis-flex-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-agent-runtime</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.agent.config;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
||||
/**
|
||||
* Agent 模块自动配置。
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@MapperScan("tech.easyflow.agent.mapper")
|
||||
@EnableConfigurationProperties(AgentRuntimeProperties.class)
|
||||
public class AgentModuleConfig {
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package tech.easyflow.agent.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Agent 运行态生产化配置。
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "easyflow.agent.runtime")
|
||||
public class AgentRuntimeProperties {
|
||||
|
||||
/**
|
||||
* Redis 热态 session 缓存 TTL。
|
||||
*/
|
||||
private Duration sessionCacheTtl = Duration.ofHours(24);
|
||||
|
||||
/**
|
||||
* HITL pending 默认过期时间。
|
||||
*/
|
||||
private Duration hitlPendingTimeout = Duration.ofMinutes(30);
|
||||
|
||||
/**
|
||||
* 运行锁等待时间。
|
||||
*/
|
||||
private Duration lockWaitTimeout = Duration.ofSeconds(2);
|
||||
|
||||
/**
|
||||
* 运行锁租约时间。
|
||||
*/
|
||||
private Duration lockLeaseTimeout = Duration.ofMinutes(5);
|
||||
|
||||
/**
|
||||
* 运行锁续期间隔。
|
||||
*/
|
||||
private Duration lockRenewInterval = Duration.ofMinutes(1);
|
||||
|
||||
/**
|
||||
* 获取 Redis 热态 session 缓存 TTL。
|
||||
*
|
||||
* @return 缓存 TTL
|
||||
*/
|
||||
public Duration getSessionCacheTtl() {
|
||||
return sessionCacheTtl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Redis 热态 session 缓存 TTL。
|
||||
*
|
||||
* @param sessionCacheTtl 缓存 TTL
|
||||
*/
|
||||
public void setSessionCacheTtl(Duration sessionCacheTtl) {
|
||||
this.sessionCacheTtl = sessionCacheTtl == null ? Duration.ofHours(24) : sessionCacheTtl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HITL pending 默认过期时间。
|
||||
*
|
||||
* @return 过期时间
|
||||
*/
|
||||
public Duration getHitlPendingTimeout() {
|
||||
return hitlPendingTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 HITL pending 默认过期时间。
|
||||
*
|
||||
* @param hitlPendingTimeout 过期时间
|
||||
*/
|
||||
public void setHitlPendingTimeout(Duration hitlPendingTimeout) {
|
||||
this.hitlPendingTimeout = hitlPendingTimeout == null ? Duration.ofMinutes(30) : hitlPendingTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行锁等待时间。
|
||||
*
|
||||
* @return 等待时间
|
||||
*/
|
||||
public Duration getLockWaitTimeout() {
|
||||
return lockWaitTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置运行锁等待时间。
|
||||
*
|
||||
* @param lockWaitTimeout 等待时间
|
||||
*/
|
||||
public void setLockWaitTimeout(Duration lockWaitTimeout) {
|
||||
this.lockWaitTimeout = lockWaitTimeout == null ? Duration.ofSeconds(2) : lockWaitTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行锁租约时间。
|
||||
*
|
||||
* @return 租约时间
|
||||
*/
|
||||
public Duration getLockLeaseTimeout() {
|
||||
return lockLeaseTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置运行锁租约时间。
|
||||
*
|
||||
* @param lockLeaseTimeout 租约时间
|
||||
*/
|
||||
public void setLockLeaseTimeout(Duration lockLeaseTimeout) {
|
||||
this.lockLeaseTimeout = lockLeaseTimeout == null ? Duration.ofMinutes(5) : lockLeaseTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行锁续期间隔。
|
||||
*
|
||||
* @return 续期间隔
|
||||
*/
|
||||
public Duration getLockRenewInterval() {
|
||||
return lockRenewInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置运行锁续期间隔。
|
||||
*
|
||||
* @param lockRenewInterval 续期间隔
|
||||
*/
|
||||
public void setLockRenewInterval(Duration lockRenewInterval) {
|
||||
this.lockRenewInterval = lockRenewInterval == null ? Duration.ofMinutes(1) : lockRenewInterval;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package tech.easyflow.agent.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
import tech.easyflow.common.entity.DateEntity;
|
||||
import tech.easyflow.system.permission.resource.VisibilityResource;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Agent 主实体。
|
||||
*/
|
||||
@Table("tb_agent")
|
||||
public class Agent extends DateEntity implements VisibilityResource, Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger deptId;
|
||||
private String name;
|
||||
private String description;
|
||||
private String avatar;
|
||||
private BigInteger categoryId;
|
||||
private BigInteger modelId;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> modelConfigJson = new LinkedHashMap<>();
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> generationConfigJson = new LinkedHashMap<>();
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> promptConfigJson = new LinkedHashMap<>();
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> memoryConfigJson = new LinkedHashMap<>();
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> executionConfigJson = new LinkedHashMap<>();
|
||||
private Integer status;
|
||||
private String visibilityScope;
|
||||
private String publishStatus;
|
||||
private BigInteger currentApprovalInstanceId;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> publishedSnapshotJson = new LinkedHashMap<>();
|
||||
private Date publishedAt;
|
||||
private BigInteger publishedBy;
|
||||
private Date created;
|
||||
private BigInteger createdBy;
|
||||
private Date modified;
|
||||
private BigInteger modifiedBy;
|
||||
|
||||
@Column(ignore = true)
|
||||
private Boolean approvalPending;
|
||||
@Column(ignore = true)
|
||||
private String currentApprovalActionType;
|
||||
@Column(ignore = true)
|
||||
private String displayPublishStatus;
|
||||
@Column(ignore = true)
|
||||
private String createdByName;
|
||||
@Column(ignore = true)
|
||||
private List<AgentToolBinding> toolBindings;
|
||||
@Column(ignore = true)
|
||||
private List<AgentKnowledgeBinding> knowledgeBindings;
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getDeptId() { return deptId; }
|
||||
public void setDeptId(BigInteger deptId) { this.deptId = deptId; }
|
||||
public String getName() { return name; }
|
||||
public void setName(String name) { this.name = name; }
|
||||
public String getDescription() { return description; }
|
||||
public void setDescription(String description) { this.description = description; }
|
||||
public String getAvatar() { return avatar; }
|
||||
public void setAvatar(String avatar) { this.avatar = avatar; }
|
||||
public BigInteger getCategoryId() { return categoryId; }
|
||||
public void setCategoryId(BigInteger categoryId) { this.categoryId = categoryId; }
|
||||
public BigInteger getModelId() { return modelId; }
|
||||
public void setModelId(BigInteger modelId) { this.modelId = modelId; }
|
||||
public Map<String, Object> getModelConfigJson() { return modelConfigJson; }
|
||||
public void setModelConfigJson(Map<String, Object> modelConfigJson) { this.modelConfigJson = modelConfigJson == null ? new LinkedHashMap<>() : modelConfigJson; }
|
||||
public Map<String, Object> getGenerationConfigJson() { return generationConfigJson; }
|
||||
public void setGenerationConfigJson(Map<String, Object> generationConfigJson) { this.generationConfigJson = generationConfigJson == null ? new LinkedHashMap<>() : generationConfigJson; }
|
||||
public Map<String, Object> getPromptConfigJson() { return promptConfigJson; }
|
||||
public void setPromptConfigJson(Map<String, Object> promptConfigJson) { this.promptConfigJson = promptConfigJson == null ? new LinkedHashMap<>() : promptConfigJson; }
|
||||
public Map<String, Object> getMemoryConfigJson() { return memoryConfigJson; }
|
||||
public void setMemoryConfigJson(Map<String, Object> memoryConfigJson) { this.memoryConfigJson = memoryConfigJson == null ? new LinkedHashMap<>() : memoryConfigJson; }
|
||||
public Map<String, Object> getExecutionConfigJson() { return executionConfigJson; }
|
||||
public void setExecutionConfigJson(Map<String, Object> executionConfigJson) { this.executionConfigJson = executionConfigJson == null ? new LinkedHashMap<>() : executionConfigJson; }
|
||||
public Integer getStatus() { return status; }
|
||||
public void setStatus(Integer status) { this.status = status; }
|
||||
public String getVisibilityScope() { return visibilityScope; }
|
||||
public void setVisibilityScope(String visibilityScope) { this.visibilityScope = visibilityScope; }
|
||||
public String getPublishStatus() { return publishStatus; }
|
||||
public void setPublishStatus(String publishStatus) { this.publishStatus = publishStatus; }
|
||||
public BigInteger getCurrentApprovalInstanceId() { return currentApprovalInstanceId; }
|
||||
public void setCurrentApprovalInstanceId(BigInteger currentApprovalInstanceId) { this.currentApprovalInstanceId = currentApprovalInstanceId; }
|
||||
public Map<String, Object> getPublishedSnapshotJson() { return publishedSnapshotJson; }
|
||||
public void setPublishedSnapshotJson(Map<String, Object> publishedSnapshotJson) { this.publishedSnapshotJson = publishedSnapshotJson == null ? new LinkedHashMap<>() : publishedSnapshotJson; }
|
||||
public Date getPublishedAt() { return publishedAt; }
|
||||
public void setPublishedAt(Date publishedAt) { this.publishedAt = publishedAt; }
|
||||
public BigInteger getPublishedBy() { return publishedBy; }
|
||||
public void setPublishedBy(BigInteger publishedBy) { this.publishedBy = publishedBy; }
|
||||
@Override public Date getCreated() { return created; }
|
||||
@Override public void setCreated(Date created) { this.created = created; }
|
||||
public BigInteger getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||
@Override public Date getModified() { return modified; }
|
||||
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||
public Boolean getApprovalPending() { return approvalPending; }
|
||||
public void setApprovalPending(Boolean approvalPending) { this.approvalPending = approvalPending; }
|
||||
public String getCurrentApprovalActionType() { return currentApprovalActionType; }
|
||||
public void setCurrentApprovalActionType(String currentApprovalActionType) { this.currentApprovalActionType = currentApprovalActionType; }
|
||||
public String getDisplayPublishStatus() { return displayPublishStatus; }
|
||||
public void setDisplayPublishStatus(String displayPublishStatus) { this.displayPublishStatus = displayPublishStatus; }
|
||||
public String getCreatedByName() { return createdByName; }
|
||||
public void setCreatedByName(String createdByName) { this.createdByName = createdByName; }
|
||||
public List<AgentToolBinding> getToolBindings() { return toolBindings; }
|
||||
public void setToolBindings(List<AgentToolBinding> toolBindings) { this.toolBindings = toolBindings; }
|
||||
public List<AgentKnowledgeBinding> getKnowledgeBindings() { return knowledgeBindings; }
|
||||
public void setKnowledgeBindings(List<AgentKnowledgeBinding> knowledgeBindings) { this.knowledgeBindings = knowledgeBindings; }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package tech.easyflow.agent.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.common.entity.DateEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Agent 分类实体。
|
||||
*/
|
||||
@Table("tb_agent_category")
|
||||
public class AgentCategory extends DateEntity implements Serializable {
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private String categoryName;
|
||||
private Integer sortNo;
|
||||
private Integer status;
|
||||
private Date created;
|
||||
private BigInteger createdBy;
|
||||
private Date modified;
|
||||
private BigInteger modifiedBy;
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public String getCategoryName() { return categoryName; }
|
||||
public void setCategoryName(String categoryName) { this.categoryName = categoryName; }
|
||||
public Integer getSortNo() { return sortNo; }
|
||||
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
|
||||
public Integer getStatus() { return status; }
|
||||
public void setStatus(Integer status) { this.status = status; }
|
||||
@Override public Date getCreated() { return created; }
|
||||
@Override public void setCreated(Date created) { this.created = created; }
|
||||
public BigInteger getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||
@Override public Date getModified() { return modified; }
|
||||
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package tech.easyflow.agent.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
import tech.easyflow.common.entity.DateEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Agent 工具审批挂起态实体。
|
||||
*/
|
||||
@Table("tb_agent_hitl_pending")
|
||||
public class AgentHitlPending extends DateEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger agentId;
|
||||
private BigInteger chatSessionId;
|
||||
private String runtimeSessionId;
|
||||
private String requestId;
|
||||
private String resumeToken;
|
||||
private String toolCallId;
|
||||
private String toolName;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> toolInputJson = new LinkedHashMap<>();
|
||||
private String status;
|
||||
private String rejectReason;
|
||||
private Date expiresAt;
|
||||
private Date consumedAt;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> metadataJson = new LinkedHashMap<>();
|
||||
private Date created;
|
||||
private BigInteger createdBy;
|
||||
private Date modified;
|
||||
private BigInteger modifiedBy;
|
||||
private Integer isDeleted;
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getAgentId() { return agentId; }
|
||||
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
|
||||
public BigInteger getChatSessionId() { return chatSessionId; }
|
||||
public void setChatSessionId(BigInteger chatSessionId) { this.chatSessionId = chatSessionId; }
|
||||
public String getRuntimeSessionId() { return runtimeSessionId; }
|
||||
public void setRuntimeSessionId(String runtimeSessionId) { this.runtimeSessionId = runtimeSessionId; }
|
||||
public String getRequestId() { return requestId; }
|
||||
public void setRequestId(String requestId) { this.requestId = requestId; }
|
||||
public String getResumeToken() { return resumeToken; }
|
||||
public void setResumeToken(String resumeToken) { this.resumeToken = resumeToken; }
|
||||
public String getToolCallId() { return toolCallId; }
|
||||
public void setToolCallId(String toolCallId) { this.toolCallId = toolCallId; }
|
||||
public String getToolName() { return toolName; }
|
||||
public void setToolName(String toolName) { this.toolName = toolName; }
|
||||
public Map<String, Object> getToolInputJson() { return toolInputJson; }
|
||||
public void setToolInputJson(Map<String, Object> toolInputJson) { this.toolInputJson = toolInputJson == null ? new LinkedHashMap<>() : toolInputJson; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getRejectReason() { return rejectReason; }
|
||||
public void setRejectReason(String rejectReason) { this.rejectReason = rejectReason; }
|
||||
public Date getExpiresAt() { return expiresAt; }
|
||||
public void setExpiresAt(Date expiresAt) { this.expiresAt = expiresAt; }
|
||||
public Date getConsumedAt() { return consumedAt; }
|
||||
public void setConsumedAt(Date consumedAt) { this.consumedAt = consumedAt; }
|
||||
public Map<String, Object> getMetadataJson() { return metadataJson; }
|
||||
public void setMetadataJson(Map<String, Object> metadataJson) { this.metadataJson = metadataJson == null ? new LinkedHashMap<>() : metadataJson; }
|
||||
@Override public Date getCreated() { return created; }
|
||||
@Override public void setCreated(Date created) { this.created = created; }
|
||||
public BigInteger getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||
@Override public Date getModified() { return modified; }
|
||||
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||
public Integer getIsDeleted() { return isDeleted; }
|
||||
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package tech.easyflow.agent.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
import tech.easyflow.common.entity.DateEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Agent 知识库绑定实体。
|
||||
*/
|
||||
@Table("tb_agent_knowledge_binding")
|
||||
public class AgentKnowledgeBinding extends DateEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger agentId;
|
||||
private BigInteger knowledgeId;
|
||||
private String retrievalMode;
|
||||
private Boolean enabled;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> optionsJson = new LinkedHashMap<>();
|
||||
@Column(ignore = true)
|
||||
private Map<String, Object> resourceSnapshot = new LinkedHashMap<>();
|
||||
@Column(ignore = true)
|
||||
private Map<String, Object> resourceSummary = new LinkedHashMap<>();
|
||||
private Integer sortNo;
|
||||
private Date created;
|
||||
private BigInteger createdBy;
|
||||
private Date modified;
|
||||
private BigInteger modifiedBy;
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getAgentId() { return agentId; }
|
||||
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
|
||||
public BigInteger getKnowledgeId() { return knowledgeId; }
|
||||
public void setKnowledgeId(BigInteger knowledgeId) { this.knowledgeId = knowledgeId; }
|
||||
public String getRetrievalMode() { return retrievalMode; }
|
||||
public void setRetrievalMode(String retrievalMode) { this.retrievalMode = retrievalMode; }
|
||||
public Boolean getEnabled() { return enabled; }
|
||||
public void setEnabled(Boolean enabled) { this.enabled = enabled; }
|
||||
public Map<String, Object> getOptionsJson() { return optionsJson; }
|
||||
public void setOptionsJson(Map<String, Object> optionsJson) { this.optionsJson = optionsJson == null ? new LinkedHashMap<>() : optionsJson; }
|
||||
public Map<String, Object> getResourceSnapshot() { return resourceSnapshot; }
|
||||
public void setResourceSnapshot(Map<String, Object> resourceSnapshot) { this.resourceSnapshot = resourceSnapshot == null ? new LinkedHashMap<>() : resourceSnapshot; }
|
||||
public Map<String, Object> getResourceSummary() { return resourceSummary; }
|
||||
public void setResourceSummary(Map<String, Object> resourceSummary) { this.resourceSummary = resourceSummary == null ? new LinkedHashMap<>() : resourceSummary; }
|
||||
public Integer getSortNo() { return sortNo; }
|
||||
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
|
||||
@Override public Date getCreated() { return created; }
|
||||
@Override public void setCreated(Date created) { this.created = created; }
|
||||
public BigInteger getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||
@Override public Date getModified() { return modified; }
|
||||
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package tech.easyflow.agent.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Agent 运行事件摘要实体。
|
||||
*/
|
||||
@Table("tb_agent_run_event")
|
||||
public class AgentRunEventRecord implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger agentId;
|
||||
private BigInteger chatSessionId;
|
||||
private BigInteger roundId;
|
||||
private Integer roundNo;
|
||||
private Integer variantIndex;
|
||||
private String requestId;
|
||||
private String eventId;
|
||||
private String eventType;
|
||||
private String eventPhase;
|
||||
private String toolCallId;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> payloadJson = new LinkedHashMap<>();
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> metadataJson = new LinkedHashMap<>();
|
||||
private Date created;
|
||||
private BigInteger createdBy;
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getAgentId() { return agentId; }
|
||||
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
|
||||
public BigInteger getChatSessionId() { return chatSessionId; }
|
||||
public void setChatSessionId(BigInteger chatSessionId) { this.chatSessionId = chatSessionId; }
|
||||
public BigInteger getRoundId() { return roundId; }
|
||||
public void setRoundId(BigInteger roundId) { this.roundId = roundId; }
|
||||
public Integer getRoundNo() { return roundNo; }
|
||||
public void setRoundNo(Integer roundNo) { this.roundNo = roundNo; }
|
||||
public Integer getVariantIndex() { return variantIndex; }
|
||||
public void setVariantIndex(Integer variantIndex) { this.variantIndex = variantIndex; }
|
||||
public String getRequestId() { return requestId; }
|
||||
public void setRequestId(String requestId) { this.requestId = requestId; }
|
||||
public String getEventId() { return eventId; }
|
||||
public void setEventId(String eventId) { this.eventId = eventId; }
|
||||
public String getEventType() { return eventType; }
|
||||
public void setEventType(String eventType) { this.eventType = eventType; }
|
||||
public String getEventPhase() { return eventPhase; }
|
||||
public void setEventPhase(String eventPhase) { this.eventPhase = eventPhase; }
|
||||
public String getToolCallId() { return toolCallId; }
|
||||
public void setToolCallId(String toolCallId) { this.toolCallId = toolCallId; }
|
||||
public Map<String, Object> getPayloadJson() { return payloadJson; }
|
||||
public void setPayloadJson(Map<String, Object> payloadJson) { this.payloadJson = payloadJson == null ? new LinkedHashMap<>() : payloadJson; }
|
||||
public Map<String, Object> getMetadataJson() { return metadataJson; }
|
||||
public void setMetadataJson(Map<String, Object> metadataJson) { this.metadataJson = metadataJson == null ? new LinkedHashMap<>() : metadataJson; }
|
||||
public Date getCreated() { return created; }
|
||||
public void setCreated(Date created) { this.created = created; }
|
||||
public BigInteger getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package tech.easyflow.agent.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
import tech.easyflow.common.entity.DateEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AgentScope 会话状态实体。
|
||||
*/
|
||||
@Table("tb_agent_session")
|
||||
public class AgentSession extends DateEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger agentId;
|
||||
private BigInteger chatSessionId;
|
||||
private String runtimeSessionId;
|
||||
private String sessionKey;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> stateJson = new LinkedHashMap<>();
|
||||
private Long version;
|
||||
private Long cacheVersion;
|
||||
private Date lastAccessAt;
|
||||
private Date expiresAt;
|
||||
private Date created;
|
||||
private BigInteger createdBy;
|
||||
private Date modified;
|
||||
private BigInteger modifiedBy;
|
||||
private Integer isDeleted;
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getAgentId() { return agentId; }
|
||||
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
|
||||
public BigInteger getChatSessionId() { return chatSessionId; }
|
||||
public void setChatSessionId(BigInteger chatSessionId) { this.chatSessionId = chatSessionId; }
|
||||
public String getRuntimeSessionId() { return runtimeSessionId; }
|
||||
public void setRuntimeSessionId(String runtimeSessionId) { this.runtimeSessionId = runtimeSessionId; }
|
||||
public String getSessionKey() { return sessionKey; }
|
||||
public void setSessionKey(String sessionKey) { this.sessionKey = sessionKey; }
|
||||
public Map<String, Object> getStateJson() { return stateJson; }
|
||||
public void setStateJson(Map<String, Object> stateJson) { this.stateJson = stateJson == null ? new LinkedHashMap<>() : stateJson; }
|
||||
public Long getVersion() { return version; }
|
||||
public void setVersion(Long version) { this.version = version; }
|
||||
public Long getCacheVersion() { return cacheVersion; }
|
||||
public void setCacheVersion(Long cacheVersion) { this.cacheVersion = cacheVersion; }
|
||||
public Date getLastAccessAt() { return lastAccessAt; }
|
||||
public void setLastAccessAt(Date lastAccessAt) { this.lastAccessAt = lastAccessAt; }
|
||||
public Date getExpiresAt() { return expiresAt; }
|
||||
public void setExpiresAt(Date expiresAt) { this.expiresAt = expiresAt; }
|
||||
@Override public Date getCreated() { return created; }
|
||||
@Override public void setCreated(Date created) { this.created = created; }
|
||||
public BigInteger getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||
@Override public Date getModified() { return modified; }
|
||||
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||
public Integer getIsDeleted() { return isDeleted; }
|
||||
public void setIsDeleted(Integer isDeleted) { this.isDeleted = isDeleted; }
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package tech.easyflow.agent.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import com.mybatisflex.core.handler.FastjsonTypeHandler;
|
||||
import tech.easyflow.common.entity.DateEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Agent 工具绑定实体。
|
||||
*/
|
||||
@Table("tb_agent_tool_binding")
|
||||
public class AgentToolBinding extends DateEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId")
|
||||
private BigInteger id;
|
||||
@Column(tenantId = true)
|
||||
private BigInteger tenantId;
|
||||
private BigInteger agentId;
|
||||
private String toolType;
|
||||
private BigInteger targetId;
|
||||
private String toolName;
|
||||
private Boolean enabled;
|
||||
private Boolean hitlEnabled;
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> hitlConfigJson = new LinkedHashMap<>();
|
||||
@Column(typeHandler = FastjsonTypeHandler.class)
|
||||
private Map<String, Object> optionsJson = new LinkedHashMap<>();
|
||||
@Column(ignore = true)
|
||||
private Map<String, Object> resourceSnapshot = new LinkedHashMap<>();
|
||||
@Column(ignore = true)
|
||||
private Map<String, Object> resourceSummary = new LinkedHashMap<>();
|
||||
private Integer sortNo;
|
||||
private Date created;
|
||||
private BigInteger createdBy;
|
||||
private Date modified;
|
||||
private BigInteger modifiedBy;
|
||||
|
||||
public BigInteger getId() { return id; }
|
||||
public void setId(BigInteger id) { this.id = id; }
|
||||
public BigInteger getTenantId() { return tenantId; }
|
||||
public void setTenantId(BigInteger tenantId) { this.tenantId = tenantId; }
|
||||
public BigInteger getAgentId() { return agentId; }
|
||||
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
|
||||
public String getToolType() { return toolType; }
|
||||
public void setToolType(String toolType) { this.toolType = toolType; }
|
||||
public BigInteger getTargetId() { return targetId; }
|
||||
public void setTargetId(BigInteger targetId) { this.targetId = targetId; }
|
||||
public String getToolName() { return toolName; }
|
||||
public void setToolName(String toolName) { this.toolName = toolName; }
|
||||
public Boolean getEnabled() { return enabled; }
|
||||
public void setEnabled(Boolean enabled) { this.enabled = enabled; }
|
||||
public Boolean getHitlEnabled() { return hitlEnabled; }
|
||||
public void setHitlEnabled(Boolean hitlEnabled) { this.hitlEnabled = hitlEnabled; }
|
||||
public Map<String, Object> getHitlConfigJson() { return hitlConfigJson; }
|
||||
public void setHitlConfigJson(Map<String, Object> hitlConfigJson) { this.hitlConfigJson = hitlConfigJson == null ? new LinkedHashMap<>() : hitlConfigJson; }
|
||||
public Map<String, Object> getOptionsJson() { return optionsJson; }
|
||||
public void setOptionsJson(Map<String, Object> optionsJson) { this.optionsJson = optionsJson == null ? new LinkedHashMap<>() : optionsJson; }
|
||||
public Map<String, Object> getResourceSnapshot() { return resourceSnapshot; }
|
||||
public void setResourceSnapshot(Map<String, Object> resourceSnapshot) { this.resourceSnapshot = resourceSnapshot == null ? new LinkedHashMap<>() : resourceSnapshot; }
|
||||
public Map<String, Object> getResourceSummary() { return resourceSummary; }
|
||||
public void setResourceSummary(Map<String, Object> resourceSummary) { this.resourceSummary = resourceSummary == null ? new LinkedHashMap<>() : resourceSummary; }
|
||||
public Integer getSortNo() { return sortNo; }
|
||||
public void setSortNo(Integer sortNo) { this.sortNo = sortNo; }
|
||||
@Override public Date getCreated() { return created; }
|
||||
@Override public void setCreated(Date created) { this.created = created; }
|
||||
public BigInteger getCreatedBy() { return createdBy; }
|
||||
public void setCreatedBy(BigInteger createdBy) { this.createdBy = createdBy; }
|
||||
@Override public Date getModified() { return modified; }
|
||||
@Override public void setModified(Date modified) { this.modified = modified; }
|
||||
public BigInteger getModifiedBy() { return modifiedBy; }
|
||||
public void setModifiedBy(BigInteger modifiedBy) { this.modifiedBy = modifiedBy; }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package tech.easyflow.agent.enums;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Agent 工具绑定类型。
|
||||
*/
|
||||
public enum AgentToolType {
|
||||
|
||||
WORKFLOW,
|
||||
PLUGIN,
|
||||
MCP;
|
||||
|
||||
/**
|
||||
* 解析工具类型。
|
||||
*
|
||||
* @param value 类型值
|
||||
* @return 工具类型
|
||||
*/
|
||||
public static AgentToolType from(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("toolType 不能为空");
|
||||
}
|
||||
return AgentToolType.valueOf(value.trim().toUpperCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.agent.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.agent.entity.AgentCategory;
|
||||
|
||||
/**
|
||||
* Agent 分类 Mapper。
|
||||
*/
|
||||
public interface AgentCategoryMapper extends BaseMapper<AgentCategory> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.agent.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.agent.entity.AgentHitlPending;
|
||||
|
||||
/**
|
||||
* Agent 工具审批挂起态 Mapper。
|
||||
*/
|
||||
public interface AgentHitlPendingMapper extends BaseMapper<AgentHitlPending> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.agent.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
|
||||
/**
|
||||
* Agent 知识库绑定 Mapper。
|
||||
*/
|
||||
public interface AgentKnowledgeBindingMapper extends BaseMapper<AgentKnowledgeBinding> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.agent.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
|
||||
/**
|
||||
* Agent Mapper。
|
||||
*/
|
||||
public interface AgentMapper extends BaseMapper<Agent> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.agent.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.agent.entity.AgentRunEventRecord;
|
||||
|
||||
/**
|
||||
* Agent 运行事件摘要 Mapper。
|
||||
*/
|
||||
public interface AgentRunEventRecordMapper extends BaseMapper<AgentRunEventRecord> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.agent.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.agent.entity.AgentSession;
|
||||
|
||||
/**
|
||||
* AgentScope 会话状态 Mapper。
|
||||
*/
|
||||
public interface AgentSessionMapper extends BaseMapper<AgentSession> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.agent.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
|
||||
/**
|
||||
* Agent 工具绑定 Mapper。
|
||||
*/
|
||||
public interface AgentToolBindingMapper extends BaseMapper<AgentToolBinding> {
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package tech.easyflow.agent.publish;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||
import tech.easyflow.agent.service.AgentService;
|
||||
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.publish.AbstractAiResourceLifecycleHandler;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||
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.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Agent 审批资源处理器。
|
||||
*/
|
||||
@Component
|
||||
public class AgentApprovalSubjectHandler extends AbstractAiResourceLifecycleHandler<Agent> {
|
||||
|
||||
private final AgentService agentService;
|
||||
private final AgentToolBindingService agentToolBindingService;
|
||||
private final AgentKnowledgeBindingService agentKnowledgeBindingService;
|
||||
private final ResourceAccessService resourceAccessService;
|
||||
|
||||
/**
|
||||
* 创建 Agent 审批资源处理器。
|
||||
*
|
||||
* @param approvalInstanceService 审批实例服务
|
||||
* @param objectMapper JSON 映射器
|
||||
* @param agentService Agent 服务
|
||||
* @param agentToolBindingService Agent 工具绑定服务
|
||||
* @param agentKnowledgeBindingService Agent 知识库绑定服务
|
||||
* @param resourceAccessService 资源访问服务
|
||||
*/
|
||||
public AgentApprovalSubjectHandler(ApprovalInstanceService approvalInstanceService,
|
||||
ObjectMapper objectMapper,
|
||||
AgentService agentService,
|
||||
AgentToolBindingService agentToolBindingService,
|
||||
AgentKnowledgeBindingService agentKnowledgeBindingService,
|
||||
ResourceAccessService resourceAccessService) {
|
||||
super(approvalInstanceService, objectMapper);
|
||||
this.agentService = agentService;
|
||||
this.agentToolBindingService = agentToolBindingService;
|
||||
this.agentKnowledgeBindingService = agentKnowledgeBindingService;
|
||||
this.resourceAccessService = resourceAccessService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String resourceType() {
|
||||
return ApprovalResourceType.AGENT.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void assertPublishedAccess(Object identifier, String denyMessage) {
|
||||
Agent agent = agentService.getById(String.valueOf(identifier));
|
||||
if (agent == null || !PublishStatus.from(agent.getPublishStatus()).isExternallyVisible()
|
||||
|| agent.getPublishedSnapshotJson() == null || agent.getPublishedSnapshotJson().isEmpty()) {
|
||||
throw new BusinessException(denyMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Agent requireResource(BigInteger resourceId) {
|
||||
Agent agent = agentService.getById(resourceId);
|
||||
if (agent == null) {
|
||||
throw new BusinessException("Agent 不存在");
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void assertManagePermission(Agent resource) {
|
||||
resourceAccessService.assertAccess(CategoryResourceType.AGENT, resource, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger getCategoryId(Agent resource) {
|
||||
return resource.getCategoryId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BigInteger getDeptId(Agent resource) {
|
||||
return resource.getDeptId();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTitle(Agent resource) {
|
||||
return resource.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PublishStatus getCurrentStatus(Agent resource) {
|
||||
return PublishStatus.from(resource.getPublishStatus());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getPublishedSnapshot(Agent resource) {
|
||||
return resource.getPublishedSnapshotJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> buildResourceSnapshot(Agent resource) {
|
||||
return agentService.buildPublishSnapshot(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void persistResourceState(BigInteger resourceId, PublishStatus publishStatus, BigInteger currentApprovalInstanceId) {
|
||||
Agent agent = new Agent();
|
||||
agent.setId(resourceId);
|
||||
agent.setPublishStatus(publishStatus.getCode());
|
||||
agent.setCurrentApprovalInstanceId(currentApprovalInstanceId);
|
||||
agentService.updateById(agent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void publishResource(BigInteger resourceId, Map<String, Object> resourceSnapshot, BigInteger operatorId) {
|
||||
Agent agent = new Agent();
|
||||
agent.setId(resourceId);
|
||||
agent.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
agent.setPublishedSnapshotJson(resourceSnapshot);
|
||||
agent.setPublishedAt(new Date());
|
||||
agent.setPublishedBy(operatorId);
|
||||
agent.setCurrentApprovalInstanceId(null);
|
||||
agentService.updateById(agent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void markResourceOffline(BigInteger resourceId) {
|
||||
Agent agent = new Agent();
|
||||
agent.setId(resourceId);
|
||||
agent.setPublishStatus(PublishStatus.OFFLINE.getCode());
|
||||
agent.setCurrentApprovalInstanceId(null);
|
||||
agentService.updateById(agent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void removeResource(BigInteger resourceId) {
|
||||
agentService.removeById(resourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeRemove(BigInteger resourceId) {
|
||||
agentToolBindingService.remove(QueryWrapper.create().eq(AgentToolBinding::getAgentId, resourceId));
|
||||
agentKnowledgeBindingService.remove(QueryWrapper.create().eq(AgentKnowledgeBinding::getAgentId, resourceId));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resourceLabel() {
|
||||
return "Agent";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package tech.easyflow.agent.publish;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.publish.AiResourceLifecycleService;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Agent 发布生命周期应用服务。
|
||||
*/
|
||||
@Service
|
||||
public class AgentPublishAppService {
|
||||
|
||||
private final AiResourceLifecycleService aiResourceLifecycleService;
|
||||
|
||||
/**
|
||||
* 创建 Agent 发布应用服务。
|
||||
*
|
||||
* @param aiResourceLifecycleService AI 资源生命周期服务
|
||||
*/
|
||||
public AgentPublishAppService(AiResourceLifecycleService aiResourceLifecycleService) {
|
||||
this.aiResourceLifecycleService = aiResourceLifecycleService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交 Agent 发布审批。
|
||||
*
|
||||
* @param id Agent ID
|
||||
* @return 审批动作结果
|
||||
*/
|
||||
public ApprovalActionResult submitPublishApproval(BigInteger id) {
|
||||
return submit(id, ApprovalActionType.PUBLISH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交 Agent 下线审批。
|
||||
*
|
||||
* @param id Agent ID
|
||||
* @return 审批动作结果
|
||||
*/
|
||||
public ApprovalActionResult submitOfflineApproval(BigInteger id) {
|
||||
return submit(id, ApprovalActionType.OFFLINE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交 Agent 删除审批。
|
||||
*
|
||||
* @param id Agent ID
|
||||
* @return 审批动作结果
|
||||
*/
|
||||
public ApprovalActionResult submitDeleteApproval(BigInteger id) {
|
||||
return submit(id, ApprovalActionType.DELETE);
|
||||
}
|
||||
|
||||
private ApprovalActionResult submit(BigInteger id, ApprovalActionType actionType) {
|
||||
if (id == null) {
|
||||
throw new BusinessException("Agent 审批时资源ID不能为空");
|
||||
}
|
||||
return aiResourceLifecycleService.submitAction(
|
||||
ApprovalResourceType.AGENT.getCode(),
|
||||
id,
|
||||
actionType.getCode(),
|
||||
SaTokenUtil.getLoginAccount().getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package tech.easyflow.agent.runtime;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Agent 管理端运行请求。
|
||||
*/
|
||||
public class AgentChatRequest {
|
||||
|
||||
private BigInteger agentId;
|
||||
private BigInteger sessionId;
|
||||
private String prompt;
|
||||
|
||||
/**
|
||||
* 获取 Agent ID。
|
||||
*
|
||||
* @return Agent ID
|
||||
*/
|
||||
public BigInteger getAgentId() { return agentId; }
|
||||
|
||||
/**
|
||||
* 设置 Agent ID。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
*/
|
||||
public void setAgentId(BigInteger agentId) { this.agentId = agentId; }
|
||||
|
||||
/**
|
||||
* 获取会话 ID。
|
||||
*
|
||||
* @return 会话 ID
|
||||
*/
|
||||
public BigInteger getSessionId() { return sessionId; }
|
||||
|
||||
/**
|
||||
* 设置会话 ID。
|
||||
*
|
||||
* @param sessionId 会话 ID
|
||||
*/
|
||||
public void setSessionId(BigInteger sessionId) { this.sessionId = sessionId; }
|
||||
|
||||
/**
|
||||
* 获取用户纯文本输入。
|
||||
*
|
||||
* @return 用户输入
|
||||
*/
|
||||
public String getPrompt() { return prompt; }
|
||||
|
||||
/**
|
||||
* 设置用户纯文本输入。
|
||||
*
|
||||
* @param prompt 用户输入
|
||||
*/
|
||||
public void setPrompt(String prompt) { this.prompt = prompt; }
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
package tech.easyflow.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentDefinition;
|
||||
import com.easyagents.agent.runtime.AgentExecutionOptions;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeDocument;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgePolicy;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetrievalResult;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec;
|
||||
import com.easyagents.agent.runtime.memory.AgentMemoryCompressionParameter;
|
||||
import com.easyagents.agent.runtime.memory.AgentMemoryPolicy;
|
||||
import com.easyagents.agent.runtime.memory.AgentMemoryType;
|
||||
import com.easyagents.agent.runtime.model.AgentGenerationOptions;
|
||||
import com.easyagents.agent.runtime.model.AgentModelProviderType;
|
||||
import com.easyagents.agent.runtime.model.AgentModelSpec;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolCategory;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolResult;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||
import com.easyagents.core.document.Document;
|
||||
import com.easyagents.core.model.chat.tool.Parameter;
|
||||
import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.enums.AgentToolType;
|
||||
import tech.easyflow.ai.easyagents.tool.ChatToolNameHelper;
|
||||
import tech.easyflow.ai.easyagents.tool.McpTool;
|
||||
import tech.easyflow.ai.easyagents.tool.WorkflowTool;
|
||||
import tech.easyflow.ai.easyagentsflow.support.PublishedWorkflowDefinitionIds;
|
||||
import tech.easyflow.ai.entity.*;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||
import tech.easyflow.ai.service.*;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 将 Agent 发布快照编译为 easy-agents-agent-runtime 可执行定义。
|
||||
*/
|
||||
@Component
|
||||
public class AgentDefinitionCompiler {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AgentDefinitionCompiler.class);
|
||||
private static final int LOG_TEXT_MAX_LENGTH = 500;
|
||||
|
||||
@Resource
|
||||
private ModelService modelService;
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private PluginItemService pluginItemService;
|
||||
@Resource
|
||||
private McpService mcpService;
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* 编译 Agent 运行时定义和调用器。
|
||||
*
|
||||
* @param agent 已发布 Agent 视图
|
||||
* @return 运行时编译结果
|
||||
*/
|
||||
public AgentRuntimeBundle compile(Agent agent) {
|
||||
if (agent == null || agent.getId() == null) {
|
||||
throw new BusinessException("Agent 运行定义不能为空");
|
||||
}
|
||||
AgentRuntimeBundle bundle = new AgentRuntimeBundle();
|
||||
AgentDefinition definition = new AgentDefinition();
|
||||
definition.setAgentId(agent.getId().toString());
|
||||
definition.setAgentName(agent.getName());
|
||||
definition.setDescription(agent.getDescription());
|
||||
definition.setSystemPrompt(stringValue(agent.getPromptConfigJson(), "systemPrompt", stringValue(agent.getPromptConfigJson(), "prompt", "")));
|
||||
definition.setModelSpec(buildModelSpec(agent));
|
||||
definition.setGenerationOptions(buildGenerationOptions(agent.getGenerationConfigJson()));
|
||||
definition.setExecutionOptions(buildExecutionOptions(agent.getExecutionConfigJson()));
|
||||
definition.setMemoryPolicy(buildMemoryPolicy(agent.getMemoryConfigJson()));
|
||||
bundle.setDefinition(definition);
|
||||
|
||||
compileTools(agent, definition, bundle);
|
||||
compileKnowledge(agent, definition, bundle);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
private AgentModelSpec buildModelSpec(Agent agent) {
|
||||
Model model = modelService.getModelInstance(agent.getModelId());
|
||||
if (model == null) {
|
||||
throw new BusinessException("Agent 模型不存在");
|
||||
}
|
||||
Map<String, Object> config = agent.getModelConfigJson();
|
||||
AgentModelSpec spec = new AgentModelSpec();
|
||||
String providerType = stringValue(config, "providerType", model.getModelProvider() == null ? null : model.getModelProvider().getProviderType());
|
||||
spec.setProviderType(parseProviderType(providerType));
|
||||
spec.setModelName(stringValue(config, "modelName", model.getModelName()));
|
||||
spec.setBaseUrl(stringValue(config, "baseUrl", model.getEndpoint()));
|
||||
spec.setEndpointPath(stringValue(config, "endpointPath", model.getRequestPath()));
|
||||
spec.setApiKey(stringValue(config, "apiKey", model.getApiKey()));
|
||||
spec.getMetadata().put("modelId", model.getId());
|
||||
return spec;
|
||||
}
|
||||
|
||||
private AgentGenerationOptions buildGenerationOptions(Map<String, Object> config) {
|
||||
AgentGenerationOptions options = new AgentGenerationOptions();
|
||||
options.setTemperature(doubleValue(config, "temperature"));
|
||||
options.setTopP(doubleValue(config, "topP"));
|
||||
options.setTopK(intValue(config, "topK"));
|
||||
options.setMaxTokens(intValue(config, "maxTokens"));
|
||||
options.setMaxCompletionTokens(intValue(config, "maxCompletionTokens"));
|
||||
options.setThinkingBudget(intValue(config, "thinkingBudget"));
|
||||
options.setReasoningEffort(stringValue(config, "reasoningEffort", stringValue(config, "thinkingLevel", null)));
|
||||
options.setThinkingEnabled(booleanValue(config, "thinkingEnabled"));
|
||||
Boolean stream = booleanValue(config, "stream");
|
||||
if (stream != null) {
|
||||
options.setStream(stream);
|
||||
}
|
||||
options.setAdditionalBodyParams(mapValue(config, "additionalBodyParams"));
|
||||
options.setAdditionalHeaders(stringMapValue(config, "additionalHeaders"));
|
||||
options.setAdditionalQueryParams(stringMapValue(config, "additionalQueryParams"));
|
||||
return options;
|
||||
}
|
||||
|
||||
private AgentExecutionOptions buildExecutionOptions(Map<String, Object> config) {
|
||||
AgentExecutionOptions options = new AgentExecutionOptions();
|
||||
Integer maxIters = intValue(config, "maxIters");
|
||||
if (maxIters != null) {
|
||||
options.setMaxIters(maxIters);
|
||||
}
|
||||
Integer timeoutSeconds = intValue(config, "timeoutSeconds");
|
||||
if (timeoutSeconds != null) {
|
||||
options.setTimeout(Duration.ofSeconds(timeoutSeconds));
|
||||
}
|
||||
Boolean reasoningEnabled = booleanValue(config, "reasoningEnabled");
|
||||
if (reasoningEnabled != null) {
|
||||
options.setReasoningEnabled(reasoningEnabled);
|
||||
}
|
||||
Boolean toolCallingEnabled = booleanValue(config, "toolCallingEnabled");
|
||||
if (toolCallingEnabled != null) {
|
||||
options.setToolCallingEnabled(toolCallingEnabled);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
private AgentMemoryPolicy buildMemoryPolicy(Map<String, Object> config) {
|
||||
AgentMemoryPolicy policy = new AgentMemoryPolicy();
|
||||
policy.setType(memoryTypeValue(config, "type"));
|
||||
Map<String, Object> compressionConfig = mapValue(config, "compressionParameter");
|
||||
if (compressionConfig.isEmpty()) {
|
||||
compressionConfig = mapValue(config, "autoContext");
|
||||
}
|
||||
AgentMemoryCompressionParameter parameter = new AgentMemoryCompressionParameter();
|
||||
Boolean enabled = booleanValue(compressionConfig, "enabled");
|
||||
if (enabled != null) {
|
||||
parameter.setEnabled(enabled);
|
||||
}
|
||||
Integer msgThreshold = intValue(compressionConfig, "msgThreshold");
|
||||
if (msgThreshold == null) {
|
||||
msgThreshold = intValue(config, "maxAttachedMessageCount");
|
||||
}
|
||||
if (msgThreshold == null) {
|
||||
msgThreshold = intValue(config, "historyLimit");
|
||||
}
|
||||
if (msgThreshold != null) {
|
||||
parameter.setMsgThreshold(msgThreshold);
|
||||
policy.setMaxAttachedMessageCount(msgThreshold);
|
||||
}
|
||||
Integer lastKeep = intValue(compressionConfig, "lastKeep");
|
||||
if (lastKeep != null) {
|
||||
parameter.setLastKeep(lastKeep);
|
||||
}
|
||||
Double tokenRatio = doubleValue(compressionConfig, "tokenRatio");
|
||||
if (tokenRatio != null) {
|
||||
parameter.setTokenRatio(tokenRatio);
|
||||
}
|
||||
Long maxToken = longValue(compressionConfig, "maxToken");
|
||||
if (maxToken != null) {
|
||||
parameter.setMaxToken(maxToken);
|
||||
}
|
||||
Long largePayloadThreshold = longValue(compressionConfig, "largePayloadThreshold");
|
||||
if (largePayloadThreshold != null) {
|
||||
parameter.setLargePayloadThreshold(largePayloadThreshold);
|
||||
}
|
||||
Integer minCompressionTokenThreshold = intValue(compressionConfig, "minCompressionTokenThreshold");
|
||||
if (minCompressionTokenThreshold != null) {
|
||||
parameter.setMinCompressionTokenThreshold(minCompressionTokenThreshold);
|
||||
}
|
||||
Double currentRoundCompressionRatio = doubleValue(compressionConfig, "currentRoundCompressionRatio");
|
||||
if (currentRoundCompressionRatio != null) {
|
||||
parameter.setCurrentRoundCompressionRatio(currentRoundCompressionRatio);
|
||||
}
|
||||
Integer minConsecutiveToolMessages = intValue(compressionConfig, "minConsecutiveToolMessages");
|
||||
if (minConsecutiveToolMessages != null) {
|
||||
parameter.setMinConsecutiveToolMessages(minConsecutiveToolMessages);
|
||||
}
|
||||
policy.setCompressionParameter(parameter);
|
||||
return policy;
|
||||
}
|
||||
|
||||
private void compileTools(Agent agent, AgentDefinition definition, AgentRuntimeBundle bundle) {
|
||||
if (agent.getToolBindings() == null) {
|
||||
return;
|
||||
}
|
||||
List<AgentToolSpec> specs = new ArrayList<>();
|
||||
Map<String, com.easyagents.agent.runtime.tool.AgentToolInvoker> invokers = new LinkedHashMap<>();
|
||||
for (AgentToolBinding binding : agent.getToolBindings()) {
|
||||
if (!Boolean.TRUE.equals(binding.getEnabled())) {
|
||||
continue;
|
||||
}
|
||||
Tool tool = buildTool(binding);
|
||||
AgentToolSpec spec = toToolSpec(tool, binding);
|
||||
specs.add(spec);
|
||||
invokers.put(spec.getName(), (arguments, context) -> invokeTool(tool, arguments));
|
||||
}
|
||||
definition.setToolSpecs(specs);
|
||||
bundle.setToolInvokers(invokers);
|
||||
}
|
||||
|
||||
private Tool buildTool(AgentToolBinding binding) {
|
||||
AgentToolType type = AgentToolType.from(binding.getToolType());
|
||||
if (type == AgentToolType.WORKFLOW) {
|
||||
Workflow workflow = snapshotOrPublishedWorkflow(binding);
|
||||
if (workflow == null) {
|
||||
throw new BusinessException("绑定工作流不存在");
|
||||
}
|
||||
return new WorkflowTool(
|
||||
workflow,
|
||||
true,
|
||||
PublishedWorkflowDefinitionIds.published(String.valueOf(workflow.getId()))
|
||||
);
|
||||
}
|
||||
if (type == AgentToolType.PLUGIN) {
|
||||
PluginItem pluginItem = snapshotOrCurrentPlugin(binding);
|
||||
if (pluginItem == null) {
|
||||
throw new BusinessException("绑定插件不存在");
|
||||
}
|
||||
return pluginItem.toFunction();
|
||||
}
|
||||
Mcp mcp = snapshotOrCurrentMcp(binding);
|
||||
if (mcp == null) {
|
||||
throw new BusinessException("绑定 MCP 不存在");
|
||||
}
|
||||
McpTool tool = new McpTool();
|
||||
tool.setMcpId(mcp.getId());
|
||||
tool.setName(binding.getToolName());
|
||||
tool.setDescription(mcp.getDescription());
|
||||
tool.setParameters(new Parameter[0]);
|
||||
return tool;
|
||||
}
|
||||
|
||||
private AgentToolSpec toToolSpec(Tool tool, AgentToolBinding binding) {
|
||||
AgentToolSpec spec = new AgentToolSpec();
|
||||
String name = resolveRuntimeToolName(tool, binding);
|
||||
spec.setName(name);
|
||||
spec.setDescription(safeDescription(tool == null ? null : tool.getDescription()));
|
||||
spec.setCategory(AgentToolCategory.valueOf(AgentToolType.from(binding.getToolType()).name()));
|
||||
spec.setParametersSchema(toSchema(tool == null ? null : tool.getParameters()));
|
||||
spec.setApprovalRequired(Boolean.TRUE.equals(binding.getHitlEnabled()));
|
||||
if (Boolean.TRUE.equals(binding.getHitlEnabled())) {
|
||||
AgentToolApprovalRequest request = new AgentToolApprovalRequest();
|
||||
request.setApprovalPrompt(stringValue(binding.getHitlConfigJson(), "prompt", "是否批准执行工具:" + name));
|
||||
Map<String, Object> metadata = sanitizedHitlMetadata(binding.getHitlConfigJson());
|
||||
metadata.put("toolType", binding.getToolType());
|
||||
metadata.put("bindingId", binding.getId());
|
||||
metadata.put("targetId", binding.getTargetId());
|
||||
request.setMetadata(metadata);
|
||||
spec.setApprovalRequest(request);
|
||||
}
|
||||
spec.getMetadata().put("bindingId", binding.getId());
|
||||
spec.getMetadata().put("targetId", binding.getTargetId());
|
||||
return spec;
|
||||
}
|
||||
|
||||
private Map<String, Object> sanitizedHitlMetadata(Map<String, Object> config) {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
if (config != null) {
|
||||
config.forEach((key, value) -> {
|
||||
if (!isHitlPromptKey(key)) {
|
||||
metadata.put(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private boolean isHitlPromptKey(String key) {
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
String normalized = key.trim();
|
||||
return "prompt".equalsIgnoreCase(normalized)
|
||||
|| "question".equalsIgnoreCase(normalized)
|
||||
|| "approvalPrompt".equalsIgnoreCase(normalized);
|
||||
}
|
||||
|
||||
private AgentToolResult invokeTool(Tool tool, Map<String, Object> arguments) {
|
||||
String toolName = tool == null ? null : tool.getName();
|
||||
LOG.info("Agent tool invoke started, toolName={}, arguments={}", toolName, arguments);
|
||||
try {
|
||||
Object result = tool.invoke(arguments == null ? Map.of() : arguments);
|
||||
String resultText = result == null ? "" : String.valueOf(result);
|
||||
LOG.info("Agent tool invoke completed, toolName={}, result={}", toolName, truncate(resultText));
|
||||
return AgentToolResult.success(resultText);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Agent tool invoke failed, toolName={}, message={}", toolName, e.getMessage(), e);
|
||||
return AgentToolResult.failure(e.getMessage() == null ? "工具执行失败" : e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveRuntimeToolName(Tool tool, AgentToolBinding binding) {
|
||||
String bindingName = binding == null ? null : binding.getToolName();
|
||||
if (ChatToolNameHelper.isSafeToolName(bindingName)) {
|
||||
return bindingName;
|
||||
}
|
||||
String toolName = tool == null ? null : tool.getName();
|
||||
if (ChatToolNameHelper.isSafeToolName(toolName)) {
|
||||
return toolName;
|
||||
}
|
||||
BigInteger targetId = binding == null ? null : binding.getTargetId();
|
||||
return ChatToolNameHelper.buildFallbackName("tool", targetId);
|
||||
}
|
||||
|
||||
private void compileKnowledge(Agent agent, AgentDefinition definition, AgentRuntimeBundle bundle) {
|
||||
if (agent.getKnowledgeBindings() == null) {
|
||||
return;
|
||||
}
|
||||
List<AgentKnowledgeSpec> specs = new ArrayList<>();
|
||||
Map<String, com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetriever> retrievers = new LinkedHashMap<>();
|
||||
for (AgentKnowledgeBinding binding : agent.getKnowledgeBindings()) {
|
||||
if (!Boolean.TRUE.equals(binding.getEnabled())) {
|
||||
continue;
|
||||
}
|
||||
DocumentCollection knowledge = snapshotOrPublishedKnowledge(binding);
|
||||
if (knowledge == null) {
|
||||
throw new BusinessException("绑定知识库不存在");
|
||||
}
|
||||
AgentKnowledgeSpec spec = new AgentKnowledgeSpec();
|
||||
spec.setKnowledgeId(binding.getKnowledgeId().toString());
|
||||
spec.setName(knowledge.getTitle());
|
||||
spec.setDescription(knowledge.getDescription());
|
||||
spec.setRetrievalMode(AgentKnowledgePolicy.AGENTIC);
|
||||
spec.getMetadata().put("knowledgeType", knowledge.getCollectionType());
|
||||
spec.getMetadata().put("faqCollection", knowledge.isFaqCollection());
|
||||
Integer limit = intValue(binding.getOptionsJson(), "limit");
|
||||
spec.setLimit(limit == null ? 5 : limit);
|
||||
Double threshold = doubleValue(binding.getOptionsJson(), "scoreThreshold");
|
||||
if (threshold != null) {
|
||||
spec.setScoreThreshold(threshold);
|
||||
}
|
||||
specs.add(spec);
|
||||
retrievers.put(spec.getKnowledgeId(), request -> retrieveKnowledge(binding, request.getQuery(), request.getLimit(), request.getScoreThreshold()));
|
||||
}
|
||||
definition.setKnowledgeSpecs(specs);
|
||||
bundle.setKnowledgeRetrievers(retrievers);
|
||||
}
|
||||
|
||||
private AgentKnowledgeRetrievalResult retrieveKnowledge(AgentKnowledgeBinding binding, String query, int limit, double scoreThreshold) {
|
||||
KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest();
|
||||
request.setKnowledgeId(binding.getKnowledgeId());
|
||||
request.setQuery(query);
|
||||
request.setLimit(limit <= 0 ? null : limit);
|
||||
request.setMinSimilarity(scoreThreshold <= 0D ? null : scoreThreshold);
|
||||
request.setRetrievalMode(KnowledgeRetrievalModes.parse(binding.getRetrievalMode()));
|
||||
request.setCallerType("AGENT_KNOWLEDGE");
|
||||
request.setCallerId(binding.getAgentId() == null ? null : binding.getAgentId().toString());
|
||||
LOG.info(
|
||||
"Agent knowledge retrieval started, agentId={}, knowledgeId={}, query={}, limit={}, scoreThreshold={}, retrievalMode={}",
|
||||
request.getCallerId(),
|
||||
request.getKnowledgeId(),
|
||||
request.getQuery(),
|
||||
request.getLimit(),
|
||||
request.getMinSimilarity(),
|
||||
request.getRetrievalMode()
|
||||
);
|
||||
List<Document> documents = documentCollectionService.search(request);
|
||||
LOG.info(
|
||||
"Agent knowledge retrieval completed, agentId={}, knowledgeId={}, query={}, documentCount={}, documents={}",
|
||||
request.getCallerId(),
|
||||
request.getKnowledgeId(),
|
||||
request.getQuery(),
|
||||
documents == null ? 0 : documents.size(),
|
||||
summarizeDocuments(documents)
|
||||
);
|
||||
List<AgentKnowledgeDocument> mapped = new ArrayList<>();
|
||||
if (documents != null) {
|
||||
for (Document document : documents) {
|
||||
AgentKnowledgeDocument item = new AgentKnowledgeDocument();
|
||||
item.setDocumentId(firstNonBlank(
|
||||
metadataString(document.getMetadataMap(), "documentId"),
|
||||
document.getId() == null ? null : String.valueOf(document.getId())
|
||||
));
|
||||
item.setDocumentName(firstNonBlank(
|
||||
metadataString(document.getMetadataMap(), "sourceFileName"),
|
||||
document.getTitle()
|
||||
));
|
||||
item.setChunkId(firstNonBlank(
|
||||
metadataString(document.getMetadataMap(), "chunkId"),
|
||||
document.getId() == null ? null : String.valueOf(document.getId())
|
||||
));
|
||||
item.setContent(document.getContent());
|
||||
item.setScore(document.getScore());
|
||||
item.setMetadata(document.getMetadataMap());
|
||||
item.setSourceUri(metadataString(document.getMetadataMap(), "sourceUri"));
|
||||
mapped.add(item);
|
||||
}
|
||||
}
|
||||
return AgentKnowledgeRetrievalResult.of(mapped);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用于日志排查的知识库命中摘要,避免完整内容撑爆日志。
|
||||
*
|
||||
* @param documents 知识库检索命中文档
|
||||
* @return 文档摘要列表
|
||||
*/
|
||||
private List<Map<String, Object>> summarizeDocuments(List<Document> documents) {
|
||||
List<Map<String, Object>> summaries = new ArrayList<>();
|
||||
if (documents == null) {
|
||||
return summaries;
|
||||
}
|
||||
for (Document document : documents) {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("id", document.getId());
|
||||
summary.put("title", document.getTitle());
|
||||
summary.put("score", document.getScore());
|
||||
summary.put("content", truncate(document.getContent()));
|
||||
summary.put("metadata", document.getMetadataMap());
|
||||
summaries.add(summary);
|
||||
}
|
||||
return summaries;
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断日志文本,保留排查所需的前缀内容。
|
||||
*
|
||||
* @param text 原始文本
|
||||
* @return 截断后的文本
|
||||
*/
|
||||
private String truncate(String text) {
|
||||
if (text == null || text.length() <= LOG_TEXT_MAX_LENGTH) {
|
||||
return text;
|
||||
}
|
||||
return text.substring(0, LOG_TEXT_MAX_LENGTH) + "...";
|
||||
}
|
||||
|
||||
private Workflow snapshotOrPublishedWorkflow(AgentToolBinding binding) {
|
||||
if (binding.getResourceSnapshot() != null && !binding.getResourceSnapshot().isEmpty()) {
|
||||
Workflow workflow = objectMapper.convertValue(binding.getResourceSnapshot(), Workflow.class);
|
||||
workflow.setId(firstNonNull(workflow.getId(), binding.getTargetId()));
|
||||
return workflow;
|
||||
}
|
||||
return workflowService.getPublishedById(binding.getTargetId());
|
||||
}
|
||||
|
||||
private PluginItem snapshotOrCurrentPlugin(AgentToolBinding binding) {
|
||||
if (binding.getResourceSnapshot() != null && !binding.getResourceSnapshot().isEmpty()) {
|
||||
PluginItem pluginItem = objectMapper.convertValue(binding.getResourceSnapshot(), PluginItem.class);
|
||||
pluginItem.setId(firstNonNull(pluginItem.getId(), binding.getTargetId()));
|
||||
return pluginItem;
|
||||
}
|
||||
return pluginItemService.getById(binding.getTargetId());
|
||||
}
|
||||
|
||||
private Mcp snapshotOrCurrentMcp(AgentToolBinding binding) {
|
||||
if (binding.getResourceSnapshot() != null && !binding.getResourceSnapshot().isEmpty()) {
|
||||
Mcp mcp = objectMapper.convertValue(binding.getResourceSnapshot(), Mcp.class);
|
||||
mcp.setId(firstNonNull(mcp.getId(), binding.getTargetId()));
|
||||
return mcp;
|
||||
}
|
||||
return mcpService.getById(binding.getTargetId());
|
||||
}
|
||||
|
||||
private DocumentCollection snapshotOrPublishedKnowledge(AgentKnowledgeBinding binding) {
|
||||
if (binding.getResourceSnapshot() != null && !binding.getResourceSnapshot().isEmpty()) {
|
||||
DocumentCollection knowledge = objectMapper.convertValue(binding.getResourceSnapshot(), DocumentCollection.class);
|
||||
knowledge.setId(firstNonNull(knowledge.getId(), binding.getKnowledgeId()));
|
||||
return knowledge;
|
||||
}
|
||||
return documentCollectionService.getPublishedById(binding.getKnowledgeId());
|
||||
}
|
||||
|
||||
private BigInteger firstNonNull(BigInteger first, BigInteger second) {
|
||||
return first == null ? second : first;
|
||||
}
|
||||
|
||||
private String firstNonBlank(String first, String second) {
|
||||
return first == null || first.isBlank() ? second : first;
|
||||
}
|
||||
|
||||
private String metadataString(Map<String, Object> metadata, String key) {
|
||||
Object value = metadata == null ? null : metadata.get(key);
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private Map<String, Object> toSchema(Parameter[] parameters) {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
List<String> required = new ArrayList<>();
|
||||
if (parameters != null) {
|
||||
for (Parameter parameter : parameters) {
|
||||
properties.put(parameter.getName(), parameterSchema(parameter));
|
||||
if (parameter.isRequired()) {
|
||||
required.add(parameter.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
schema.put("type", "object");
|
||||
schema.put("properties", properties);
|
||||
schema.put("required", required);
|
||||
return schema;
|
||||
}
|
||||
|
||||
private Map<String, Object> parameterSchema(Parameter parameter) {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("type", parameter.getType() == null ? "string" : parameter.getType());
|
||||
putOptionalString(schema, "description", parameter.getDescription());
|
||||
if (parameter.getChildren() != null && !parameter.getChildren().isEmpty()) {
|
||||
Map<String, Object> children = new LinkedHashMap<>();
|
||||
for (Parameter child : parameter.getChildren()) {
|
||||
if (child != null && child.getName() != null && !child.getName().isBlank()) {
|
||||
children.put(child.getName(), parameterSchema(child));
|
||||
}
|
||||
}
|
||||
if ("array".equalsIgnoreCase(parameter.getType())) {
|
||||
schema.put("items", firstArrayItemSchema(parameter.getChildren()));
|
||||
} else {
|
||||
schema.put("properties", children);
|
||||
}
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
private Map<String, Object> firstArrayItemSchema(List<Parameter> children) {
|
||||
return children.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.map(this::parameterSchema)
|
||||
.orElse(Map.of("type", "string"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入非空字符串字段,避免向模型 function schema 输出 null。
|
||||
*
|
||||
* @param target 目标 schema
|
||||
* @param key 字段名
|
||||
* @param value 字段值
|
||||
*/
|
||||
private void putOptionalString(Map<String, Object> target, String key, String value) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
target.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将工具描述规整为模型协议可接受的字符串。
|
||||
*
|
||||
* @param description 原始描述
|
||||
* @return 非 null 描述
|
||||
*/
|
||||
private String safeDescription(String description) {
|
||||
return description == null ? "" : description;
|
||||
}
|
||||
|
||||
private AgentModelProviderType parseProviderType(String providerType) {
|
||||
if (providerType == null || providerType.isBlank()) {
|
||||
return AgentModelProviderType.OPENAI_COMPATIBLE;
|
||||
}
|
||||
try {
|
||||
return AgentModelProviderType.valueOf(providerType.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return AgentModelProviderType.OPENAI_COMPATIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
private AgentMemoryType memoryTypeValue(Map<String, Object> map, String key) {
|
||||
String value = stringValue(map, key, AgentMemoryType.AUTO_CONTEXT.name());
|
||||
try {
|
||||
return AgentMemoryType.valueOf(value.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new BusinessException("不支持的 Agent 记忆策略:" + value);
|
||||
}
|
||||
}
|
||||
|
||||
private String stringValue(Map<String, Object> map, String key, String defaultValue) {
|
||||
Object value = map == null ? null : map.get(key);
|
||||
return value == null ? defaultValue : String.valueOf(value);
|
||||
}
|
||||
|
||||
private Integer intValue(Map<String, Object> map, String key) {
|
||||
Object value = map == null ? null : map.get(key);
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(String.valueOf(value));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new BusinessException("Agent 配置字段必须是整数:" + key);
|
||||
}
|
||||
}
|
||||
|
||||
private Long longValue(Map<String, Object> map, String key) {
|
||||
Object value = map == null ? null : map.get(key);
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(value));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new BusinessException("Agent 配置字段必须是长整数:" + key);
|
||||
}
|
||||
}
|
||||
|
||||
private Double doubleValue(Map<String, Object> map, String key) {
|
||||
Object value = map == null ? null : map.get(key);
|
||||
if (value instanceof Number number) {
|
||||
return number.doubleValue();
|
||||
}
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Double.parseDouble(String.valueOf(value));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new BusinessException("Agent 配置字段必须是数字:" + key);
|
||||
}
|
||||
}
|
||||
|
||||
private Boolean booleanValue(Map<String, Object> map, String key) {
|
||||
Object value = map == null ? null : map.get(key);
|
||||
if (value instanceof Boolean bool) {
|
||||
return bool;
|
||||
}
|
||||
return value == null ? null : Boolean.parseBoolean(String.valueOf(value));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> mapValue(Map<String, Object> map, String key) {
|
||||
Object value = map == null ? null : map.get(key);
|
||||
if (value == null) {
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
if (value instanceof Map<?, ?> rawMap) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
rawMap.forEach((rawKey, rawValue) -> result.put(String.valueOf(rawKey), rawValue));
|
||||
return result;
|
||||
}
|
||||
throw new BusinessException("Agent 配置字段必须是对象:" + key);
|
||||
}
|
||||
|
||||
private Map<String, String> stringMapValue(Map<String, Object> map, String key) {
|
||||
Map<String, Object> rawMap = mapValue(map, key);
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
rawMap.forEach((rawKey, rawValue) -> {
|
||||
if (rawValue != null) {
|
||||
result.put(rawKey, String.valueOf(rawValue));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package tech.easyflow.agent.runtime;
|
||||
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 草稿态纯文本试用请求。
|
||||
*/
|
||||
public class AgentDraftChatRequest {
|
||||
|
||||
private Agent agent;
|
||||
private List<AgentToolBinding> toolBindings;
|
||||
private List<AgentKnowledgeBinding> knowledgeBindings;
|
||||
private String sessionId;
|
||||
private String prompt;
|
||||
|
||||
/**
|
||||
* 获取 Agent 草稿快照。
|
||||
*
|
||||
* @return Agent 草稿快照
|
||||
*/
|
||||
public Agent getAgent() {
|
||||
return agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Agent 草稿快照。
|
||||
*
|
||||
* @param agent Agent 草稿快照
|
||||
*/
|
||||
public void setAgent(Agent agent) {
|
||||
this.agent = agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具绑定快照。
|
||||
*
|
||||
* @return 工具绑定快照
|
||||
*/
|
||||
public List<AgentToolBinding> getToolBindings() {
|
||||
return toolBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工具绑定快照。
|
||||
*
|
||||
* @param toolBindings 工具绑定快照
|
||||
*/
|
||||
public void setToolBindings(List<AgentToolBinding> toolBindings) {
|
||||
this.toolBindings = toolBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库绑定快照。
|
||||
*
|
||||
* @return 知识库绑定快照
|
||||
*/
|
||||
public List<AgentKnowledgeBinding> getKnowledgeBindings() {
|
||||
return knowledgeBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置知识库绑定快照。
|
||||
*
|
||||
* @param knowledgeBindings 知识库绑定快照
|
||||
*/
|
||||
public void setKnowledgeBindings(List<AgentKnowledgeBinding> knowledgeBindings) {
|
||||
this.knowledgeBindings = knowledgeBindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取草稿试运行会话 ID。
|
||||
*
|
||||
* @return 会话 ID
|
||||
*/
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置草稿试运行会话 ID。
|
||||
*
|
||||
* @param sessionId 会话 ID
|
||||
*/
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户纯文本输入。
|
||||
*
|
||||
* @return 用户输入
|
||||
*/
|
||||
public String getPrompt() {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户纯文本输入。
|
||||
*
|
||||
* @param prompt 用户输入
|
||||
*/
|
||||
public void setPrompt(String prompt) {
|
||||
this.prompt = prompt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
package tech.easyflow.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentResumeRequest;
|
||||
import com.easyagents.agent.runtime.AgentRuntime;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.hitl.AgentResumeToken;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.Disposable;
|
||||
import tech.easyflow.agent.runtime.lock.AgentRunLock;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||
import tech.easyflow.core.runtime.ChatAssistantAccumulator;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Agent 运行态注册表。
|
||||
*/
|
||||
@Component
|
||||
public class AgentRunRegistry {
|
||||
|
||||
private final Map<String, AgentRunContext> runs = new ConcurrentHashMap<>();
|
||||
private final Map<String, String> sessionRuns = new ConcurrentHashMap<>();
|
||||
private final Map<String, String> resumeTokenIndex = new ConcurrentHashMap<>();
|
||||
private final Map<String, Set<String>> requestTokens = new ConcurrentHashMap<>();
|
||||
private final Map<String, RunOwner> owners = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 注册运行态。
|
||||
*
|
||||
* @param context 运行态上下文
|
||||
*/
|
||||
public void register(AgentRunContext context) {
|
||||
if (context == null || context.requestId() == null || context.requestId().isBlank()) {
|
||||
throw new BusinessException("Agent 运行请求 ID 不能为空");
|
||||
}
|
||||
if (context.sessionId() == null || context.sessionId().isBlank()) {
|
||||
throw new BusinessException("Agent 会话 ID 不能为空");
|
||||
}
|
||||
String existingRequestId = sessionRuns.putIfAbsent(context.sessionId(), context.requestId());
|
||||
if (existingRequestId != null && !existingRequestId.equals(context.requestId())) {
|
||||
throw new BusinessException("当前 Agent 会话已有运行中的请求,请稍后再试");
|
||||
}
|
||||
AgentRunContext existing = runs.putIfAbsent(context.requestId(), context);
|
||||
if (existing != null) {
|
||||
sessionRuns.remove(context.sessionId(), context.requestId());
|
||||
throw new BusinessException("当前 Agent 运行请求已存在");
|
||||
}
|
||||
owners.put(context.requestId(), context.owner());
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定运行订阅。
|
||||
*
|
||||
* @param requestId 请求 ID
|
||||
* @param subscription Reactor 订阅
|
||||
*/
|
||||
public void bindSubscription(String requestId, Disposable subscription) {
|
||||
AgentRunContext context = runs.get(requestId);
|
||||
if (context != null) {
|
||||
context.setSubscription(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行态。
|
||||
*
|
||||
* @param requestId 请求 ID
|
||||
* @return 运行态
|
||||
*/
|
||||
public AgentRunContext get(String requestId) {
|
||||
return requestId == null ? null : runs.get(requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消并移除指定会话当前活跃运行。
|
||||
*
|
||||
* <p>草稿试运行清理会删除 AgentScope session。若同一会话仍有 SSE 运行中,
|
||||
* 必须先取消 runtime 订阅并关闭 SSE,避免旧运行继续向已清空的会话写入状态。</p>
|
||||
*
|
||||
* @param sessionId 会话 ID
|
||||
*/
|
||||
public void cancelSession(String sessionId) {
|
||||
cancelSession(sessionId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消并移除指定会话当前活跃运行。
|
||||
*
|
||||
* @param sessionId 会话 ID
|
||||
* @param userId 当前用户 ID,非空时会校验运行归属
|
||||
*/
|
||||
public void cancelSession(String sessionId, String userId) {
|
||||
if (sessionId == null || sessionId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
String requestId = sessionRuns.get(sessionId);
|
||||
if (requestId == null) {
|
||||
return;
|
||||
}
|
||||
assertOwner(requestId, userId);
|
||||
AgentRunContext context = runs.get(requestId);
|
||||
if (context != null) {
|
||||
context.cancelAndComplete();
|
||||
}
|
||||
remove(requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录等待审批的恢复令牌。
|
||||
*
|
||||
* @param requestId 请求 ID
|
||||
* @param resumeToken 恢复令牌
|
||||
*/
|
||||
public void registerResumeToken(String requestId, String resumeToken) {
|
||||
if (requestId != null && resumeToken != null && !resumeToken.isBlank()) {
|
||||
resumeTokenIndex.put(resumeToken, requestId);
|
||||
requestTokens.computeIfAbsent(requestId, ignored -> ConcurrentHashMap.newKeySet()).add(resumeToken);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行结束后移除运行态。
|
||||
*
|
||||
* @param requestId 请求 ID
|
||||
*/
|
||||
public synchronized void remove(String requestId) {
|
||||
if (requestId == null) {
|
||||
return;
|
||||
}
|
||||
AgentRunContext context = runs.remove(requestId);
|
||||
if (context != null) {
|
||||
sessionRuns.remove(context.sessionId(), requestId);
|
||||
context.releaseLock();
|
||||
}
|
||||
owners.remove(requestId);
|
||||
Set<String> tokens = requestTokens.remove(requestId);
|
||||
if (tokens != null) {
|
||||
tokens.forEach(resumeTokenIndex::remove);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批准工具执行。
|
||||
*
|
||||
* @param requestId 请求 ID
|
||||
* @param resumeToken 恢复令牌
|
||||
* @param userId 当前用户 ID
|
||||
*/
|
||||
public void approve(String requestId, String resumeToken, String userId) {
|
||||
submit(requestId, resumeToken, userId, true, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批准工具执行,并在恢复 runtime 前执行持久化消费动作。
|
||||
*
|
||||
* @param requestId 请求 ID
|
||||
* @param resumeToken 恢复令牌
|
||||
* @param userId 当前用户 ID
|
||||
* @param beforeResume runtime resume 前的持久化消费动作
|
||||
*/
|
||||
public void approve(String requestId, String resumeToken, String userId, Runnable beforeResume) {
|
||||
submit(requestId, resumeToken, userId, true, null, beforeResume);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝工具执行。
|
||||
*
|
||||
* @param requestId 请求 ID
|
||||
* @param resumeToken 恢复令牌
|
||||
* @param userId 当前用户 ID
|
||||
* @param reason 拒绝原因
|
||||
*/
|
||||
public void reject(String requestId, String resumeToken, String userId, String reason) {
|
||||
submit(requestId, resumeToken, userId, false, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝工具执行,并在恢复 runtime 前执行持久化消费动作。
|
||||
*
|
||||
* @param requestId 请求 ID
|
||||
* @param resumeToken 恢复令牌
|
||||
* @param userId 当前用户 ID
|
||||
* @param reason 拒绝原因
|
||||
* @param beforeResume runtime resume 前的持久化消费动作
|
||||
*/
|
||||
public void reject(String requestId, String resumeToken, String userId, String reason, Runnable beforeResume) {
|
||||
submit(requestId, resumeToken, userId, false, reason, beforeResume);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前进程是否存在指定 token 对应的活跃运行态。
|
||||
*
|
||||
* @param requestId 请求 ID,可为空
|
||||
* @param resumeToken 恢复令牌
|
||||
* @return true 表示当前节点可直接恢复
|
||||
*/
|
||||
public boolean containsResumeTarget(String requestId, String resumeToken) {
|
||||
try {
|
||||
String resolvedRequestId = resolveRequestId(requestId, resumeToken);
|
||||
return runs.containsKey(resolvedRequestId);
|
||||
} catch (BusinessException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void submit(String requestId, String resumeToken, String userId, boolean approved, String reason) {
|
||||
submit(requestId, resumeToken, userId, approved, reason, null);
|
||||
}
|
||||
|
||||
private synchronized void submit(String requestId,
|
||||
String resumeToken,
|
||||
String userId,
|
||||
boolean approved,
|
||||
String reason,
|
||||
Runnable beforeResume) {
|
||||
String resolvedRequestId = resolveRequestId(requestId, resumeToken);
|
||||
AgentRunContext context = runs.get(resolvedRequestId);
|
||||
if (context == null) {
|
||||
throw new BusinessException("当前 Agent 运行已结束或不在本进程内");
|
||||
}
|
||||
assertOwner(resolvedRequestId, userId);
|
||||
assertResumeTokenBelongsToRequest(resolvedRequestId, resumeToken);
|
||||
if (beforeResume != null) {
|
||||
beforeResume.run();
|
||||
}
|
||||
Set<String> tokens = requestTokens.get(resolvedRequestId);
|
||||
if (tokens != null) {
|
||||
tokens.remove(resumeToken);
|
||||
}
|
||||
resumeTokenIndex.remove(resumeToken);
|
||||
AgentResumeToken token = new AgentResumeToken();
|
||||
token.setValue(resumeToken);
|
||||
AgentResumeRequest request = new AgentResumeRequest();
|
||||
request.setResumeToken(token);
|
||||
request.setApproved(approved);
|
||||
request.setRejectReason(reason);
|
||||
request.getMetadata().put("requestId", resolvedRequestId);
|
||||
request.getMetadata().put("operatorId", userId);
|
||||
context.resume(request);
|
||||
}
|
||||
|
||||
private String resolveRequestId(String requestId, String resumeToken) {
|
||||
if (requestId != null && !requestId.isBlank()) {
|
||||
return requestId;
|
||||
}
|
||||
String resolved = resumeTokenIndex.get(resumeToken);
|
||||
if (resolved == null || resolved.isBlank()) {
|
||||
throw new BusinessException("当前 Agent 运行已结束或不在本进程内");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private void assertResumeTokenBelongsToRequest(String requestId, String resumeToken) {
|
||||
if (resumeToken == null || resumeToken.isBlank()) {
|
||||
throw new BusinessException("Agent 恢复令牌不能为空");
|
||||
}
|
||||
Set<String> tokens = requestTokens.get(requestId);
|
||||
if (tokens == null || !tokens.contains(resumeToken)) {
|
||||
throw new BusinessException("Agent 审批请求已失效");
|
||||
}
|
||||
}
|
||||
|
||||
private void assertOwner(String requestId, String userId) {
|
||||
if (userId == null || userId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
RunOwner owner = owners.get(requestId);
|
||||
if (owner == null) {
|
||||
throw new BusinessException("当前 Agent 运行已结束或不在本进程内");
|
||||
}
|
||||
if (owner.userId() != null && !owner.userId().equals(userId)) {
|
||||
throw new BusinessException("无权处理该 Agent 运行审批");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前运行归属信息。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @param sessionId 会话 ID
|
||||
* @param userId 用户 ID
|
||||
*/
|
||||
public record RunOwner(String agentId, String sessionId, String userId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 单机内存运行态。
|
||||
*
|
||||
*/
|
||||
public static final class AgentRunContext {
|
||||
|
||||
private final String requestId;
|
||||
private final String sessionId;
|
||||
private final AgentRuntime runtime;
|
||||
private final ChatSseEmitter chatSseEmitter;
|
||||
private final ChatRuntimeContext chatContext;
|
||||
private final StringBuilder answer;
|
||||
private final ChatAssistantAccumulator assistantAccumulator;
|
||||
private final AtomicBoolean finished;
|
||||
private final boolean persistChatlog;
|
||||
private final RunOwner owner;
|
||||
private final AgentRunLock.Handle lockHandle;
|
||||
private final Consumer<AgentRuntimeEvent> eventConsumer;
|
||||
private final Consumer<Throwable> errorConsumer;
|
||||
private final Runnable completionHandler;
|
||||
private final AtomicBoolean suspended = new AtomicBoolean(false);
|
||||
private final AtomicReference<Disposable> subscription = new AtomicReference<>();
|
||||
|
||||
/**
|
||||
* 创建运行态。
|
||||
*
|
||||
* @param requestId 请求 ID
|
||||
* @param sessionId 会话 ID
|
||||
* @param runtime 有状态运行时
|
||||
* @param chatSseEmitter SSE 连接
|
||||
* @param chatContext 聊天上下文
|
||||
* @param answer 助手正文累计缓冲
|
||||
* @param assistantAccumulator 助手结构化累计器
|
||||
* @param finished 运行收口标记
|
||||
* @param persistChatlog 是否持久化聊天日志
|
||||
* @param owner 运行归属
|
||||
* @param eventConsumer 运行事件处理器
|
||||
* @param errorConsumer 错误处理器
|
||||
* @param completionHandler 完成处理器
|
||||
*/
|
||||
public AgentRunContext(String requestId,
|
||||
String sessionId,
|
||||
AgentRuntime runtime,
|
||||
ChatSseEmitter chatSseEmitter,
|
||||
ChatRuntimeContext chatContext,
|
||||
StringBuilder answer,
|
||||
ChatAssistantAccumulator assistantAccumulator,
|
||||
AtomicBoolean finished,
|
||||
boolean persistChatlog,
|
||||
RunOwner owner,
|
||||
AgentRunLock.Handle lockHandle,
|
||||
Consumer<AgentRuntimeEvent> eventConsumer,
|
||||
Consumer<Throwable> errorConsumer,
|
||||
Runnable completionHandler) {
|
||||
this.requestId = requestId;
|
||||
this.sessionId = sessionId;
|
||||
this.runtime = runtime;
|
||||
this.chatSseEmitter = chatSseEmitter;
|
||||
this.chatContext = chatContext;
|
||||
this.answer = answer;
|
||||
this.assistantAccumulator = assistantAccumulator;
|
||||
this.finished = finished;
|
||||
this.persistChatlog = persistChatlog;
|
||||
this.owner = owner;
|
||||
this.lockHandle = lockHandle;
|
||||
this.eventConsumer = eventConsumer;
|
||||
this.errorConsumer = errorConsumer;
|
||||
this.completionHandler = completionHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求 ID。
|
||||
*
|
||||
* @return 请求 ID
|
||||
*/
|
||||
public String requestId() {
|
||||
return requestId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话 ID。
|
||||
*
|
||||
* @return 会话 ID
|
||||
*/
|
||||
public String sessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行归属。
|
||||
*
|
||||
* @return 运行归属
|
||||
*/
|
||||
public RunOwner owner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行事件处理器。
|
||||
*
|
||||
* @return 运行事件处理器
|
||||
*/
|
||||
public Consumer<AgentRuntimeEvent> eventConsumer() {
|
||||
return eventConsumer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误处理器。
|
||||
*
|
||||
* @return 错误处理器
|
||||
*/
|
||||
public Consumer<Throwable> errorConsumer() {
|
||||
return errorConsumer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完成处理器。
|
||||
*
|
||||
* @return 完成处理器
|
||||
*/
|
||||
public Runnable completionHandler() {
|
||||
return completionHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记当前运行已进入 HITL 挂起态。
|
||||
*/
|
||||
public void markSuspended() {
|
||||
suspended.set(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前运行是否处于 HITL 挂起态。
|
||||
*
|
||||
* @return true 表示等待审批恢复
|
||||
*/
|
||||
public boolean isSuspended() {
|
||||
return suspended.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定运行订阅。
|
||||
*
|
||||
* @param subscription Reactor 订阅
|
||||
*/
|
||||
public void setSubscription(Disposable subscription) {
|
||||
if (subscription == null) {
|
||||
return;
|
||||
}
|
||||
Disposable previous = this.subscription.getAndSet(subscription);
|
||||
if (previous != null && !previous.isDisposed()) {
|
||||
previous.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前运行订阅。
|
||||
*/
|
||||
public void cancel() {
|
||||
Disposable subscription = this.subscription.getAndSet(null);
|
||||
if (subscription != null && !subscription.isDisposed()) {
|
||||
subscription.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消当前运行并关闭 SSE。
|
||||
*
|
||||
* <p>该方法用于调用方主动清理会话的场景。它不通过 runtime 事件链发送取消事件,
|
||||
* 因为调用方此时已经明确要求丢弃当前草稿会话。</p>
|
||||
*/
|
||||
public void cancelAndComplete() {
|
||||
cancel();
|
||||
if (finished.compareAndSet(false, true) && chatSseEmitter != null) {
|
||||
chatSseEmitter.complete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放当前运行持有的分布式锁。
|
||||
*/
|
||||
public void releaseLock() {
|
||||
if (lockHandle != null) {
|
||||
lockHandle.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过同一个 runtime 恢复挂起运行,事件继续写入原 SSE。
|
||||
*
|
||||
* @param request 恢复请求
|
||||
*/
|
||||
public void resume(AgentResumeRequest request) {
|
||||
suspended.set(false);
|
||||
Disposable subscription = runtime.resume(request).subscribe(eventConsumer, errorConsumer, completionHandler);
|
||||
setSubscription(subscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,72 @@
|
||||
package tech.easyflow.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentDefinition;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetriever;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolInvoker;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Agent 运行时编译结果。
|
||||
*/
|
||||
public class AgentRuntimeBundle {
|
||||
|
||||
private AgentDefinition definition;
|
||||
private Map<String, AgentToolInvoker> toolInvokers = new LinkedHashMap<>();
|
||||
private Map<String, AgentKnowledgeRetriever> knowledgeRetrievers = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 获取 Agent 定义。
|
||||
*
|
||||
* @return Agent 定义
|
||||
*/
|
||||
public AgentDefinition getDefinition() {
|
||||
return definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Agent 定义。
|
||||
*
|
||||
* @param definition Agent 定义
|
||||
*/
|
||||
public void setDefinition(AgentDefinition definition) {
|
||||
this.definition = definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具调用器。
|
||||
*
|
||||
* @return 工具调用器
|
||||
*/
|
||||
public Map<String, AgentToolInvoker> getToolInvokers() {
|
||||
return toolInvokers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工具调用器。
|
||||
*
|
||||
* @param toolInvokers 工具调用器
|
||||
*/
|
||||
public void setToolInvokers(Map<String, AgentToolInvoker> toolInvokers) {
|
||||
this.toolInvokers = toolInvokers == null ? new LinkedHashMap<>() : toolInvokers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库检索器。
|
||||
*
|
||||
* @return 知识库检索器
|
||||
*/
|
||||
public Map<String, AgentKnowledgeRetriever> getKnowledgeRetrievers() {
|
||||
return knowledgeRetrievers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置知识库检索器。
|
||||
*
|
||||
* @param knowledgeRetrievers 知识库检索器
|
||||
*/
|
||||
public void setKnowledgeRetrievers(Map<String, AgentKnowledgeRetriever> knowledgeRetrievers) {
|
||||
this.knowledgeRetrievers = knowledgeRetrievers == null ? new LinkedHashMap<>() : knowledgeRetrievers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package tech.easyflow.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntime;
|
||||
|
||||
/**
|
||||
* Agent 运行时工厂
|
||||
*/
|
||||
public interface AgentRuntimeFactory {
|
||||
|
||||
/**
|
||||
* 创建新的有状态 Agent 运行时实例。
|
||||
*
|
||||
* @return Agent 运行时实例
|
||||
*/
|
||||
AgentRuntime create();
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package tech.easyflow.agent.runtime;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.agent.runtime.hitl.AgentHitlPendingService;
|
||||
import tech.easyflow.agent.runtime.session.EasyFlowAgentSessionStore;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Agent 运行态持久化状态清理服务。
|
||||
*/
|
||||
@Service
|
||||
public class AgentRuntimeStateCleanupService {
|
||||
|
||||
private final AgentRunRegistry agentRunRegistry;
|
||||
private final EasyFlowAgentSessionStore sessionStore;
|
||||
private final AgentHitlPendingService pendingService;
|
||||
|
||||
/**
|
||||
* 创建清理服务。
|
||||
*
|
||||
* @param agentRunRegistry 当前节点运行态注册表
|
||||
* @param sessionStore AgentScope session store
|
||||
* @param pendingService HITL pending 服务
|
||||
*/
|
||||
public AgentRuntimeStateCleanupService(AgentRunRegistry agentRunRegistry,
|
||||
EasyFlowAgentSessionStore sessionStore,
|
||||
AgentHitlPendingService pendingService) {
|
||||
this.agentRunRegistry = agentRunRegistry;
|
||||
this.sessionStore = sessionStore;
|
||||
this.pendingService = pendingService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理指定正式聊天会话关联的 Agent 运行态。
|
||||
*
|
||||
* @param chatSessionId chatlog 会话 ID
|
||||
* @param userId 当前用户 ID
|
||||
*/
|
||||
public void clearChatSession(BigInteger chatSessionId, BigInteger userId) {
|
||||
if (chatSessionId == null) {
|
||||
return;
|
||||
}
|
||||
String runtimeSessionId = chatSessionId.toString();
|
||||
agentRunRegistry.cancelSession(runtimeSessionId, userId == null ? null : userId.toString());
|
||||
sessionStore.deleteByChatSessionId(chatSessionId);
|
||||
pendingService.deleteByChatSessionId(chatSessionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package tech.easyflow.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntime;
|
||||
import com.easyagents.agent.runtime.agentscope.AgentScopeReActRuntime;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* ReActAgent 运行时工厂。
|
||||
*/
|
||||
@Component
|
||||
public class AgentScopeRuntimeFactory implements AgentRuntimeFactory {
|
||||
|
||||
/**
|
||||
* 创建新的 ReAct 运行时实例。
|
||||
*
|
||||
* @return 新的运行时实例
|
||||
*/
|
||||
@Override
|
||||
public AgentRuntime create() {
|
||||
return new AgentScopeReActRuntime();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package tech.easyflow.agent.runtime;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Agent 工具调用人工确认事件载荷。
|
||||
*/
|
||||
public class AgentToolHitlPayload {
|
||||
|
||||
private String requestId;
|
||||
private String resumeToken;
|
||||
private String sessionId;
|
||||
private String agentId;
|
||||
private String toolCallId;
|
||||
private String toolName;
|
||||
private String toolDisplayName;
|
||||
private String toolType;
|
||||
private Map<String, Object> input = new LinkedHashMap<>();
|
||||
private String expiresAt;
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 获取运行请求 ID。
|
||||
*
|
||||
* @return 运行请求 ID
|
||||
*/
|
||||
public String getRequestId() {
|
||||
return requestId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置运行请求 ID。
|
||||
*
|
||||
* @param requestId 运行请求 ID
|
||||
*/
|
||||
public void setRequestId(String requestId) {
|
||||
this.requestId = requestId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取恢复令牌。
|
||||
*
|
||||
* @return 恢复令牌
|
||||
*/
|
||||
public String getResumeToken() {
|
||||
return resumeToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置恢复令牌。
|
||||
*
|
||||
* @param resumeToken 恢复令牌
|
||||
*/
|
||||
public void setResumeToken(String resumeToken) {
|
||||
this.resumeToken = resumeToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话 ID。
|
||||
*
|
||||
* @return 会话 ID
|
||||
*/
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置会话 ID。
|
||||
*
|
||||
* @param sessionId 会话 ID
|
||||
*/
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent ID。
|
||||
*
|
||||
* @return Agent ID
|
||||
*/
|
||||
public String getAgentId() {
|
||||
return agentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Agent ID。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
*/
|
||||
public void setAgentId(String agentId) {
|
||||
this.agentId = agentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具调用 ID。
|
||||
*
|
||||
* @return 工具调用 ID
|
||||
*/
|
||||
public String getToolCallId() {
|
||||
return toolCallId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工具调用 ID。
|
||||
*
|
||||
* @param toolCallId 工具调用 ID
|
||||
*/
|
||||
public void setToolCallId(String toolCallId) {
|
||||
this.toolCallId = toolCallId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具名称。
|
||||
*
|
||||
* @return 工具名称
|
||||
*/
|
||||
public String getToolName() {
|
||||
return toolName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工具名称。
|
||||
*
|
||||
* @param toolName 工具名称
|
||||
*/
|
||||
public void setToolName(String toolName) {
|
||||
this.toolName = toolName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具展示名称。
|
||||
*
|
||||
* @return 工具展示名称
|
||||
*/
|
||||
public String getToolDisplayName() {
|
||||
return toolDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工具展示名称。
|
||||
*
|
||||
* @param toolDisplayName 工具展示名称
|
||||
*/
|
||||
public void setToolDisplayName(String toolDisplayName) {
|
||||
this.toolDisplayName = toolDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具类型。
|
||||
*
|
||||
* @return 工具类型
|
||||
*/
|
||||
public String getToolType() {
|
||||
return toolType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工具类型。
|
||||
*
|
||||
* @param toolType 工具类型
|
||||
*/
|
||||
public void setToolType(String toolType) {
|
||||
this.toolType = toolType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具入参。
|
||||
*
|
||||
* @return 工具入参
|
||||
*/
|
||||
public Map<String, Object> getInput() {
|
||||
return input;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工具入参。
|
||||
*
|
||||
* @param input 工具入参
|
||||
*/
|
||||
public void setInput(Map<String, Object> input) {
|
||||
this.input = input == null ? new LinkedHashMap<>() : input;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过期时间。
|
||||
*
|
||||
* @return 过期时间
|
||||
*/
|
||||
public String getExpiresAt() {
|
||||
return expiresAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置过期时间。
|
||||
*
|
||||
* @param expiresAt 过期时间
|
||||
*/
|
||||
public void setExpiresAt(String expiresAt) {
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取扩展元数据。
|
||||
*
|
||||
* @return 扩展元数据
|
||||
*/
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置扩展元数据。
|
||||
*
|
||||
* @param metadata 扩展元数据
|
||||
*/
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package tech.easyflow.agent.runtime.event;
|
||||
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
|
||||
/**
|
||||
* Agent 运行事件落库记录器。
|
||||
*/
|
||||
public interface AgentRunEventRecorder {
|
||||
|
||||
/**
|
||||
* 记录运行事件。
|
||||
*
|
||||
* @param requestId 请求 ID
|
||||
* @param chatContext 聊天上下文
|
||||
* @param event 运行时事件
|
||||
*/
|
||||
void record(String requestId, ChatRuntimeContext chatContext, AgentRuntimeEvent event);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package tech.easyflow.agent.runtime.event;
|
||||
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.agent.entity.AgentRunEventRecord;
|
||||
import tech.easyflow.agent.mapper.AgentRunEventRecordMapper;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeExtKeys;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MySQL Agent 运行事件记录器。
|
||||
*/
|
||||
@Service
|
||||
public class MySqlAgentRunEventRecorder implements AgentRunEventRecorder {
|
||||
|
||||
private final AgentRunEventRecordMapper eventRecordMapper;
|
||||
|
||||
/**
|
||||
* 创建记录器。
|
||||
*
|
||||
* @param eventRecordMapper 事件 Mapper
|
||||
*/
|
||||
public MySqlAgentRunEventRecorder(AgentRunEventRecordMapper eventRecordMapper) {
|
||||
this.eventRecordMapper = eventRecordMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void record(String requestId, ChatRuntimeContext chatContext, AgentRuntimeEvent event) {
|
||||
if (event == null || event.getEventType() == null || !shouldPersist(event.getEventType())) {
|
||||
return;
|
||||
}
|
||||
AgentRunEventRecord record = new AgentRunEventRecord();
|
||||
record.setTenantId(chatContext == null ? null : chatContext.getTenantId());
|
||||
record.setAgentId(resolveAgentId(event, chatContext));
|
||||
record.setChatSessionId(chatContext == null ? null : chatContext.getSessionId());
|
||||
record.setRoundId(resolveNumber(chatContext, ChatRuntimeExtKeys.CURRENT_ROUND_ID));
|
||||
record.setRoundNo(resolveInteger(chatContext, ChatRuntimeExtKeys.CURRENT_ROUND_NO));
|
||||
record.setVariantIndex(resolveInteger(chatContext, ChatRuntimeExtKeys.CURRENT_VARIANT_INDEX, 1));
|
||||
record.setRequestId(firstText(requestId, stringValue(event.getPayload().get("requestId"))));
|
||||
record.setEventId(event.getEventId());
|
||||
record.setEventType(event.getEventType().name());
|
||||
record.setEventPhase(stringValue(event.getMetadata().get("phase")));
|
||||
record.setToolCallId(firstText(event.getToolCallId(), stringValue(event.getPayload().get("toolCallId"))));
|
||||
record.setPayloadJson(new LinkedHashMap<>(event.getPayload() == null ? Map.of() : event.getPayload()));
|
||||
record.setMetadataJson(new LinkedHashMap<>(event.getMetadata() == null ? Map.of() : event.getMetadata()));
|
||||
record.setCreated(toDate(event.getCreatedAt()));
|
||||
record.setCreatedBy(chatContext == null ? null : chatContext.getUserId());
|
||||
eventRecordMapper.insert(record);
|
||||
}
|
||||
|
||||
private boolean shouldPersist(AgentRuntimeEventType type) {
|
||||
return type != AgentRuntimeEventType.MESSAGE_DELTA
|
||||
&& type != AgentRuntimeEventType.REASONING_DELTA
|
||||
&& type != AgentRuntimeEventType.STARTED
|
||||
&& type != AgentRuntimeEventType.COMPLETED;
|
||||
}
|
||||
|
||||
private BigInteger resolveAgentId(AgentRuntimeEvent event, ChatRuntimeContext chatContext) {
|
||||
String agentId = firstText(event.getAgentId(), stringValue(event.getPayload().get("agentId")));
|
||||
if (agentId != null && !agentId.isBlank()) {
|
||||
try {
|
||||
return new BigInteger(agentId);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return chatContext == null ? null : chatContext.getAssistantId();
|
||||
}
|
||||
}
|
||||
return chatContext == null ? null : chatContext.getAssistantId();
|
||||
}
|
||||
|
||||
private BigInteger resolveNumber(ChatRuntimeContext context, String key) {
|
||||
Object value = context == null || context.getExt() == null ? null : context.getExt().get(key);
|
||||
if (value instanceof BigInteger bigInteger) {
|
||||
return bigInteger;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return BigInteger.valueOf(number.longValue());
|
||||
}
|
||||
String text = stringValue(value);
|
||||
if (text == null || text.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new BigInteger(text);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer resolveInteger(ChatRuntimeContext context, String key) {
|
||||
return resolveInteger(context, key, null);
|
||||
}
|
||||
|
||||
private Integer resolveInteger(ChatRuntimeContext context, String key, Integer defaultValue) {
|
||||
Object value = context == null || context.getExt() == null ? null : context.getExt().get(key);
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
String text = stringValue(value);
|
||||
if (text == null || text.isBlank()) {
|
||||
return defaultValue;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(text);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private Date toDate(Instant instant) {
|
||||
return instant == null ? new Date() : Date.from(instant);
|
||||
}
|
||||
|
||||
private String firstText(String left, String right) {
|
||||
return left != null && !left.isBlank() ? left : right;
|
||||
}
|
||||
|
||||
private String stringValue(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.easyflow.agent.runtime.hitl;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.agent.entity.AgentHitlPending;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 工具审批 pending 过期清理任务。
|
||||
*/
|
||||
@Component
|
||||
public class AgentHitlPendingExpirationTask {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AgentHitlPendingExpirationTask.class);
|
||||
private static final int BATCH_SIZE = 100;
|
||||
|
||||
private final AgentHitlPendingService pendingService;
|
||||
|
||||
/**
|
||||
* 创建任务。
|
||||
*
|
||||
* @param pendingService pending 服务
|
||||
*/
|
||||
public AgentHitlPendingExpirationTask(AgentHitlPendingService pendingService) {
|
||||
this.pendingService = pendingService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 定期将超时 pending 标记为 EXPIRED。
|
||||
*/
|
||||
@Scheduled(fixedDelayString = "${easyflow.agent.runtime.hitl-expire-scan-delay:60000}", initialDelay = 60000L)
|
||||
public void expirePending() {
|
||||
try {
|
||||
List<AgentHitlPending> expired = pendingService.expirePending(BATCH_SIZE);
|
||||
if (!expired.isEmpty()) {
|
||||
LOG.info("Expired Agent HITL pending records, count={}", expired.size());
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
LOG.warn("Expire Agent HITL pending records failed, message={}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package tech.easyflow.agent.runtime.hitl;
|
||||
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import tech.easyflow.agent.entity.AgentHitlPending;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 工具审批 pending 持久化服务。
|
||||
*/
|
||||
public interface AgentHitlPendingService {
|
||||
|
||||
/**
|
||||
* 从 TOOL_APPROVAL_REQUIRED 事件创建或刷新 pending。
|
||||
*
|
||||
* @param requestId 请求 ID
|
||||
* @param chatContext 聊天上下文
|
||||
* @param event 运行时事件
|
||||
*/
|
||||
void recordApprovalRequired(String requestId, ChatRuntimeContext chatContext, AgentRuntimeEvent event);
|
||||
|
||||
/**
|
||||
* 批准并原子消费 pending。
|
||||
*
|
||||
* @param resumeToken 恢复令牌
|
||||
* @param operatorId 操作人 ID
|
||||
* @return pending 记录
|
||||
*/
|
||||
AgentHitlPending approve(String resumeToken, BigInteger operatorId);
|
||||
|
||||
/**
|
||||
* 拒绝并原子消费 pending。
|
||||
*
|
||||
* @param resumeToken 恢复令牌
|
||||
* @param operatorId 操作人 ID
|
||||
* @param reason 拒绝原因
|
||||
* @return pending 记录
|
||||
*/
|
||||
AgentHitlPending reject(String resumeToken, BigInteger operatorId, String reason);
|
||||
|
||||
/**
|
||||
* 取消指定请求的 pending。
|
||||
*
|
||||
* @param requestId 请求 ID
|
||||
* @param reason 取消原因
|
||||
*/
|
||||
void cancelByRequestId(String requestId, String reason);
|
||||
|
||||
/**
|
||||
* 删除指定聊天会话的 pending。
|
||||
*
|
||||
* @param chatSessionId 聊天会话 ID
|
||||
*/
|
||||
void deleteByChatSessionId(BigInteger chatSessionId);
|
||||
|
||||
/**
|
||||
* 删除指定运行会话的 pending。
|
||||
*
|
||||
* @param runtimeSessionId 运行会话 ID
|
||||
*/
|
||||
void deleteByRuntimeSessionId(String runtimeSessionId);
|
||||
|
||||
/**
|
||||
* 过期 pending 并返回被过期的记录。
|
||||
*
|
||||
* @param limit 每批数量
|
||||
* @return 过期记录
|
||||
*/
|
||||
List<AgentHitlPending> expirePending(int limit);
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package tech.easyflow.agent.runtime.hitl;
|
||||
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.agent.config.AgentRuntimeProperties;
|
||||
import tech.easyflow.agent.entity.AgentHitlPending;
|
||||
import tech.easyflow.agent.mapper.AgentHitlPendingMapper;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Agent 工具审批 pending 持久化服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class AgentHitlPendingServiceImpl implements AgentHitlPendingService {
|
||||
|
||||
private final AgentHitlPendingMapper pendingMapper;
|
||||
private final AgentRuntimeProperties properties;
|
||||
|
||||
/**
|
||||
* 创建服务。
|
||||
*
|
||||
* @param pendingMapper pending Mapper
|
||||
* @param properties Agent 运行态配置
|
||||
*/
|
||||
public AgentHitlPendingServiceImpl(AgentHitlPendingMapper pendingMapper,
|
||||
AgentRuntimeProperties properties) {
|
||||
this.pendingMapper = pendingMapper;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordApprovalRequired(String requestId, ChatRuntimeContext chatContext, AgentRuntimeEvent event) {
|
||||
if (event == null || event.getPayload() == null) {
|
||||
return;
|
||||
}
|
||||
String resumeToken = stringValue(event.getPayload().get("resumeToken"));
|
||||
if (!StringUtils.hasText(resumeToken)) {
|
||||
return;
|
||||
}
|
||||
AgentHitlPending pending = findByToken(resumeToken);
|
||||
Date now = new Date();
|
||||
if (pending == null) {
|
||||
pending = new AgentHitlPending();
|
||||
pending.setResumeToken(resumeToken);
|
||||
pending.setCreated(now);
|
||||
pending.setCreatedBy(chatContext == null ? null : chatContext.getUserId());
|
||||
pending.setIsDeleted(0);
|
||||
}
|
||||
pending.setTenantId(chatContext == null ? null : chatContext.getTenantId());
|
||||
pending.setAgentId(resolveAgentId(event, chatContext));
|
||||
pending.setChatSessionId(chatContext == null ? null : chatContext.getSessionId());
|
||||
pending.setRuntimeSessionId(firstText(event.getSessionId(), stringValue(event.getPayload().get("sessionId"))));
|
||||
pending.setRequestId(requestId);
|
||||
pending.setToolCallId(firstText(event.getToolCallId(), stringValue(event.getPayload().get("toolCallId"))));
|
||||
pending.setToolName(stringValue(event.getPayload().get("toolName")));
|
||||
pending.setToolInputJson(mapValue(firstNonNull(event.getPayload().get("toolInput"), event.getPayload().get("input"))));
|
||||
pending.setStatus(AgentHitlPendingStatus.PENDING.name());
|
||||
pending.setExpiresAt(resolveExpiresAt(event));
|
||||
pending.setMetadataJson(metadata(event));
|
||||
pending.setModified(now);
|
||||
pending.setModifiedBy(chatContext == null ? null : chatContext.getUserId());
|
||||
pendingMapper.insertOrUpdate(pending);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AgentHitlPending approve(String resumeToken, BigInteger operatorId) {
|
||||
return consume(resumeToken, operatorId, AgentHitlPendingStatus.APPROVED, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AgentHitlPending reject(String resumeToken, BigInteger operatorId, String reason) {
|
||||
return consume(resumeToken, operatorId, AgentHitlPendingStatus.REJECTED, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelByRequestId(String requestId, String reason) {
|
||||
if (!StringUtils.hasText(requestId)) {
|
||||
return;
|
||||
}
|
||||
List<AgentHitlPending> records = pendingMapper.selectListByQuery(QueryWrapper.create()
|
||||
.eq("request_id", requestId)
|
||||
.eq("status", AgentHitlPendingStatus.PENDING.name())
|
||||
.eq("is_deleted", 0));
|
||||
Date now = new Date();
|
||||
for (AgentHitlPending record : records) {
|
||||
record.setStatus(AgentHitlPendingStatus.CANCELLED.name());
|
||||
record.setRejectReason(reason);
|
||||
record.setConsumedAt(now);
|
||||
record.setModified(now);
|
||||
pendingMapper.update(record);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByChatSessionId(BigInteger chatSessionId) {
|
||||
if (chatSessionId == null) {
|
||||
return;
|
||||
}
|
||||
List<AgentHitlPending> records = pendingMapper.selectListByQuery(QueryWrapper.create()
|
||||
.eq("chat_session_id", chatSessionId)
|
||||
.eq("is_deleted", 0));
|
||||
softDelete(records);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByRuntimeSessionId(String runtimeSessionId) {
|
||||
if (!StringUtils.hasText(runtimeSessionId)) {
|
||||
return;
|
||||
}
|
||||
List<AgentHitlPending> records = pendingMapper.selectListByQuery(QueryWrapper.create()
|
||||
.eq("runtime_session_id", runtimeSessionId)
|
||||
.eq("is_deleted", 0));
|
||||
softDelete(records);
|
||||
}
|
||||
|
||||
private void softDelete(List<AgentHitlPending> records) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Date now = new Date();
|
||||
for (AgentHitlPending record : records) {
|
||||
record.setIsDeleted(1);
|
||||
record.setModified(now);
|
||||
pendingMapper.update(record);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<AgentHitlPending> expirePending(int limit) {
|
||||
List<AgentHitlPending> records = pendingMapper.selectListByQuery(QueryWrapper.create()
|
||||
.eq("status", AgentHitlPendingStatus.PENDING.name())
|
||||
.eq("is_deleted", 0)
|
||||
.le("expires_at", new Date())
|
||||
.limit(Math.max(1, limit)));
|
||||
Date now = new Date();
|
||||
for (AgentHitlPending record : records) {
|
||||
record.setStatus(AgentHitlPendingStatus.EXPIRED.name());
|
||||
record.setRejectReason("审批超时,已自动拒绝");
|
||||
record.setConsumedAt(now);
|
||||
record.setModified(now);
|
||||
pendingMapper.update(record);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
private AgentHitlPending consume(String resumeToken,
|
||||
BigInteger operatorId,
|
||||
AgentHitlPendingStatus targetStatus,
|
||||
String reason) {
|
||||
if (!StringUtils.hasText(resumeToken)) {
|
||||
throw new BusinessException("Agent 恢复令牌不能为空");
|
||||
}
|
||||
AgentHitlPending pending = findByToken(resumeToken);
|
||||
if (pending == null || Integer.valueOf(1).equals(pending.getIsDeleted())) {
|
||||
throw new BusinessException("Agent 审批请求不存在或已失效");
|
||||
}
|
||||
if (!AgentHitlPendingStatus.PENDING.name().equals(pending.getStatus())) {
|
||||
throw new BusinessException("Agent 审批请求已处理");
|
||||
}
|
||||
if (pending.getExpiresAt() != null && pending.getExpiresAt().before(new Date())) {
|
||||
markConsumed(pending, operatorId, AgentHitlPendingStatus.EXPIRED, "审批超时,已自动拒绝");
|
||||
throw new BusinessException("Agent 审批请求已过期");
|
||||
}
|
||||
if (!markConsumed(pending, operatorId, targetStatus, reason)) {
|
||||
throw new BusinessException("Agent 审批请求已处理");
|
||||
}
|
||||
pending.setStatus(targetStatus.name());
|
||||
pending.setRejectReason(reason);
|
||||
pending.setConsumedAt(new Date());
|
||||
pending.setModifiedBy(operatorId);
|
||||
return pending;
|
||||
}
|
||||
|
||||
private boolean markConsumed(AgentHitlPending pending,
|
||||
BigInteger operatorId,
|
||||
AgentHitlPendingStatus targetStatus,
|
||||
String reason) {
|
||||
Date now = new Date();
|
||||
AgentHitlPending update = new AgentHitlPending();
|
||||
update.setStatus(targetStatus.name());
|
||||
update.setRejectReason(reason);
|
||||
update.setConsumedAt(now);
|
||||
update.setModified(now);
|
||||
update.setModifiedBy(operatorId);
|
||||
// 用 status=PENDING 作为消费条件,避免两个审批请求同时把同一个 token 消费两次。
|
||||
return pendingMapper.updateByQuery(update, QueryWrapper.create()
|
||||
.eq("id", pending.getId())
|
||||
.eq("status", AgentHitlPendingStatus.PENDING.name())
|
||||
.eq("is_deleted", 0)) > 0;
|
||||
}
|
||||
|
||||
private AgentHitlPending findByToken(String resumeToken) {
|
||||
return pendingMapper.selectOneByQuery(QueryWrapper.create()
|
||||
.eq("resume_token", resumeToken)
|
||||
.eq("is_deleted", 0)
|
||||
.limit(1));
|
||||
}
|
||||
|
||||
private BigInteger resolveAgentId(AgentRuntimeEvent event, ChatRuntimeContext chatContext) {
|
||||
String agentId = firstText(event.getAgentId(), stringValue(event.getPayload().get("agentId")));
|
||||
if (StringUtils.hasText(agentId)) {
|
||||
try {
|
||||
return new BigInteger(agentId);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return chatContext == null ? null : chatContext.getAssistantId();
|
||||
}
|
||||
}
|
||||
return chatContext == null ? null : chatContext.getAssistantId();
|
||||
}
|
||||
|
||||
private Map<String, Object> metadata(AgentRuntimeEvent event) {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
metadata.putAll(event.getMetadata() == null ? Map.of() : event.getMetadata());
|
||||
Object approvalMetadata = event.getPayload().get("approvalMetadata");
|
||||
if (approvalMetadata instanceof Map<?, ?> map) {
|
||||
map.forEach((key, value) -> metadata.put(String.valueOf(key), value));
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> mapValue(Object value) {
|
||||
if (value instanceof Map<?, ?> map) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
map.forEach((key, item) -> result.put(String.valueOf(key), item));
|
||||
return result;
|
||||
}
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
private Date dateValue(Object value) {
|
||||
if (value instanceof Date date) {
|
||||
return date;
|
||||
}
|
||||
String text = stringValue(value);
|
||||
if (!StringUtils.hasText(text)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Date.from(Instant.parse(text));
|
||||
} catch (RuntimeException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Date resolveExpiresAt(AgentRuntimeEvent event) {
|
||||
Date eventExpiresAt = dateValue(event.getPayload().get("expiresAt"));
|
||||
if (eventExpiresAt != null) {
|
||||
return eventExpiresAt;
|
||||
}
|
||||
return Date.from(Instant.now().plus(properties.getHitlPendingTimeout()));
|
||||
}
|
||||
|
||||
private Object firstNonNull(Object left, Object right) {
|
||||
return left == null ? right : left;
|
||||
}
|
||||
|
||||
private String firstText(String left, String right) {
|
||||
return StringUtils.hasText(left) ? left : right;
|
||||
}
|
||||
|
||||
private String stringValue(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package tech.easyflow.agent.runtime.hitl;
|
||||
|
||||
/**
|
||||
* Agent HITL pending 状态。
|
||||
*/
|
||||
public enum AgentHitlPendingStatus {
|
||||
/**
|
||||
* 等待审批。
|
||||
*/
|
||||
PENDING,
|
||||
|
||||
/**
|
||||
* 已批准。
|
||||
*/
|
||||
APPROVED,
|
||||
|
||||
/**
|
||||
* 已拒绝。
|
||||
*/
|
||||
REJECTED,
|
||||
|
||||
/**
|
||||
* 已过期。
|
||||
*/
|
||||
EXPIRED,
|
||||
|
||||
/**
|
||||
* 已取消。
|
||||
*/
|
||||
CANCELLED
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package tech.easyflow.agent.runtime.lock;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Agent 会话级运行锁。
|
||||
*/
|
||||
public interface AgentRunLock {
|
||||
|
||||
/**
|
||||
* 获取指定 Agent 会话的运行锁。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @param sessionId 运行时会话 ID
|
||||
* @return 锁句柄
|
||||
*/
|
||||
Handle acquire(BigInteger agentId, String sessionId);
|
||||
|
||||
/**
|
||||
* Agent 运行锁句柄。
|
||||
*/
|
||||
interface Handle extends AutoCloseable {
|
||||
|
||||
/**
|
||||
* 续期锁。
|
||||
*
|
||||
* @return 续期成功时为 true
|
||||
*/
|
||||
boolean renew();
|
||||
|
||||
/**
|
||||
* 释放锁。
|
||||
*/
|
||||
void release();
|
||||
|
||||
@Override
|
||||
default void close() {
|
||||
release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package tech.easyflow.agent.runtime.lock;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import tech.easyflow.agent.config.AgentRuntimeProperties;
|
||||
import tech.easyflow.common.cache.RedisLockExecutor;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* 基于 Redis 的 Agent 会话级运行锁。
|
||||
*/
|
||||
@Component
|
||||
public class RedisAgentRunLock implements AgentRunLock {
|
||||
|
||||
private static final String LOCK_PREFIX = "easyflow:agent:run:";
|
||||
private static final ScheduledExecutorService RENEW_EXECUTOR = Executors.newSingleThreadScheduledExecutor(task -> {
|
||||
Thread thread = new Thread(task, "agent-run-lock-renew");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
|
||||
private final RedisLockExecutor redisLockExecutor;
|
||||
private final AgentRuntimeProperties properties;
|
||||
|
||||
/**
|
||||
* 创建 Redis Agent 运行锁。
|
||||
*
|
||||
* @param redisLockExecutor Redis 锁执行器
|
||||
* @param properties Agent 运行态配置
|
||||
*/
|
||||
public RedisAgentRunLock(RedisLockExecutor redisLockExecutor, AgentRuntimeProperties properties) {
|
||||
this.redisLockExecutor = redisLockExecutor;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Handle acquire(BigInteger agentId, String sessionId) {
|
||||
try {
|
||||
RedisLockExecutor.LockHandle handle = redisLockExecutor.acquire(
|
||||
lockKey(agentId, sessionId),
|
||||
properties.getLockWaitTimeout(),
|
||||
properties.getLockLeaseTimeout());
|
||||
return new RedisHandle(handle, scheduleRenew(handle));
|
||||
} catch (IllegalStateException e) {
|
||||
throw new BusinessException("当前 Agent 会话正在运行,请稍后再试");
|
||||
}
|
||||
}
|
||||
|
||||
private ScheduledFuture<?> scheduleRenew(RedisLockExecutor.LockHandle handle) {
|
||||
long intervalMillis = Math.max(1000L, properties.getLockRenewInterval().toMillis());
|
||||
return RENEW_EXECUTOR.scheduleAtFixedRate(handle::renew, intervalMillis, intervalMillis, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private String lockKey(BigInteger agentId, String sessionId) {
|
||||
return LOCK_PREFIX + "agent:" + (agentId == null ? "unknown" : agentId)
|
||||
+ ":session:" + (sessionId == null ? "unknown" : sessionId);
|
||||
}
|
||||
|
||||
private static final class RedisHandle implements Handle {
|
||||
|
||||
private final RedisLockExecutor.LockHandle delegate;
|
||||
private final ScheduledFuture<?> renewTask;
|
||||
private final AtomicBoolean released = new AtomicBoolean(false);
|
||||
|
||||
private RedisHandle(RedisLockExecutor.LockHandle delegate, ScheduledFuture<?> renewTask) {
|
||||
this.delegate = delegate;
|
||||
this.renewTask = renewTask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean renew() {
|
||||
return delegate.renew();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (!released.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
if (renewTask != null) {
|
||||
renewTask.cancel(false);
|
||||
}
|
||||
delegate.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
package tech.easyflow.agent.runtime.session;
|
||||
|
||||
import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import io.agentscope.core.state.State;
|
||||
import io.agentscope.core.util.JsonUtils;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import tech.easyflow.agent.config.AgentRuntimeProperties;
|
||||
import tech.easyflow.agent.entity.AgentSession;
|
||||
import tech.easyflow.agent.mapper.AgentSessionMapper;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* session store 持久化实现
|
||||
*/
|
||||
@Service
|
||||
public class EasyFlowAgentSessionStore implements AgentSessionStore {
|
||||
|
||||
private static final String REDIS_PREFIX = "easyflow:agent:session:";
|
||||
private static final String ENVELOPE_VERSION = "1";
|
||||
private static final String SINGLE_STATES = "singleStates";
|
||||
private static final String LIST_STATES = "listStates";
|
||||
|
||||
private final AgentSessionMapper agentSessionMapper;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final AgentRuntimeProperties properties;
|
||||
|
||||
/**
|
||||
* 创建 EasyFlow Agent session store。
|
||||
*
|
||||
* @param agentSessionMapper session Mapper
|
||||
* @param stringRedisTemplate Redis 模板
|
||||
* @param properties Agent 运行态配置
|
||||
*/
|
||||
public EasyFlowAgentSessionStore(AgentSessionMapper agentSessionMapper,
|
||||
StringRedisTemplate stringRedisTemplate,
|
||||
AgentRuntimeProperties properties) {
|
||||
this.agentSessionMapper = agentSessionMapper;
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定业务会话元信息。
|
||||
*
|
||||
* <p>AgentScope Session API 不会把 EasyFlow 的 {@code agentId/chatSessionId/tenantId} 传入
|
||||
* {@code save(...)},因此运行入口必须先调用本方法建立或刷新元信息。后续 state 写入会复用
|
||||
* 这些字段,避免表里只剩裸 sessionKey。</p>
|
||||
*
|
||||
* @param metadata 业务会话元信息
|
||||
*/
|
||||
public void bindSession(AgentSessionMetadata metadata) {
|
||||
if (metadata == null || !StringUtils.hasText(metadata.sessionKey())) {
|
||||
return;
|
||||
}
|
||||
AgentSession session = findBySessionKey(metadata.sessionKey());
|
||||
Date now = new Date();
|
||||
if (session == null) {
|
||||
session = new AgentSession();
|
||||
session.setTenantId(metadata.tenantId());
|
||||
session.setAgentId(metadata.agentId());
|
||||
session.setChatSessionId(metadata.chatSessionId());
|
||||
session.setRuntimeSessionId(metadata.runtimeSessionId());
|
||||
session.setSessionKey(metadata.sessionKey());
|
||||
session.setStateJson(emptyEnvelope());
|
||||
session.setVersion(0L);
|
||||
session.setCacheVersion(0L);
|
||||
session.setCreated(now);
|
||||
session.setCreatedBy(metadata.operatorId());
|
||||
session.setIsDeleted(0);
|
||||
}
|
||||
session.setTenantId(firstNonNull(metadata.tenantId(), session.getTenantId()));
|
||||
session.setAgentId(firstNonNull(metadata.agentId(), session.getAgentId()));
|
||||
session.setChatSessionId(firstNonNull(metadata.chatSessionId(), session.getChatSessionId()));
|
||||
session.setRuntimeSessionId(firstText(metadata.runtimeSessionId(), session.getRuntimeSessionId()));
|
||||
session.setLastAccessAt(now);
|
||||
session.setModified(now);
|
||||
session.setModifiedBy(metadata.operatorId());
|
||||
agentSessionMapper.insertOrUpdate(session);
|
||||
writeCache(session.getSessionKey(), session.getStateJson());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按聊天会话清理 AgentScope session。
|
||||
*
|
||||
* @param chatSessionId 聊天会话 ID
|
||||
*/
|
||||
public void deleteByChatSessionId(BigInteger chatSessionId) {
|
||||
if (chatSessionId == null) {
|
||||
return;
|
||||
}
|
||||
List<AgentSession> sessions = agentSessionMapper.selectListByQuery(QueryWrapper.create()
|
||||
.eq("chat_session_id", chatSessionId)
|
||||
.eq("is_deleted", 0));
|
||||
Date now = new Date();
|
||||
for (AgentSession session : sessions) {
|
||||
session.setIsDeleted(1);
|
||||
session.setModified(now);
|
||||
agentSessionMapper.update(session);
|
||||
deleteCache(session.getSessionKey());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(String sessionKey, String name, State state) {
|
||||
if (!StringUtils.hasText(sessionKey) || !StringUtils.hasText(name) || state == null) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> envelope = loadEnvelope(sessionKey);
|
||||
singleStates(envelope).put(name, JsonUtils.getJsonCodec().toJson(state));
|
||||
persistEnvelope(sessionKey, envelope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveList(String sessionKey, String name, List<? extends State> states) {
|
||||
if (!StringUtils.hasText(sessionKey) || !StringUtils.hasText(name)) {
|
||||
return;
|
||||
}
|
||||
List<String> values = new ArrayList<>();
|
||||
if (states != null) {
|
||||
for (State state : states) {
|
||||
values.add(JsonUtils.getJsonCodec().toJson(state));
|
||||
}
|
||||
}
|
||||
Map<String, Object> envelope = loadEnvelope(sessionKey);
|
||||
listStates(envelope).put(name, values);
|
||||
persistEnvelope(sessionKey, envelope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends State> Optional<T> get(String sessionKey, String name, Class<T> type) {
|
||||
if (!StringUtils.hasText(sessionKey) || !StringUtils.hasText(name) || type == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Object json = singleStates(loadEnvelope(sessionKey)).get(name);
|
||||
if (!(json instanceof String text) || text.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(JsonUtils.getJsonCodec().fromJson(text, type));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends State> List<T> getList(String sessionKey, String name, Class<T> itemType) {
|
||||
if (!StringUtils.hasText(sessionKey) || !StringUtils.hasText(name) || itemType == null) {
|
||||
return List.of();
|
||||
}
|
||||
Object raw = listStates(loadEnvelope(sessionKey)).get(name);
|
||||
if (!(raw instanceof List<?> values) || values.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<T> result = new ArrayList<>();
|
||||
for (Object value : values) {
|
||||
if (value instanceof String text && !text.isBlank()) {
|
||||
result.add(JsonUtils.getJsonCodec().fromJson(text, itemType));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String sessionKey) {
|
||||
if (!StringUtils.hasText(sessionKey)) {
|
||||
return false;
|
||||
}
|
||||
if (readCache(sessionKey) != null) {
|
||||
return true;
|
||||
}
|
||||
return findBySessionKey(sessionKey) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String sessionKey) {
|
||||
if (!StringUtils.hasText(sessionKey)) {
|
||||
return;
|
||||
}
|
||||
AgentSession session = findBySessionKey(sessionKey);
|
||||
if (session != null) {
|
||||
session.setIsDeleted(1);
|
||||
session.setModified(new Date());
|
||||
agentSessionMapper.update(session);
|
||||
}
|
||||
deleteCache(sessionKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> listSessionKeys() {
|
||||
List<AgentSession> sessions = agentSessionMapper.selectListByQuery(QueryWrapper.create()
|
||||
.eq("is_deleted", 0)
|
||||
.select("session_key"));
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
for (AgentSession session : sessions) {
|
||||
if (StringUtils.hasText(session.getSessionKey())) {
|
||||
keys.add(session.getSessionKey());
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private void persistEnvelope(String sessionKey, Map<String, Object> envelope) {
|
||||
AgentSession session = findBySessionKey(sessionKey);
|
||||
Date now = new Date();
|
||||
if (session == null) {
|
||||
session = new AgentSession();
|
||||
session.setRuntimeSessionId(sessionKey);
|
||||
session.setSessionKey(sessionKey);
|
||||
session.setCreated(now);
|
||||
session.setVersion(0L);
|
||||
session.setCacheVersion(0L);
|
||||
session.setIsDeleted(0);
|
||||
}
|
||||
long nextVersion = session.getVersion() == null ? 1L : session.getVersion() + 1L;
|
||||
session.setStateJson(envelope);
|
||||
session.setVersion(nextVersion);
|
||||
session.setCacheVersion(nextVersion);
|
||||
session.setLastAccessAt(now);
|
||||
session.setModified(now);
|
||||
session.setIsDeleted(0);
|
||||
// 同步写会话状态
|
||||
agentSessionMapper.insertOrUpdate(session);
|
||||
// 同步写缓存
|
||||
writeCache(sessionKey, envelope);
|
||||
}
|
||||
|
||||
private Map<String, Object> loadEnvelope(String sessionKey) {
|
||||
Map<String, Object> cached = readCache(sessionKey);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
AgentSession session = findBySessionKey(sessionKey);
|
||||
if (session == null || session.getStateJson() == null || session.getStateJson().isEmpty()) {
|
||||
return emptyEnvelope();
|
||||
}
|
||||
writeCache(sessionKey, session.getStateJson());
|
||||
return deepCopy(session.getStateJson());
|
||||
}
|
||||
|
||||
private AgentSession findBySessionKey(String sessionKey) {
|
||||
return agentSessionMapper.selectOneByQuery(QueryWrapper.create()
|
||||
.eq("session_key", sessionKey)
|
||||
.eq("is_deleted", 0)
|
||||
.limit(1));
|
||||
}
|
||||
|
||||
private Map<String, Object> emptyEnvelope() {
|
||||
Map<String, Object> envelope = new LinkedHashMap<>();
|
||||
envelope.put("version", ENVELOPE_VERSION);
|
||||
envelope.put(SINGLE_STATES, new LinkedHashMap<String, Object>());
|
||||
envelope.put(LIST_STATES, new LinkedHashMap<String, Object>());
|
||||
return envelope;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> singleStates(Map<String, Object> envelope) {
|
||||
return (Map<String, Object>) envelope.computeIfAbsent(SINGLE_STATES, key -> new LinkedHashMap<String, Object>());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> listStates(Map<String, Object> envelope) {
|
||||
return (Map<String, Object>) envelope.computeIfAbsent(LIST_STATES, key -> new LinkedHashMap<String, Object>());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> readCache(String sessionKey) {
|
||||
try {
|
||||
String value = stringRedisTemplate.opsForValue().get(cacheKey(sessionKey));
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return JsonUtils.getJsonCodec().fromJson(value, Map.class);
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeCache(String sessionKey, Map<String, Object> envelope) {
|
||||
try {
|
||||
long seconds = Math.max(1L, properties.getSessionCacheTtl().toSeconds());
|
||||
stringRedisTemplate.opsForValue().set(cacheKey(sessionKey), JsonUtils.getJsonCodec().toJson(envelope),
|
||||
seconds, TimeUnit.SECONDS);
|
||||
} catch (RuntimeException e) {
|
||||
// MySQL 是 AgentScope session 的真相源。Redis 只做热缓存,写缓存失败不能掩盖已完成的持久化写入。
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteCache(String sessionKey) {
|
||||
try {
|
||||
stringRedisTemplate.delete(cacheKey(sessionKey));
|
||||
} catch (RuntimeException ignored) {
|
||||
// 清理缓存失败不应掩盖 MySQL 删除结果,后续 TTL 会自然回收。
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> deepCopy(Map<String, Object> source) {
|
||||
if (source == null || source.isEmpty()) {
|
||||
return emptyEnvelope();
|
||||
}
|
||||
return JsonUtils.getJsonCodec().fromJson(JsonUtils.getJsonCodec().toJson(source), Map.class);
|
||||
}
|
||||
|
||||
private String cacheKey(String sessionKey) {
|
||||
return REDIS_PREFIX + hash(sessionKey);
|
||||
}
|
||||
|
||||
private String hash(String value) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] bytes = digest.digest(value.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
return value.replace(':', '_');
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T firstNonNull(T left, T right) {
|
||||
return left == null ? right : left;
|
||||
}
|
||||
|
||||
private String firstText(String left, String right) {
|
||||
return StringUtils.hasText(left) ? left : right;
|
||||
}
|
||||
|
||||
/**
|
||||
* AgentScope session 绑定的 EasyFlow 业务元信息。
|
||||
*
|
||||
* @param sessionKey AgentScope session key
|
||||
* @param runtimeSessionId runtime session ID
|
||||
* @param agentId Agent ID
|
||||
* @param chatSessionId chatlog 会话 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @param operatorId 操作人 ID
|
||||
*/
|
||||
public record AgentSessionMetadata(String sessionKey,
|
||||
String runtimeSessionId,
|
||||
BigInteger agentId,
|
||||
BigInteger chatSessionId,
|
||||
BigInteger tenantId,
|
||||
BigInteger operatorId) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package tech.easyflow.agent.service;
|
||||
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Agent 审批状态派生服务。
|
||||
*/
|
||||
public interface AgentApprovalStateService {
|
||||
|
||||
/**
|
||||
* 填充单个 Agent 的审批展示状态。
|
||||
*
|
||||
* @param agent Agent 资源
|
||||
*/
|
||||
void fillAgentApprovalState(Agent agent);
|
||||
|
||||
/**
|
||||
* 批量填充 Agent 的审批展示状态。
|
||||
*
|
||||
* @param agents Agent 资源集合
|
||||
*/
|
||||
void fillAgentApprovalState(Collection<Agent> agents);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package tech.easyflow.agent.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.agent.entity.AgentCategory;
|
||||
|
||||
/**
|
||||
* Agent 分类服务。
|
||||
*/
|
||||
public interface AgentCategoryService extends IService<AgentCategory> {
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package tech.easyflow.agent.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 知识库绑定服务。
|
||||
*/
|
||||
public interface AgentKnowledgeBindingService extends IService<AgentKnowledgeBinding> {
|
||||
|
||||
/**
|
||||
* 替换 Agent 知识库绑定。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @param bindings 新绑定列表
|
||||
* @return 保存后的绑定列表
|
||||
*/
|
||||
List<AgentKnowledgeBinding> replaceBindings(BigInteger agentId, List<AgentKnowledgeBinding> bindings);
|
||||
|
||||
/**
|
||||
* 查询 Agent 启用知识库绑定。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @return 启用绑定列表
|
||||
*/
|
||||
List<AgentKnowledgeBinding> listEnabled(BigInteger agentId);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package tech.easyflow.agent.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Agent 配置服务。
|
||||
*/
|
||||
public interface AgentService extends IService<Agent> {
|
||||
|
||||
/**
|
||||
* 获取 Agent 详情。
|
||||
*
|
||||
* @param id Agent ID
|
||||
* @return Agent 详情
|
||||
*/
|
||||
Agent getDetail(BigInteger id);
|
||||
|
||||
/**
|
||||
* 保存草稿 Agent。
|
||||
*
|
||||
* @param agent Agent 草稿
|
||||
* @return 保存后的 Agent
|
||||
*/
|
||||
Agent saveDraft(Agent agent);
|
||||
|
||||
/**
|
||||
* 更新草稿 Agent。
|
||||
*
|
||||
* @param agent Agent 草稿
|
||||
* @return 更新后的 Agent
|
||||
*/
|
||||
Agent updateDraft(Agent agent);
|
||||
|
||||
/**
|
||||
* 获取已发布运行视图。
|
||||
*
|
||||
* @param id Agent ID
|
||||
* @return 已发布运行视图
|
||||
*/
|
||||
Agent getPublishedView(BigInteger id);
|
||||
|
||||
/**
|
||||
* 构建发布快照。
|
||||
*
|
||||
* @param agent Agent 当前草稿
|
||||
* @return 发布快照
|
||||
*/
|
||||
Map<String, Object> buildPublishSnapshot(Agent agent);
|
||||
|
||||
/**
|
||||
* 从快照还原运行视图。
|
||||
*
|
||||
* @param snapshot 发布快照
|
||||
* @return Agent 运行视图
|
||||
*/
|
||||
Agent fromSnapshot(Map<String, Object> snapshot);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package tech.easyflow.agent.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 工具绑定服务。
|
||||
*/
|
||||
public interface AgentToolBindingService extends IService<AgentToolBinding> {
|
||||
|
||||
/**
|
||||
* 替换 Agent 工具绑定。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @param bindings 新绑定列表
|
||||
* @return 保存后的绑定列表
|
||||
*/
|
||||
List<AgentToolBinding> replaceBindings(BigInteger agentId, List<AgentToolBinding> bindings);
|
||||
|
||||
/**
|
||||
* 查询 Agent 启用工具绑定。
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @return 启用绑定列表
|
||||
*/
|
||||
List<AgentToolBinding> listEnabled(BigInteger agentId);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package tech.easyflow.agent.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.service.AgentApprovalStateService;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||
import tech.easyflow.approval.enums.ApprovalInstanceStatus;
|
||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||
import tech.easyflow.approval.mapper.ApprovalInstanceMapper;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Agent 审批状态派生服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class AgentApprovalStateServiceImpl implements AgentApprovalStateService {
|
||||
|
||||
private final ApprovalInstanceMapper approvalInstanceMapper;
|
||||
|
||||
/**
|
||||
* 创建 Agent 审批状态派生服务。
|
||||
*
|
||||
* @param approvalInstanceMapper 审批实例 Mapper
|
||||
*/
|
||||
public AgentApprovalStateServiceImpl(ApprovalInstanceMapper approvalInstanceMapper) {
|
||||
this.approvalInstanceMapper = approvalInstanceMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillAgentApprovalState(Agent agent) {
|
||||
fillAgentApprovalState(agent == null ? List.of() : List.of(agent));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void fillAgentApprovalState(Collection<Agent> agents) {
|
||||
if (CollectionUtils.isEmpty(agents)) {
|
||||
return;
|
||||
}
|
||||
List<Agent> validAgents = agents.stream().filter(Objects::nonNull).toList();
|
||||
if (validAgents.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<BigInteger, ApprovalInstance> instanceMap = loadInstanceMap(validAgents, Agent::getCurrentApprovalInstanceId);
|
||||
for (Agent agent : validAgents) {
|
||||
fillOne(agent, instanceMap.get(agent.getCurrentApprovalInstanceId()));
|
||||
}
|
||||
}
|
||||
|
||||
private void fillOne(Agent agent, ApprovalInstance instance) {
|
||||
PublishStatus currentStatus = PublishStatus.from(agent.getPublishStatus());
|
||||
if (!isValidCurrentInstance(instance)) {
|
||||
agent.setApprovalPending(false);
|
||||
agent.setCurrentApprovalActionType(null);
|
||||
agent.setDisplayPublishStatus(currentStatus.getCode());
|
||||
return;
|
||||
}
|
||||
ApprovalInstanceStatus instanceStatus = ApprovalInstanceStatus.from(instance.getStatus());
|
||||
if (instanceStatus.isFinished()) {
|
||||
agent.setApprovalPending(false);
|
||||
agent.setCurrentApprovalActionType(null);
|
||||
agent.setDisplayPublishStatus(currentStatus.getCode());
|
||||
return;
|
||||
}
|
||||
ApprovalActionType actionType = ApprovalActionType.from(instance.getActionType());
|
||||
agent.setApprovalPending(true);
|
||||
agent.setCurrentApprovalActionType(actionType.getCode());
|
||||
agent.setDisplayPublishStatus(resolveDisplayStatusWithActiveInstance(currentStatus, actionType).getCode());
|
||||
}
|
||||
|
||||
private Map<BigInteger, ApprovalInstance> loadInstanceMap(Collection<Agent> agents,
|
||||
Function<Agent, BigInteger> instanceIdGetter) {
|
||||
Set<BigInteger> instanceIds = agents.stream()
|
||||
.map(instanceIdGetter)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (instanceIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<ApprovalInstance> instances = approvalInstanceMapper.selectListByQuery(
|
||||
QueryWrapper.create().in(ApprovalInstance::getId, instanceIds)
|
||||
);
|
||||
return instances.stream().collect(Collectors.toMap(ApprovalInstance::getId, Function.identity()));
|
||||
}
|
||||
|
||||
private boolean isValidCurrentInstance(ApprovalInstance instance) {
|
||||
return instance != null && ApprovalResourceType.AGENT.getCode().equals(instance.getResourceType());
|
||||
}
|
||||
|
||||
private PublishStatus resolveDisplayStatusWithActiveInstance(PublishStatus currentStatus,
|
||||
ApprovalActionType actionType) {
|
||||
if (currentStatus == PublishStatus.PUBLISH_PENDING
|
||||
|| currentStatus == PublishStatus.OFFLINE_PENDING
|
||||
|| currentStatus == PublishStatus.DELETE_PENDING) {
|
||||
return currentStatus;
|
||||
}
|
||||
return switch (actionType) {
|
||||
case PUBLISH -> PublishStatus.PUBLISH_PENDING;
|
||||
case OFFLINE -> PublishStatus.OFFLINE_PENDING;
|
||||
case DELETE -> PublishStatus.DELETE_PENDING;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package tech.easyflow.agent.service.impl;
|
||||
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.agent.entity.AgentCategory;
|
||||
import tech.easyflow.agent.mapper.AgentCategoryMapper;
|
||||
import tech.easyflow.agent.service.AgentCategoryService;
|
||||
|
||||
/**
|
||||
* Agent 分类服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class AgentCategoryServiceImpl extends ServiceImpl<AgentCategoryMapper, AgentCategory> implements AgentCategoryService {
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package tech.easyflow.agent.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.mapper.AgentKnowledgeBindingMapper;
|
||||
import tech.easyflow.agent.mapper.AgentMapper;
|
||||
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.rag.KnowledgeRetrievalModes;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
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 javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 知识库绑定服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class AgentKnowledgeBindingServiceImpl extends ServiceImpl<AgentKnowledgeBindingMapper, AgentKnowledgeBinding>
|
||||
implements AgentKnowledgeBindingService {
|
||||
|
||||
private static final String DEFAULT_RETRIEVAL_MODE = "HYBRID";
|
||||
|
||||
@Resource
|
||||
private AgentMapper agentMapper;
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<AgentKnowledgeBinding> replaceBindings(BigInteger agentId, List<AgentKnowledgeBinding> bindings) {
|
||||
Agent agent = requireAgent(agentId);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
remove(QueryWrapper.create().where("agent_id = ?", agentId));
|
||||
if (bindings == null || bindings.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
for (int i = 0; i < bindings.size(); i++) {
|
||||
AgentKnowledgeBinding binding = bindings.get(i);
|
||||
validateBinding(binding);
|
||||
applyBindingDefaults(agent, binding, i);
|
||||
}
|
||||
saveBatch(bindings);
|
||||
return listEnabled(agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<AgentKnowledgeBinding> listEnabled(BigInteger agentId) {
|
||||
return list(QueryWrapper.create()
|
||||
.where("agent_id = ?", agentId)
|
||||
.and("enabled = ?", true)
|
||||
.orderBy("sort_no asc, id asc"));
|
||||
}
|
||||
|
||||
private Agent requireAgent(BigInteger agentId) {
|
||||
Agent agent = agentMapper.selectOneById(agentId);
|
||||
if (agent == null) {
|
||||
throw new BusinessException("Agent 不存在");
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
private void validateBinding(AgentKnowledgeBinding binding) {
|
||||
if (binding == null || binding.getKnowledgeId() == null) {
|
||||
throw new BusinessException("知识库绑定参数不完整");
|
||||
}
|
||||
DocumentCollection knowledge = documentCollectionService.getById(binding.getKnowledgeId());
|
||||
if (knowledge == null || PublishStatus.from(knowledge.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("绑定知识库不存在或未发布");
|
||||
}
|
||||
KnowledgeRetrievalModes.parse(binding.getRetrievalMode());
|
||||
resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, knowledge, ResourceAction.USE, "无权限绑定该知识库");
|
||||
}
|
||||
|
||||
private void applyBindingDefaults(Agent agent, AgentKnowledgeBinding binding, int index) {
|
||||
LoginAccount account = requireCurrentLoginAccount();
|
||||
Date now = new Date();
|
||||
binding.setId(null);
|
||||
binding.setTenantId(agent.getTenantId());
|
||||
binding.setAgentId(agent.getId());
|
||||
if (binding.getRetrievalMode() == null || binding.getRetrievalMode().isBlank()) {
|
||||
binding.setRetrievalMode(DEFAULT_RETRIEVAL_MODE);
|
||||
} else {
|
||||
binding.setRetrievalMode(binding.getRetrievalMode().trim().toUpperCase());
|
||||
}
|
||||
binding.setEnabled(binding.getEnabled() == null || binding.getEnabled());
|
||||
binding.setSortNo(binding.getSortNo() == null ? index : binding.getSortNo());
|
||||
binding.setCreated(now);
|
||||
binding.setCreatedBy(account.getId());
|
||||
binding.setModified(now);
|
||||
binding.setModifiedBy(account.getId());
|
||||
}
|
||||
|
||||
private LoginAccount requireCurrentLoginAccount() {
|
||||
try {
|
||||
return SaTokenUtil.getLoginAccount();
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
package tech.easyflow.agent.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.mapper.AgentMapper;
|
||||
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||
import tech.easyflow.agent.service.AgentService;
|
||||
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||
import tech.easyflow.ai.entity.*;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.*;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.enums.CategoryResourceType;
|
||||
import tech.easyflow.system.enums.ResourceAction;
|
||||
import tech.easyflow.system.enums.VisibilityScope;
|
||||
import tech.easyflow.system.service.ResourceAccessService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Agent 配置服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class AgentServiceImpl extends ServiceImpl<AgentMapper, Agent> implements AgentService {
|
||||
|
||||
private static final TypeReference<List<AgentToolBinding>> TOOL_BINDING_LIST_TYPE = new TypeReference<>() {};
|
||||
private static final TypeReference<List<AgentKnowledgeBinding>> KNOWLEDGE_BINDING_LIST_TYPE = new TypeReference<>() {};
|
||||
|
||||
@Resource
|
||||
private AgentToolBindingService agentToolBindingService;
|
||||
@Resource
|
||||
private AgentKnowledgeBindingService agentKnowledgeBindingService;
|
||||
@Resource
|
||||
private ModelService modelService;
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private PluginItemService pluginItemService;
|
||||
@Resource
|
||||
private McpService mcpService;
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Agent getDetail(BigInteger id) {
|
||||
Agent agent = requireAgent(id);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.AGENT, agent, ResourceAction.READ, "无权限查看该 Agent");
|
||||
agent.setToolBindings(agentToolBindingService.listEnabled(id));
|
||||
agent.setKnowledgeBindings(agentKnowledgeBindingService.listEnabled(id));
|
||||
return agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Agent saveDraft(Agent agent) {
|
||||
validateDraft(agent);
|
||||
applyDraftDefaults(agent);
|
||||
save(agent);
|
||||
return getDetail(agent.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Agent updateDraft(Agent agent) {
|
||||
if (agent == null || agent.getId() == null) {
|
||||
throw new BusinessException("Agent ID 不能为空");
|
||||
}
|
||||
Agent existing = requireAgent(agent.getId());
|
||||
resourceAccessService.assertAccess(CategoryResourceType.AGENT, existing, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
validateDraft(agent);
|
||||
applyDraftUpdate(existing, agent);
|
||||
updateById(existing);
|
||||
return getDetail(existing.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Agent getPublishedView(BigInteger id) {
|
||||
Agent agent = requireAgent(id);
|
||||
PublishStatus status = PublishStatus.from(agent.getPublishStatus());
|
||||
if (!status.isExternallyVisible() || agent.getPublishedSnapshotJson() == null || agent.getPublishedSnapshotJson().isEmpty()) {
|
||||
throw new BusinessException("Agent 未发布");
|
||||
}
|
||||
return fromSnapshot(agent.getPublishedSnapshotJson());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> buildPublishSnapshot(Agent agent) {
|
||||
Agent detail = getDetail(agent.getId());
|
||||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||||
snapshot.put("id", detail.getId());
|
||||
snapshot.put("tenantId", detail.getTenantId());
|
||||
snapshot.put("deptId", detail.getDeptId());
|
||||
snapshot.put("createdBy", detail.getCreatedBy());
|
||||
snapshot.put("name", detail.getName());
|
||||
snapshot.put("description", detail.getDescription());
|
||||
snapshot.put("avatar", detail.getAvatar());
|
||||
snapshot.put("categoryId", detail.getCategoryId());
|
||||
snapshot.put("modelId", detail.getModelId());
|
||||
snapshot.put("modelConfigJson", detail.getModelConfigJson());
|
||||
snapshot.put("generationConfigJson", detail.getGenerationConfigJson());
|
||||
snapshot.put("promptConfigJson", detail.getPromptConfigJson());
|
||||
snapshot.put("memoryConfigJson", detail.getMemoryConfigJson());
|
||||
snapshot.put("executionConfigJson", detail.getExecutionConfigJson());
|
||||
snapshot.put("visibilityScope", detail.getVisibilityScope());
|
||||
snapshot.put("toolBindings", snapshotToolBindings(detail.getToolBindings()));
|
||||
snapshot.put("knowledgeBindings", snapshotKnowledgeBindings(detail.getKnowledgeBindings()));
|
||||
snapshot.put("basicSummary", basicSummary(detail));
|
||||
snapshot.put("modelSummary", modelSummary(detail.getModelId()));
|
||||
snapshot.put("parameterSummary", parameterSummary(detail));
|
||||
snapshot.put("promptSummary", promptSummary(detail));
|
||||
snapshot.put("toolSummaries", toolSummaries(detail.getToolBindings()));
|
||||
snapshot.put("knowledgeSummaries", knowledgeSummaries(detail.getKnowledgeBindings()));
|
||||
snapshot.put("snapshotAt", new Date());
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Agent fromSnapshot(Map<String, Object> snapshot) {
|
||||
if (snapshot == null || snapshot.isEmpty()) {
|
||||
throw new BusinessException("Agent 发布快照为空");
|
||||
}
|
||||
Agent agent = objectMapper.convertValue(snapshot, Agent.class);
|
||||
agent.setId(toBigInteger(snapshot.get("id")));
|
||||
agent.setTenantId(toBigInteger(snapshot.get("tenantId")));
|
||||
agent.setDeptId(toBigInteger(snapshot.get("deptId")));
|
||||
agent.setCreatedBy(toBigInteger(snapshot.get("createdBy")));
|
||||
agent.setModelId(toBigInteger(snapshot.get("modelId")));
|
||||
agent.setCategoryId(toBigInteger(snapshot.get("categoryId")));
|
||||
agent.setPublishStatus(PublishStatus.PUBLISHED.getCode());
|
||||
agent.setPublishedSnapshotJson(snapshot);
|
||||
agent.setToolBindings(objectMapper.convertValue(snapshot.get("toolBindings"), TOOL_BINDING_LIST_TYPE));
|
||||
agent.setKnowledgeBindings(objectMapper.convertValue(snapshot.get("knowledgeBindings"), KNOWLEDGE_BINDING_LIST_TYPE));
|
||||
return agent;
|
||||
}
|
||||
|
||||
private Agent requireAgent(BigInteger id) {
|
||||
Agent agent = getById(id);
|
||||
if (agent == null) {
|
||||
throw new BusinessException("Agent 不存在");
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
private void validateDraft(Agent agent) {
|
||||
if (agent == null) {
|
||||
throw new BusinessException("Agent 不能为空");
|
||||
}
|
||||
if (agent.getName() == null || agent.getName().isBlank()) {
|
||||
throw new BusinessException("Agent 名称不能为空");
|
||||
}
|
||||
if (agent.getModelId() == null) {
|
||||
throw new BusinessException("Agent 模型不能为空");
|
||||
}
|
||||
Model model = modelService.getModelInstance(agent.getModelId());
|
||||
if (model == null) {
|
||||
throw new BusinessException("Agent 模型不存在");
|
||||
}
|
||||
agent.setVisibilityScope(VisibilityScope.fromOrDefault(agent.getVisibilityScope(), VisibilityScope.PRIVATE).name());
|
||||
}
|
||||
|
||||
private void applyDraftDefaults(Agent agent) {
|
||||
LoginAccount account = requireCurrentLoginAccount();
|
||||
Date now = new Date();
|
||||
agent.setTenantId(account.getTenantId());
|
||||
agent.setDeptId(account.getDeptId());
|
||||
agent.setCreated(now);
|
||||
agent.setCreatedBy(account.getId());
|
||||
agent.setModified(now);
|
||||
agent.setModifiedBy(account.getId());
|
||||
agent.setStatus(agent.getStatus() == null ? 1 : agent.getStatus());
|
||||
agent.setPublishStatus(PublishStatus.DRAFT.getCode());
|
||||
}
|
||||
|
||||
private Map<String, Object> basicSummary(Agent agent) {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("id", agent.getId());
|
||||
summary.put("name", agent.getName());
|
||||
summary.put("description", agent.getDescription());
|
||||
summary.put("avatar", agent.getAvatar());
|
||||
summary.put("categoryId", agent.getCategoryId());
|
||||
summary.put("status", agent.getStatus());
|
||||
summary.put("publishStatus", PublishStatus.PUBLISHED.getCode());
|
||||
summary.put("visibilityScope", agent.getVisibilityScope());
|
||||
return summary;
|
||||
}
|
||||
|
||||
private void applyDraftUpdate(Agent existing, Agent incoming) {
|
||||
LoginAccount account = requireCurrentLoginAccount();
|
||||
existing.setName(incoming.getName());
|
||||
existing.setDescription(incoming.getDescription());
|
||||
existing.setAvatar(incoming.getAvatar());
|
||||
existing.setCategoryId(incoming.getCategoryId());
|
||||
existing.setModelId(incoming.getModelId());
|
||||
existing.setModelConfigJson(incoming.getModelConfigJson());
|
||||
existing.setGenerationConfigJson(incoming.getGenerationConfigJson());
|
||||
existing.setPromptConfigJson(incoming.getPromptConfigJson());
|
||||
existing.setMemoryConfigJson(incoming.getMemoryConfigJson());
|
||||
existing.setExecutionConfigJson(incoming.getExecutionConfigJson());
|
||||
existing.setStatus(incoming.getStatus() == null ? 1 : incoming.getStatus());
|
||||
existing.setVisibilityScope(incoming.getVisibilityScope());
|
||||
existing.setModified(new Date());
|
||||
existing.setModifiedBy(account.getId());
|
||||
}
|
||||
|
||||
private Map<String, Object> modelSummary(BigInteger modelId) {
|
||||
Model model = modelService.getModelInstance(modelId);
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("id", model.getId());
|
||||
summary.put("title", model.getTitle());
|
||||
summary.put("modelName", model.getModelName());
|
||||
summary.put("providerType", model.getModelProvider() == null ? null : model.getModelProvider().getProviderType());
|
||||
return summary;
|
||||
}
|
||||
|
||||
private Map<String, Object> parameterSummary(Agent agent) {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("generationConfigJson", agent.getGenerationConfigJson());
|
||||
summary.put("modelConfigJson", agent.getModelConfigJson());
|
||||
summary.put("memoryConfigJson", agent.getMemoryConfigJson());
|
||||
summary.put("executionConfigJson", agent.getExecutionConfigJson());
|
||||
return summary;
|
||||
}
|
||||
|
||||
private Map<String, Object> promptSummary(Agent agent) {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("promptConfigJson", agent.getPromptConfigJson());
|
||||
summary.put("systemPrompt", agent.getPromptConfigJson() == null ? null : agent.getPromptConfigJson().get("systemPrompt"));
|
||||
summary.put("prompt", agent.getPromptConfigJson() == null ? null : agent.getPromptConfigJson().get("prompt"));
|
||||
return summary;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> toolSummaries(List<AgentToolBinding> bindings) {
|
||||
if (bindings == null) {
|
||||
return List.of();
|
||||
}
|
||||
return bindings.stream().map(this::toolSummary).toList();
|
||||
}
|
||||
|
||||
private Map<String, Object> toolSummary(AgentToolBinding binding) {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("bindingId", binding.getId());
|
||||
summary.put("toolType", binding.getToolType());
|
||||
summary.put("targetId", binding.getTargetId());
|
||||
summary.put("toolName", binding.getToolName());
|
||||
summary.put("enabled", Boolean.TRUE.equals(binding.getEnabled()));
|
||||
summary.put("hitlEnabled", Boolean.TRUE.equals(binding.getHitlEnabled()));
|
||||
summary.put("hitlConfigJson", binding.getHitlConfigJson());
|
||||
summary.put("sortNo", binding.getSortNo());
|
||||
if ("WORKFLOW".equalsIgnoreCase(binding.getToolType())) {
|
||||
Workflow workflow = workflowService.getById(binding.getTargetId());
|
||||
summary.put("title", workflow == null ? null : workflow.getTitle());
|
||||
} else if ("PLUGIN".equalsIgnoreCase(binding.getToolType())) {
|
||||
PluginItem pluginItem = pluginItemService.getById(binding.getTargetId());
|
||||
summary.put("title", pluginItem == null ? null : pluginItem.getName());
|
||||
} else {
|
||||
Mcp mcp = mcpService.getById(binding.getTargetId());
|
||||
summary.put("title", mcp == null ? null : mcp.getTitle());
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
private List<AgentToolBinding> snapshotToolBindings(List<AgentToolBinding> bindings) {
|
||||
if (bindings == null) {
|
||||
return List.of();
|
||||
}
|
||||
return bindings.stream().map(binding -> {
|
||||
AgentToolBinding snapshot = objectMapper.convertValue(binding, AgentToolBinding.class);
|
||||
snapshot.setResourceSummary(toolSummary(binding));
|
||||
snapshot.setResourceSnapshot(toolResourceSnapshot(binding));
|
||||
return snapshot;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private Map<String, Object> toolResourceSnapshot(AgentToolBinding binding) {
|
||||
if ("WORKFLOW".equalsIgnoreCase(binding.getToolType())) {
|
||||
Workflow workflow = workflowService.getPublishedById(binding.getTargetId());
|
||||
if (workflow == null || !PublishStatus.from(workflow.getPublishStatus()).isExternallyVisible()) {
|
||||
throw new BusinessException("绑定工作流不存在或未发布");
|
||||
}
|
||||
return objectMapper.convertValue(workflow, new TypeReference<Map<String, Object>>() {});
|
||||
}
|
||||
if ("PLUGIN".equalsIgnoreCase(binding.getToolType())) {
|
||||
PluginItem pluginItem = pluginItemService.getById(binding.getTargetId());
|
||||
if (pluginItem == null) {
|
||||
throw new BusinessException("绑定插件不存在");
|
||||
}
|
||||
return objectMapper.convertValue(pluginItem, new TypeReference<Map<String, Object>>() {});
|
||||
}
|
||||
Mcp mcp = mcpService.getById(binding.getTargetId());
|
||||
if (mcp == null) {
|
||||
throw new BusinessException("绑定 MCP 不存在");
|
||||
}
|
||||
return objectMapper.convertValue(mcp, new TypeReference<Map<String, Object>>() {});
|
||||
}
|
||||
|
||||
private List<AgentKnowledgeBinding> snapshotKnowledgeBindings(List<AgentKnowledgeBinding> bindings) {
|
||||
if (bindings == null) {
|
||||
return List.of();
|
||||
}
|
||||
return bindings.stream().map(binding -> {
|
||||
AgentKnowledgeBinding snapshot = objectMapper.convertValue(binding, AgentKnowledgeBinding.class);
|
||||
snapshot.setResourceSummary(knowledgeSummary(binding));
|
||||
snapshot.setResourceSnapshot(knowledgeResourceSnapshot(binding));
|
||||
return snapshot;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private Map<String, Object> knowledgeResourceSnapshot(AgentKnowledgeBinding binding) {
|
||||
DocumentCollection knowledge = documentCollectionService.getPublishedById(binding.getKnowledgeId());
|
||||
if (knowledge == null || !PublishStatus.from(knowledge.getPublishStatus()).isExternallyVisible()) {
|
||||
throw new BusinessException("绑定知识库不存在或未发布");
|
||||
}
|
||||
return objectMapper.convertValue(knowledge, new TypeReference<Map<String, Object>>() {});
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> knowledgeSummaries(List<AgentKnowledgeBinding> bindings) {
|
||||
if (bindings == null) {
|
||||
return List.of();
|
||||
}
|
||||
return bindings.stream().map(this::knowledgeSummary).toList();
|
||||
}
|
||||
|
||||
private Map<String, Object> knowledgeSummary(AgentKnowledgeBinding binding) {
|
||||
DocumentCollection knowledge = documentCollectionService.getById(binding.getKnowledgeId());
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("bindingId", binding.getId());
|
||||
summary.put("knowledgeId", binding.getKnowledgeId());
|
||||
summary.put("retrievalMode", binding.getRetrievalMode());
|
||||
summary.put("enabled", Boolean.TRUE.equals(binding.getEnabled()));
|
||||
summary.put("optionsJson", binding.getOptionsJson());
|
||||
summary.put("sortNo", binding.getSortNo());
|
||||
summary.put("title", knowledge == null ? null : knowledge.getTitle());
|
||||
return summary;
|
||||
}
|
||||
|
||||
private LoginAccount requireCurrentLoginAccount() {
|
||||
try {
|
||||
return SaTokenUtil.getLoginAccount();
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
}
|
||||
|
||||
private BigInteger toBigInteger(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof BigInteger bigInteger) {
|
||||
return bigInteger;
|
||||
}
|
||||
if (value instanceof Number number) {
|
||||
return BigInteger.valueOf(number.longValue());
|
||||
}
|
||||
return new BigInteger(String.valueOf(value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package tech.easyflow.agent.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.enums.AgentToolType;
|
||||
import tech.easyflow.agent.mapper.AgentMapper;
|
||||
import tech.easyflow.agent.mapper.AgentToolBindingMapper;
|
||||
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||
import tech.easyflow.ai.entity.Mcp;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.service.McpService;
|
||||
import tech.easyflow.ai.service.PluginItemService;
|
||||
import tech.easyflow.ai.service.WorkflowService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
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 javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Agent 工具绑定服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class AgentToolBindingServiceImpl extends ServiceImpl<AgentToolBindingMapper, AgentToolBinding>
|
||||
implements AgentToolBindingService {
|
||||
|
||||
@Resource
|
||||
private AgentMapper agentMapper;
|
||||
@Resource
|
||||
private WorkflowService workflowService;
|
||||
@Resource
|
||||
private PluginItemService pluginItemService;
|
||||
@Resource
|
||||
private McpService mcpService;
|
||||
@Resource
|
||||
private ResourceAccessService resourceAccessService;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public List<AgentToolBinding> replaceBindings(BigInteger agentId, List<AgentToolBinding> bindings) {
|
||||
Agent agent = requireAgent(agentId);
|
||||
resourceAccessService.assertAccess(CategoryResourceType.AGENT, agent, ResourceAction.MANAGE, "无权限管理该 Agent");
|
||||
remove(QueryWrapper.create().where("agent_id = ?", agentId));
|
||||
if (bindings == null || bindings.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
for (int i = 0; i < bindings.size(); i++) {
|
||||
AgentToolBinding binding = bindings.get(i);
|
||||
validateBinding(binding);
|
||||
applyBindingDefaults(agent, binding, i);
|
||||
}
|
||||
saveBatch(bindings);
|
||||
return listEnabled(agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<AgentToolBinding> listEnabled(BigInteger agentId) {
|
||||
return list(QueryWrapper.create()
|
||||
.where("agent_id = ?", agentId)
|
||||
.and("enabled = ?", true)
|
||||
.orderBy("sort_no asc, id asc"));
|
||||
}
|
||||
|
||||
private Agent requireAgent(BigInteger agentId) {
|
||||
Agent agent = agentMapper.selectOneById(agentId);
|
||||
if (agent == null) {
|
||||
throw new BusinessException("Agent 不存在");
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
private void validateBinding(AgentToolBinding binding) {
|
||||
if (binding == null || binding.getTargetId() == null || binding.getToolType() == null) {
|
||||
throw new BusinessException("工具绑定参数不完整");
|
||||
}
|
||||
AgentToolType type = AgentToolType.from(binding.getToolType());
|
||||
if (type == AgentToolType.WORKFLOW) {
|
||||
Workflow workflow = workflowService.getById(binding.getTargetId());
|
||||
if (workflow == null || PublishStatus.from(workflow.getPublishStatus()) != PublishStatus.PUBLISHED) {
|
||||
throw new BusinessException("绑定工作流不存在或未发布");
|
||||
}
|
||||
resourceAccessService.assertAccess(CategoryResourceType.WORKFLOW, workflow, ResourceAction.USE, "无权限绑定该工作流");
|
||||
return;
|
||||
}
|
||||
if (type == AgentToolType.PLUGIN) {
|
||||
PluginItem pluginItem = pluginItemService.getById(binding.getTargetId());
|
||||
if (pluginItem == null || pluginItem.getStatus() == null || pluginItem.getStatus() != 1) {
|
||||
throw new BusinessException("绑定插件不存在或未启用");
|
||||
}
|
||||
return;
|
||||
}
|
||||
Mcp mcp = mcpService.getById(binding.getTargetId());
|
||||
if (mcp == null || !Boolean.TRUE.equals(mcp.getStatus())) {
|
||||
throw new BusinessException("绑定 MCP 不存在或未启用");
|
||||
}
|
||||
}
|
||||
|
||||
private void applyBindingDefaults(Agent agent, AgentToolBinding binding, int index) {
|
||||
LoginAccount account = requireCurrentLoginAccount();
|
||||
Date now = new Date();
|
||||
binding.setId(null);
|
||||
binding.setTenantId(agent.getTenantId());
|
||||
binding.setAgentId(agent.getId());
|
||||
binding.setEnabled(binding.getEnabled() == null || binding.getEnabled());
|
||||
binding.setHitlEnabled(Boolean.TRUE.equals(binding.getHitlEnabled()));
|
||||
binding.setSortNo(binding.getSortNo() == null ? index : binding.getSortNo());
|
||||
binding.setCreated(now);
|
||||
binding.setCreatedBy(account.getId());
|
||||
binding.setModified(now);
|
||||
binding.setModifiedBy(account.getId());
|
||||
}
|
||||
|
||||
private LoginAccount requireCurrentLoginAccount() {
|
||||
try {
|
||||
return SaTokenUtil.getLoginAccount();
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException("当前登录状态失效,请重新登录后再试");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package tech.easyflow.agent.publish;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.agent.service.AgentKnowledgeBindingService;
|
||||
import tech.easyflow.agent.service.AgentToolBindingService;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.math.BigInteger;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* {@link AgentApprovalSubjectHandler} 单元测试。
|
||||
*/
|
||||
public class AgentApprovalSubjectHandlerTest {
|
||||
|
||||
/**
|
||||
* 审批删除 Agent 前必须同步清理工具绑定和知识库绑定,避免留下孤儿数据。
|
||||
*/
|
||||
@Test
|
||||
public void beforeRemoveShouldCleanAgentBindings() {
|
||||
AtomicInteger toolRemoveCalls = new AtomicInteger();
|
||||
AtomicInteger knowledgeRemoveCalls = new AtomicInteger();
|
||||
AgentToolBindingService toolBindingService = proxy(AgentToolBindingService.class, toolRemoveCalls);
|
||||
AgentKnowledgeBindingService knowledgeBindingService = proxy(AgentKnowledgeBindingService.class, knowledgeRemoveCalls);
|
||||
AgentApprovalSubjectHandler handler = new AgentApprovalSubjectHandler(
|
||||
null,
|
||||
new ObjectMapper(),
|
||||
null,
|
||||
toolBindingService,
|
||||
knowledgeBindingService,
|
||||
null
|
||||
);
|
||||
|
||||
handler.beforeRemove(BigInteger.valueOf(1001));
|
||||
|
||||
Assert.assertEquals(1, toolRemoveCalls.get());
|
||||
Assert.assertEquals(1, knowledgeRemoveCalls.get());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T proxy(Class<T> type, AtomicInteger removeCalls) {
|
||||
return (T) Proxy.newProxyInstance(
|
||||
type.getClassLoader(),
|
||||
new Class<?>[]{type},
|
||||
(proxy, method, args) -> {
|
||||
if ("remove".equals(method.getName()) && args != null && args.length == 1) {
|
||||
removeCalls.incrementAndGet();
|
||||
return true;
|
||||
}
|
||||
if (method.getReturnType() == boolean.class || method.getReturnType() == Boolean.class) {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package tech.easyflow.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentInitRequest;
|
||||
import com.easyagents.agent.runtime.AgentResumeRequest;
|
||||
import com.easyagents.agent.runtime.AgentRuntime;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.core.runtime.ChatAssistantAccumulator;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Agent 运行态注册表测试。
|
||||
*/
|
||||
public class AgentRunRegistryTest {
|
||||
|
||||
/**
|
||||
* 验证批准请求会恢复当前运行时。
|
||||
*/
|
||||
@Test
|
||||
public void approveShouldResumeRuntime() {
|
||||
AgentRunRegistry registry = new AgentRunRegistry();
|
||||
CapturingRuntime runtime = new CapturingRuntime();
|
||||
|
||||
registry.register(context("request-1", "session-1", "user-1", runtime));
|
||||
registry.registerResumeToken("request-1", "token-1");
|
||||
registry.approve("request-1", "token-1", "user-1");
|
||||
|
||||
AgentResumeRequest request = runtime.resumeRequest.get();
|
||||
Assert.assertNotNull(request);
|
||||
Assert.assertTrue(request.isApproved());
|
||||
Assert.assertEquals("token-1", request.getResumeToken().getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证拒绝请求支持通过恢复令牌反查运行时。
|
||||
*/
|
||||
@Test
|
||||
public void rejectShouldResolveRequestByResumeToken() {
|
||||
AgentRunRegistry registry = new AgentRunRegistry();
|
||||
CapturingRuntime runtime = new CapturingRuntime();
|
||||
|
||||
registry.register(context("request-2", "session-2", "user-1", runtime));
|
||||
registry.registerResumeToken("request-2", "token-2");
|
||||
registry.reject(null, "token-2", "user-1", "denied");
|
||||
|
||||
AgentResumeRequest request = runtime.resumeRequest.get();
|
||||
Assert.assertNotNull(request);
|
||||
Assert.assertFalse(request.isApproved());
|
||||
Assert.assertEquals("denied", request.getRejectReason());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证运行结束后清理运行态与恢复令牌索引。
|
||||
*/
|
||||
@Test
|
||||
public void removeShouldClearRuntimeAndResumeTokens() {
|
||||
AgentRunRegistry registry = new AgentRunRegistry();
|
||||
|
||||
registry.register(context("request-3", "session-3", "user-1", new CapturingRuntime()));
|
||||
registry.registerResumeToken("request-3", "token-3");
|
||||
registry.remove("request-3");
|
||||
|
||||
Assert.assertThrows(BusinessException.class, () -> registry.approve(null, "token-3", "user-1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证运行审批只能由运行发起人处理。
|
||||
*/
|
||||
@Test
|
||||
public void approveShouldRejectDifferentOwner() {
|
||||
AgentRunRegistry registry = new AgentRunRegistry();
|
||||
CapturingRuntime runtime = new CapturingRuntime();
|
||||
|
||||
registry.register(context("request-4", "session-4", "user-1", runtime));
|
||||
registry.registerResumeToken("request-4", "token-4");
|
||||
|
||||
Assert.assertThrows(BusinessException.class, () -> registry.approve("request-4", "token-4", "user-2"));
|
||||
Assert.assertNull(runtime.resumeRequest.get());
|
||||
|
||||
registry.approve("request-4", "token-4", "user-1");
|
||||
Assert.assertTrue(runtime.resumeRequest.get().isApproved());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证传入 requestId 时仍会校验恢复令牌归属,避免错误令牌打断挂起运行。
|
||||
*/
|
||||
@Test
|
||||
public void approveShouldRejectTokenNotBelongingToRequest() {
|
||||
AgentRunRegistry registry = new AgentRunRegistry();
|
||||
CapturingRuntime runtime = new CapturingRuntime();
|
||||
|
||||
registry.register(context("request-5", "session-5", "user-1", runtime));
|
||||
|
||||
Assert.assertThrows(BusinessException.class,
|
||||
() -> registry.approve("request-5", "wrong-token", "user-1"));
|
||||
Assert.assertNull(runtime.resumeRequest.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证同一会话同一时刻只允许一个运行态。
|
||||
*/
|
||||
@Test
|
||||
public void registerShouldRejectActiveRunInSameSession() {
|
||||
AgentRunRegistry registry = new AgentRunRegistry();
|
||||
|
||||
registry.register(context("request-6", "session-6", "user-1", new CapturingRuntime()));
|
||||
|
||||
Assert.assertThrows(BusinessException.class,
|
||||
() -> registry.register(context("request-7", "session-6", "user-1", new CapturingRuntime())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证按会话清理会取消当前运行并释放同会话运行锁。
|
||||
*/
|
||||
@Test
|
||||
public void cancelSessionShouldRemoveActiveRun() {
|
||||
AgentRunRegistry registry = new AgentRunRegistry();
|
||||
AgentRunRegistry.AgentRunContext context = context("request-8", "session-8", "user-1", new CapturingRuntime());
|
||||
|
||||
registry.register(context);
|
||||
registry.cancelSession("session-8");
|
||||
|
||||
Assert.assertNull(registry.get("request-8"));
|
||||
registry.register(context("request-9", "session-8", "user-1", new CapturingRuntime()));
|
||||
Assert.assertNotNull(registry.get("request-9"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证按会话清理时会校验运行归属。
|
||||
*/
|
||||
@Test
|
||||
public void cancelSessionShouldRejectDifferentOwner() {
|
||||
AgentRunRegistry registry = new AgentRunRegistry();
|
||||
|
||||
registry.register(context("request-10", "session-10", "user-1", new CapturingRuntime()));
|
||||
|
||||
Assert.assertThrows(BusinessException.class, () -> registry.cancelSession("session-10", "user-2"));
|
||||
Assert.assertNotNull(registry.get("request-10"));
|
||||
}
|
||||
|
||||
private AgentRunRegistry.AgentRunContext context(String requestId,
|
||||
String sessionId,
|
||||
String userId,
|
||||
AgentRuntime runtime) {
|
||||
return new AgentRunRegistry.AgentRunContext(
|
||||
requestId,
|
||||
sessionId,
|
||||
runtime,
|
||||
null,
|
||||
new ChatRuntimeContext(),
|
||||
new StringBuilder(),
|
||||
new ChatAssistantAccumulator(),
|
||||
new AtomicBoolean(false),
|
||||
false,
|
||||
new AgentRunRegistry.RunOwner("agent-1", sessionId, userId),
|
||||
null,
|
||||
event -> {
|
||||
},
|
||||
error -> {
|
||||
},
|
||||
() -> {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static final class CapturingRuntime implements AgentRuntime {
|
||||
|
||||
private final AtomicReference<AgentResumeRequest> resumeRequest = new AtomicReference<>();
|
||||
|
||||
@Override
|
||||
public void init(AgentInitRequest request) {
|
||||
// 测试桩无需初始化。
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<AgentRuntimeEvent> stream(com.easyagents.agent.runtime.message.AgentMessage userMessage) {
|
||||
return Flux.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<AgentRuntimeEvent> resume(AgentResumeRequest request) {
|
||||
resumeRequest.set(request);
|
||||
return Flux.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,660 @@
|
||||
package tech.easyflow.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||
import com.easyagents.agent.runtime.message.AgentKnowledgeReference;
|
||||
import com.easyagents.agent.runtime.message.AgentMessage;
|
||||
import com.easyagents.agent.runtime.message.AgentMessageRole;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.runtime.lock.AgentRunLock;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.core.chat.protocol.ChatDomain;
|
||||
import tech.easyflow.core.chat.protocol.ChatEnvelope;
|
||||
import tech.easyflow.core.chat.protocol.ChatType;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||
import tech.easyflow.core.runtime.ChatAssistantAccumulator;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeManager;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeMessage;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Agent 草稿试用与 HITL 事件映射测试。
|
||||
*/
|
||||
public class AgentRunServiceDraftAndHitlTest {
|
||||
|
||||
/**
|
||||
* 验证工具 HITL 事件会映射为显式前端载荷。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void buildToolHitlPayloadShouldExposeStableFieldsWithoutPrompt() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED);
|
||||
event.setToolCallId("call-1");
|
||||
event.getPayload().put("resumeToken", "token-1");
|
||||
event.getPayload().put("sessionId", "session-1");
|
||||
event.getPayload().put("agentId", "agent-1");
|
||||
event.getPayload().put("toolName", "search");
|
||||
event.getPayload().put("toolType", "PLUGIN");
|
||||
event.getPayload().put("approvalPrompt", "不应透出");
|
||||
event.getPayload().put("toolInput", Map.of("keyword", "EasyFlow"));
|
||||
event.getPayload().put("approvalMetadata", Map.of(
|
||||
"risk", "low",
|
||||
"prompt", "不应透出",
|
||||
"toolType", "WORKFLOW"
|
||||
));
|
||||
|
||||
AgentToolHitlPayload payload = invoke(service, "buildToolHitlPayload",
|
||||
new Class<?>[]{String.class, AgentRuntimeEvent.class}, "request-1", event);
|
||||
|
||||
Assert.assertEquals("request-1", payload.getRequestId());
|
||||
Assert.assertEquals("token-1", payload.getResumeToken());
|
||||
Assert.assertEquals("session-1", payload.getSessionId());
|
||||
Assert.assertEquals("agent-1", payload.getAgentId());
|
||||
Assert.assertEquals("call-1", payload.getToolCallId());
|
||||
Assert.assertEquals("search", payload.getToolName());
|
||||
Assert.assertEquals("PLUGIN", payload.getToolType());
|
||||
Assert.assertEquals("EasyFlow", payload.getInput().get("keyword"));
|
||||
Assert.assertEquals("low", payload.getMetadata().get("risk"));
|
||||
Assert.assertEquals("PLUGIN", payload.getMetadata().get("toolType"));
|
||||
Assert.assertFalse(payload.getMetadata().containsKey("prompt"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证工具事件发送给前端时会携带稳定工具调用 ID。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void buildToolEventPayloadShouldExposeRuntimeToolCallId() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.TOOL_RESULT);
|
||||
event.setToolCallId("call-runtime");
|
||||
event.getPayload().put("toolName", "search");
|
||||
event.getPayload().put("text", "ok");
|
||||
|
||||
Map<String, Object> payload = invoke(service, "buildToolEventPayload",
|
||||
new Class<?>[]{AgentRuntimeEvent.class}, event);
|
||||
|
||||
Assert.assertEquals("call-runtime", payload.get("toolCallId"));
|
||||
Assert.assertEquals("search", payload.get("toolName"));
|
||||
Assert.assertEquals("ok", payload.get("text"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证思考事件会优先读取 reasoning 字段。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void stringPayloadShouldExposeReasoningValue() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.REASONING_DELTA);
|
||||
event.getPayload().put("reasoning", "思考中");
|
||||
|
||||
String reasoning = invoke(service, "stringPayload",
|
||||
new Class<?>[]{AgentRuntimeEvent.class, String.class}, event, "reasoning");
|
||||
String fallback = invoke(service, "firstText",
|
||||
new Class<?>[]{String.class, String.class}, reasoning, "正文");
|
||||
|
||||
Assert.assertEquals("思考中", fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证思考事件会作为增量发送给前端。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void handleRuntimeEventShouldSendReasoningDeltaAsThinkingEnvelope() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.REASONING_DELTA);
|
||||
event.getPayload().put("reasoning", "思考增量");
|
||||
|
||||
invoke(service, "handleRuntimeEvent",
|
||||
runtimeEventParameterTypes(),
|
||||
event, "request-1", emitter, new StringBuilder(), new ChatAssistantAccumulator(),
|
||||
chatContext(), new AtomicBoolean(false), false);
|
||||
|
||||
Assert.assertEquals(1, emitter.envelopes.size());
|
||||
Assert.assertEquals(ChatDomain.LLM, emitter.envelopes.get(0).getDomain());
|
||||
Assert.assertEquals(ChatType.THINKING, emitter.envelopes.get(0).getType());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> payload = (Map<String, Object>) emitter.envelopes.get(0).getPayload();
|
||||
Assert.assertEquals("思考增量", payload.get("reasoning"));
|
||||
Assert.assertEquals("思考增量", payload.get("delta"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证正文事件会作为增量发送给前端并累计到持久化缓冲。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void handleRuntimeEventShouldSendMessageDeltaAndAccumulateAnswer() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
|
||||
StringBuilder answer = new StringBuilder();
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.MESSAGE_DELTA);
|
||||
event.getPayload().put("text", "正文增量");
|
||||
|
||||
invoke(service, "handleRuntimeEvent",
|
||||
runtimeEventParameterTypes(),
|
||||
event, "request-1", emitter, answer, new ChatAssistantAccumulator(),
|
||||
chatContext(), new AtomicBoolean(false), false);
|
||||
|
||||
Assert.assertEquals("正文增量", answer.toString());
|
||||
Assert.assertEquals(1, emitter.envelopes.size());
|
||||
Assert.assertEquals(ChatDomain.LLM, emitter.envelopes.get(0).getDomain());
|
||||
Assert.assertEquals(ChatType.MESSAGE, emitter.envelopes.get(0).getType());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> payload = (Map<String, Object>) emitter.envelopes.get(0).getPayload();
|
||||
Assert.assertEquals("正文增量", payload.get("delta"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证自动上下文压缩事件会作为业务状态发送给前端。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void handleRuntimeEventShouldSendMemoryCompressionAsStatusEnvelope() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.MEMORY_COMPRESSION_STARTED);
|
||||
event.getPayload().put("statusKey", "memory-compression");
|
||||
event.getPayload().put("status", "running");
|
||||
event.getPayload().put("label", "正在整理上下文");
|
||||
|
||||
invoke(service, "handleRuntimeEvent",
|
||||
runtimeEventParameterTypes(),
|
||||
event, "request-1", emitter, new StringBuilder(), new ChatAssistantAccumulator(),
|
||||
chatContext(), new AtomicBoolean(false), false);
|
||||
|
||||
Assert.assertEquals(1, emitter.envelopes.size());
|
||||
Assert.assertEquals(ChatDomain.BUSINESS, emitter.envelopes.get(0).getDomain());
|
||||
Assert.assertEquals(ChatType.STATUS, emitter.envelopes.get(0).getType());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> payload = (Map<String, Object>) emitter.envelopes.get(0).getPayload();
|
||||
Assert.assertEquals("memory-compression", payload.get("statusKey"));
|
||||
Assert.assertEquals("running", payload.get("status"));
|
||||
Assert.assertEquals("正在整理上下文", payload.get("label"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证完成事件不会再次发送正文消息,只用于最终收口。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void handleRuntimeEventShouldNotSendMessageEnvelopeOnCompleted() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
setField(service, "agentRunRegistry", new AgentRunRegistry());
|
||||
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
|
||||
StringBuilder answer = new StringBuilder("流式正文");
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.COMPLETED);
|
||||
event.getPayload().put("text", "最终正文");
|
||||
|
||||
invoke(service, "handleRuntimeEvent",
|
||||
runtimeEventParameterTypes(),
|
||||
event, "request-1", emitter, answer, new ChatAssistantAccumulator(),
|
||||
chatContext(), new AtomicBoolean(false), false);
|
||||
|
||||
Assert.assertEquals("最终正文", answer.toString());
|
||||
Assert.assertTrue(emitter.envelopes.stream().noneMatch(envelope ->
|
||||
envelope.getDomain() == ChatDomain.LLM && envelope.getType() == ChatType.MESSAGE));
|
||||
Assert.assertTrue(emitter.envelopes.stream().anyMatch(envelope ->
|
||||
envelope.getDomain() == ChatDomain.SYSTEM && envelope.getType() == ChatType.DONE));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证挂起事件后自然完成不会关闭 SSE 或清理运行态。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void finishIfNeededShouldKeepRuntimeWhenSuspended() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
AgentRunRegistry registry = new AgentRunRegistry();
|
||||
setField(service, "agentRunRegistry", registry);
|
||||
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
|
||||
AtomicBoolean finished = new AtomicBoolean(false);
|
||||
AgentRunRegistry.AgentRunContext runContext = new AgentRunRegistry.AgentRunContext(
|
||||
"request-suspended",
|
||||
"session-suspended",
|
||||
new NoopRuntime(),
|
||||
emitter,
|
||||
chatContext(),
|
||||
new StringBuilder(),
|
||||
new ChatAssistantAccumulator(),
|
||||
finished,
|
||||
false,
|
||||
new AgentRunRegistry.RunOwner("agent-1", "session-suspended", "user-1"),
|
||||
null,
|
||||
event -> {
|
||||
},
|
||||
error -> {
|
||||
},
|
||||
() -> {
|
||||
}
|
||||
);
|
||||
registry.register(runContext);
|
||||
runContext.markSuspended();
|
||||
|
||||
invoke(service, "finishIfNeeded",
|
||||
new Class<?>[]{String.class, ChatSseEmitter.class, ChatRuntimeContext.class, StringBuilder.class,
|
||||
ChatAssistantAccumulator.class, AtomicBoolean.class, boolean.class},
|
||||
"request-suspended", emitter, chatContext(), new StringBuilder(),
|
||||
new ChatAssistantAccumulator(), finished, false);
|
||||
|
||||
Assert.assertFalse(finished.get());
|
||||
Assert.assertNotNull(registry.get("request-suspended"));
|
||||
Assert.assertTrue(emitter.envelopes.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证取消事件作为业务状态收口,不按系统错误发送。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void handleRuntimeEventShouldSendCancelledAsStatusAndDone() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
setField(service, "agentRunRegistry", new AgentRunRegistry());
|
||||
RecordingChatRuntimeManager chatRuntimeManager = new RecordingChatRuntimeManager();
|
||||
setField(service, "chatRuntimeManager", chatRuntimeManager);
|
||||
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
|
||||
StringBuilder answer = new StringBuilder("取消前正文");
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.CANCELLED);
|
||||
event.getPayload().put("reason", "用户拒绝执行");
|
||||
|
||||
invoke(service, "handleRuntimeEvent",
|
||||
runtimeEventParameterTypes(),
|
||||
event, "request-1", emitter, answer, new ChatAssistantAccumulator(),
|
||||
chatContext(), new AtomicBoolean(false), true);
|
||||
|
||||
Assert.assertEquals(2, emitter.envelopes.size());
|
||||
Assert.assertEquals(ChatDomain.BUSINESS, emitter.envelopes.get(0).getDomain());
|
||||
Assert.assertEquals(ChatType.STATUS, emitter.envelopes.get(0).getType());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> payload = (Map<String, Object>) emitter.envelopes.get(0).getPayload();
|
||||
Assert.assertEquals("agent-cancelled", payload.get("statusKey"));
|
||||
Assert.assertEquals("cancelled", payload.get("status"));
|
||||
Assert.assertEquals("用户拒绝执行", payload.get("message"));
|
||||
Assert.assertEquals(ChatDomain.SYSTEM, emitter.envelopes.get(1).getDomain());
|
||||
Assert.assertEquals(ChatType.DONE, emitter.envelopes.get(1).getType());
|
||||
Assert.assertEquals(1, chatRuntimeManager.recordAssistantCompletedCount);
|
||||
Assert.assertEquals("取消前正文", chatRuntimeManager.lastAssistantMessage.getContentText());
|
||||
Assert.assertEquals(1, chatRuntimeManager.recordFailureCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证最终知识库引用会保留命中分片原文。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void buildKnowledgeCitationPayloadShouldExposeChunkContent() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.COMPLETED);
|
||||
AgentMessage message = AgentMessage.text(AgentMessageRole.ASSISTANT, "answer");
|
||||
AgentKnowledgeReference reference = new AgentKnowledgeReference();
|
||||
reference.setKnowledgeId("kb-1");
|
||||
reference.setKnowledgeName("学生事务 FAQ");
|
||||
reference.setDocumentId("faq-1");
|
||||
reference.setDocumentName("faq-1.md");
|
||||
reference.setChunkId("chunk-1");
|
||||
reference.setChunkContent("暑假安排原文");
|
||||
reference.setScore(0.91D);
|
||||
reference.getMetadata().put("knowledgeType", "FAQ");
|
||||
reference.getMetadata().put("faqCollection", true);
|
||||
message.setKnowledgeReferences(List.of(reference));
|
||||
event.setMessage(message);
|
||||
|
||||
List<Map<String, Object>> payload = invoke(service, "buildKnowledgeCitationPayload",
|
||||
new Class<?>[]{AgentRuntimeEvent.class}, event);
|
||||
|
||||
Assert.assertEquals(1, payload.size());
|
||||
Assert.assertEquals("学生事务 FAQ", payload.get(0).get("knowledgeName"));
|
||||
Assert.assertEquals("暑假安排原文", payload.get(0).get("chunkContent"));
|
||||
Assert.assertEquals("FAQ", payload.get(0).get("knowledgeType"));
|
||||
Assert.assertEquals(Boolean.TRUE, payload.get(0).get("faqCollection"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证未保存草稿会生成临时 Agent ID,并把绑定指向该运行 ID。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void buildDraftAgentShouldGenerateRuntimeIdForUnsavedAgent() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
AgentDraftChatRequest request = new AgentDraftChatRequest();
|
||||
Agent agent = new Agent();
|
||||
agent.setModelId(BigInteger.valueOf(10));
|
||||
request.setAgent(agent);
|
||||
|
||||
AgentToolBinding toolBinding = new AgentToolBinding();
|
||||
toolBinding.setToolType("PLUGIN");
|
||||
toolBinding.setTargetId(BigInteger.valueOf(20));
|
||||
toolBinding.setResourceSnapshot(Map.of("name", "client-forged"));
|
||||
toolBinding.setResourceSummary(Map.of("name", "client-forged"));
|
||||
request.setToolBindings(List.of(toolBinding));
|
||||
|
||||
AgentKnowledgeBinding knowledgeBinding = new AgentKnowledgeBinding();
|
||||
knowledgeBinding.setKnowledgeId(BigInteger.valueOf(30));
|
||||
knowledgeBinding.setResourceSnapshot(Map.of("title", "client-forged"));
|
||||
knowledgeBinding.setResourceSummary(Map.of("title", "client-forged"));
|
||||
request.setKnowledgeBindings(List.of(knowledgeBinding));
|
||||
|
||||
LoginAccount account = new LoginAccount();
|
||||
account.setId(BigInteger.ONE);
|
||||
account.setTenantId(BigInteger.valueOf(2));
|
||||
account.setDeptId(BigInteger.valueOf(3));
|
||||
|
||||
Agent draftAgent = invoke(service, "buildDraftAgent",
|
||||
new Class<?>[]{AgentDraftChatRequest.class, LoginAccount.class}, request, account);
|
||||
|
||||
Assert.assertNotNull(draftAgent.getId());
|
||||
Assert.assertEquals(BigInteger.valueOf(2), draftAgent.getTenantId());
|
||||
Assert.assertEquals(draftAgent.getId(), draftAgent.getToolBindings().get(0).getAgentId());
|
||||
Assert.assertEquals(draftAgent.getId(), draftAgent.getKnowledgeBindings().get(0).getAgentId());
|
||||
Assert.assertTrue(draftAgent.getToolBindings().get(0).getEnabled());
|
||||
Assert.assertTrue(draftAgent.getKnowledgeBindings().get(0).getEnabled());
|
||||
Assert.assertTrue(draftAgent.getToolBindings().get(0).getResourceSnapshot().isEmpty());
|
||||
Assert.assertTrue(draftAgent.getToolBindings().get(0).getResourceSummary().isEmpty());
|
||||
Assert.assertTrue(draftAgent.getKnowledgeBindings().get(0).getResourceSnapshot().isEmpty());
|
||||
Assert.assertTrue(draftAgent.getKnowledgeBindings().get(0).getResourceSummary().isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证正式聊天在获取运行锁失败时不会提前写入用户消息。
|
||||
*
|
||||
* <p>运行锁是 AgentScope session 与 chatlog 的一致性入口。若同会话并发请求抢锁失败,
|
||||
* 必须在 prepareSession 和 recordUserMessage 之前失败,避免 chatlog 出现没有真实运行的用户消息。</p>
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void runShouldAcquireLockBeforePersistingUserMessage() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
RecordingChatRuntimeManager chatRuntimeManager = new RecordingChatRuntimeManager();
|
||||
setField(service, "chatRuntimeManager", chatRuntimeManager);
|
||||
setField(service, "agentRunLock", new RejectingAgentRunLock());
|
||||
|
||||
Agent agent = new Agent();
|
||||
agent.setId(BigInteger.valueOf(100));
|
||||
ChatRuntimeContext context = chatContext();
|
||||
|
||||
Exception thrown = Assert.assertThrows(Exception.class, () -> invoke(service, "run",
|
||||
new Class<?>[]{Agent.class, String.class, String.class, String.class, String.class,
|
||||
String.class, ChatRuntimeContext.class, boolean.class},
|
||||
agent, "你好", "request-lock", "trace-lock", "session-lock", "AGENT", context, true));
|
||||
|
||||
Assert.assertTrue(rootCause(thrown) instanceof BusinessException);
|
||||
Assert.assertEquals(0, chatRuntimeManager.prepareSessionCount);
|
||||
Assert.assertEquals(0, chatRuntimeManager.recordUserMessageCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证正式聊天会在会话准备完成后向前端返回真实会话 ID。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void sendSessionCreatedShouldExposeSessionId() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
|
||||
|
||||
Boolean sent = invoke(service, "sendSessionCreated",
|
||||
new Class<?>[]{ChatSseEmitter.class, BigInteger.class}, emitter, BigInteger.valueOf(123));
|
||||
|
||||
Assert.assertTrue(sent);
|
||||
Assert.assertEquals(1, emitter.envelopes.size());
|
||||
Assert.assertEquals(ChatDomain.SYSTEM, emitter.envelopes.get(0).getDomain());
|
||||
Assert.assertEquals(ChatType.SESSION_CREATED, emitter.envelopes.get(0).getType());
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> payload = (Map<String, Object>) emitter.envelopes.get(0).getPayload();
|
||||
Assert.assertEquals("123", payload.get("sessionId"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证正式聊天只有新会话首轮会自动设置默认标题。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void applyFormalSessionTitleShouldOnlyUseFirstPromptForNewSession() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
ChatRuntimeContext newContext = chatContext();
|
||||
ChatRuntimeContext existingContext = chatContext();
|
||||
ChatSessionSummary existingSession = new ChatSessionSummary();
|
||||
existingSession.setId(BigInteger.valueOf(123));
|
||||
existingSession.setTitle("用户改过的标题");
|
||||
existingSession.setMessageCount(2);
|
||||
|
||||
invoke(service, "applyFormalSessionTitle",
|
||||
new Class<?>[]{ChatRuntimeContext.class, String.class, ChatSessionSummary.class},
|
||||
newContext, "第一句话", null);
|
||||
invoke(service, "applyFormalSessionTitle",
|
||||
new Class<?>[]{ChatRuntimeContext.class, String.class, ChatSessionSummary.class},
|
||||
existingContext, "后续消息", existingSession);
|
||||
|
||||
Assert.assertEquals("第一句话", newContext.getSessionTitle());
|
||||
Assert.assertNull(existingContext.getSessionTitle());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证正式聊天 SSE 断开后会取消运行并保存断开前已输出的 assistant 内容。
|
||||
*
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void handleRuntimeEventShouldCancelAndRecordFailureWhenSseDisconnected() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
AgentRunRegistry registry = new AgentRunRegistry();
|
||||
RecordingChatRuntimeManager chatRuntimeManager = new RecordingChatRuntimeManager();
|
||||
setField(service, "agentRunRegistry", registry);
|
||||
setField(service, "chatRuntimeManager", chatRuntimeManager);
|
||||
|
||||
AtomicBoolean finished = new AtomicBoolean(false);
|
||||
ChatRuntimeContext context = chatContext();
|
||||
AgentRunRegistry.AgentRunContext runContext = new AgentRunRegistry.AgentRunContext(
|
||||
"request-disconnected",
|
||||
"session-disconnected",
|
||||
new NoopRuntime(),
|
||||
new FailingChatSseEmitter(),
|
||||
context,
|
||||
new StringBuilder(),
|
||||
new ChatAssistantAccumulator(),
|
||||
finished,
|
||||
true,
|
||||
new AgentRunRegistry.RunOwner("agent-1", "session-disconnected", "user-1"),
|
||||
null,
|
||||
ignored -> {
|
||||
},
|
||||
ignored -> {
|
||||
},
|
||||
() -> {
|
||||
}
|
||||
);
|
||||
registry.register(runContext);
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.MESSAGE_DELTA);
|
||||
event.getPayload().put("text", "断连前正文");
|
||||
StringBuilder answer = new StringBuilder();
|
||||
ChatAssistantAccumulator assistantAccumulator = new ChatAssistantAccumulator();
|
||||
|
||||
invoke(service, "handleRuntimeEvent",
|
||||
runtimeEventParameterTypes(),
|
||||
event, "request-disconnected", new FailingChatSseEmitter(), answer,
|
||||
assistantAccumulator, context, finished, true);
|
||||
|
||||
Assert.assertTrue(finished.get());
|
||||
Assert.assertNull(registry.get("request-disconnected"));
|
||||
Assert.assertEquals(1, chatRuntimeManager.recordAssistantCompletedCount);
|
||||
Assert.assertEquals("断连前正文", chatRuntimeManager.lastAssistantMessage.getContentText());
|
||||
Assert.assertEquals(1, chatRuntimeManager.recordFailureCount);
|
||||
Assert.assertEquals(0, chatRuntimeManager.recordCompletedCount);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T invoke(Object target, String methodName, Class<?>[] parameterTypes, Object... args) throws Exception {
|
||||
Method method = target.getClass().getDeclaredMethod(methodName, parameterTypes);
|
||||
method.setAccessible(true);
|
||||
return (T) method.invoke(target, args);
|
||||
}
|
||||
|
||||
private void setField(Object target, String fieldName, Object value) throws Exception {
|
||||
java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
private Class<?>[] runtimeEventParameterTypes() {
|
||||
return new Class<?>[]{AgentRuntimeEvent.class, String.class, ChatSseEmitter.class, StringBuilder.class,
|
||||
ChatAssistantAccumulator.class,
|
||||
ChatRuntimeContext.class, AtomicBoolean.class, boolean.class};
|
||||
}
|
||||
|
||||
private ChatRuntimeContext chatContext() {
|
||||
ChatRuntimeContext context = new ChatRuntimeContext();
|
||||
context.setAssistantId(BigInteger.valueOf(100));
|
||||
context.setAssistantName("Agent");
|
||||
context.setUserId(BigInteger.valueOf(101));
|
||||
context.setUserName("用户");
|
||||
return context;
|
||||
}
|
||||
|
||||
private Throwable rootCause(Throwable throwable) {
|
||||
Throwable current = throwable;
|
||||
while (current.getCause() != null) {
|
||||
current = current.getCause();
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录发送内容的 SSE 测试桩。
|
||||
*/
|
||||
private static class RecordingChatSseEmitter extends ChatSseEmitter {
|
||||
|
||||
private final List<ChatEnvelope<?>> envelopes = new java.util.ArrayList<>();
|
||||
|
||||
@Override
|
||||
public boolean send(ChatEnvelope<?> envelope) {
|
||||
envelopes.add(envelope);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sendDone(ChatEnvelope<?> envelope) {
|
||||
envelopes.add(envelope);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FailingChatSseEmitter extends ChatSseEmitter {
|
||||
|
||||
@Override
|
||||
public boolean send(ChatEnvelope<?> envelope) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sendDone(ChatEnvelope<?> envelope) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoopRuntime implements com.easyagents.agent.runtime.AgentRuntime {
|
||||
|
||||
@Override
|
||||
public void init(com.easyagents.agent.runtime.AgentInitRequest request) {
|
||||
// 测试桩无需初始化。
|
||||
}
|
||||
|
||||
@Override
|
||||
public reactor.core.publisher.Flux<AgentRuntimeEvent> stream(AgentMessage userMessage) {
|
||||
return reactor.core.publisher.Flux.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public reactor.core.publisher.Flux<AgentRuntimeEvent> resume(com.easyagents.agent.runtime.AgentResumeRequest request) {
|
||||
return reactor.core.publisher.Flux.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 chatlog 写入动作的测试桩。
|
||||
*/
|
||||
private static class RecordingChatRuntimeManager implements ChatRuntimeManager {
|
||||
|
||||
private int prepareSessionCount;
|
||||
private int recordUserMessageCount;
|
||||
private int recordAssistantCompletedCount;
|
||||
private int recordFailureCount;
|
||||
private int recordCompletedCount;
|
||||
private ChatRuntimeMessage lastAssistantMessage;
|
||||
|
||||
@Override
|
||||
public void prepareSession(ChatRuntimeContext context) {
|
||||
prepareSessionCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordUserMessage(ChatRuntimeContext context, ChatRuntimeMessage message) {
|
||||
recordUserMessageCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordAssistantDelta(ChatRuntimeContext context, ChatRuntimeMessage message) {
|
||||
// 测试桩无需记录。
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordAssistantCompleted(ChatRuntimeContext context, ChatRuntimeMessage message) {
|
||||
recordAssistantCompletedCount++;
|
||||
lastAssistantMessage = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordFailure(ChatRuntimeContext context, Throwable throwable) {
|
||||
recordFailureCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordCompleted(ChatRuntimeContext context) {
|
||||
recordCompletedCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatRuntimeMessage> loadMessages(ChatRuntimeContext context, int limit) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟同会话运行锁已被占用的测试桩。
|
||||
*/
|
||||
private static class RejectingAgentRunLock implements AgentRunLock {
|
||||
|
||||
@Override
|
||||
public Handle acquire(BigInteger agentId, String sessionId) {
|
||||
throw new BusinessException("当前 Agent 会话已有运行中的请求,请稍后再试");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user