Compare commits
59 Commits
8356560c26
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 130423edb4 | |||
| 7900552ede | |||
| 368b90b211 | |||
| 1ea7b7527f | |||
| 68fd303656 | |||
| c1fe64cefa | |||
| 1b36067e6c | |||
| 876517f821 | |||
| 2d50f7de15 | |||
| 93296eb810 | |||
| 47a2706f11 | |||
| e146653de7 | |||
| 880f810ba2 | |||
| b4bdc392ee | |||
| 02b8fdd3ae | |||
| 2f67d90144 | |||
| 0af5147c19 | |||
| 870c2cc583 | |||
| c8be163124 | |||
| 9612c5bd62 | |||
| c7d410d755 | |||
| 49a7de34bb | |||
| a34ca9271e | |||
| b313523aba | |||
| 857fe7caf8 | |||
| 8d8d77ffda | |||
| f13e24751a | |||
| bdb69a2250 | |||
| 5fd4d845af | |||
| 15adcfff42 | |||
| f0a5aacc92 | |||
| fd3d9ad419 | |||
| 4af2d7cd34 | |||
| fcc36dc699 | |||
| e74d229de2 | |||
| 851dd1be01 | |||
| 12491b3724 | |||
| c72a167633 | |||
| a7e89cee3d | |||
| c48d9a9da6 | |||
| 6fa93bd671 | |||
| e995088d79 | |||
| 5b6b2db5d8 | |||
| fbeece2d89 | |||
| 7e59f0e638 | |||
| f057900f7a | |||
| 66da0c9039 | |||
| 9d0d148415 | |||
| 3bd346ea77 | |||
| 848197b556 | |||
| 13e848ddf4 | |||
| 55434466d4 | |||
| 7cac558b6c | |||
| 43f45956ff | |||
| 2bc525c16e | |||
| f324acb83c | |||
| 8b600f9d6c | |||
| 5c7182ac3f | |||
| 2b5e701ade |
51
README.md
51
README.md
@@ -10,6 +10,8 @@ Easy-Agents 是一个轻量、可扩展的 Java AI 应用开发框架,覆盖
|
||||
- MCP 客户端能力(调用、拦截、缓存与管理)
|
||||
- 文档读取与切分、向量存储与检索
|
||||
- 工作流执行引擎(Flow)与 Easy-Agents 适配支持
|
||||
- 基于 Calcite 的 SQL 编译、方言适配与流式 JDBC 查询
|
||||
- 基于 Quartz JDBC JobStore 的嵌入式分布式定时调度
|
||||
|
||||
## 模块说明
|
||||
|
||||
@@ -24,7 +26,10 @@ Easy-Agents 是一个轻量、可扩展的 Java AI 应用开发框架,覆盖
|
||||
- `easy-agents-search-engine`:检索引擎实现。
|
||||
- `easy-agents-tool`:工具调用能力。
|
||||
- `easy-agents-mcp`:MCP 集成。
|
||||
- `easy-agents-skill`:标准 Agent Skills 包模型、安全校验、资源存储与 ZIP 双向编解码。
|
||||
- `easy-agents-flow`:流程编排核心引擎。
|
||||
- `easy-agents-federation-sql`:高性能 SQL 联邦查询内核与可扩展数据库 Adapter。
|
||||
- `easy-agents-scheduler`:业务无关的调度 API、Quartz Provider 与独立 Spring Boot Starter。
|
||||
- `easy-agents-support`:Flow 与 Easy-Agents 适配模块。
|
||||
- `easy-agents-spring-boot-starter`:Spring Boot 自动配置支持。
|
||||
- `easy-agents-samples`:示例工程。
|
||||
@@ -39,11 +44,17 @@ Easy-Agents 是一个轻量、可扩展的 Java AI 应用开发框架,覆盖
|
||||
在项目根目录执行:
|
||||
|
||||
```bash
|
||||
mvn -DskipTests -Dmaven.javadoc.skip=true -Dgpg.skip=true clean install
|
||||
mvn -DskipTests clean install
|
||||
```
|
||||
|
||||
构建完成后,相关构件会安装到本地 Maven 仓库,可供 `easyflow` 等项目直接依赖。
|
||||
|
||||
发布时启用 `release` profile,生成源码包和 Javadoc 包,并调用 Maven Central 发布插件:
|
||||
|
||||
```bash
|
||||
mvn -Prelease -DskipTests deploy
|
||||
```
|
||||
|
||||
## 快速示例
|
||||
|
||||
```java
|
||||
@@ -69,7 +80,9 @@ public static void main(String[] args) {
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-bom</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<version>1.2.0-RC</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
@@ -85,3 +98,37 @@ public static void main(String[] args) {
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
## 嵌入式分布式调度
|
||||
|
||||
Spring Boot 项目可直接引入独立 Starter:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.easyagents</groupId>
|
||||
<artifactId>easy-agents-scheduler-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
引用方需要先从 `easy-agents-scheduler-quartz` 构件的 `quartz-schema/` 目录选择 MySQL、PostgreSQL 或 H2 脚本,并纳入自己的 Flyway、Liquibase 或初始化流程。Starter 不会自动创建、删除或修改 Quartz 表。
|
||||
|
||||
最小配置:
|
||||
|
||||
```yaml
|
||||
easy-agents:
|
||||
scheduler:
|
||||
enabled: true
|
||||
# 多 DataSource 时必须指定 Bean 名称
|
||||
data-source-bean-name: dataSource
|
||||
quartz:
|
||||
scheduler-name: easyAgentsScheduler
|
||||
instance-id: AUTO
|
||||
clustered: true
|
||||
table-prefix: QRTZ_
|
||||
thread-count: 8
|
||||
shutdown-wait-timeout-millis: 30000
|
||||
```
|
||||
|
||||
业务方将 `ScheduleHandler` 注册为 Spring Bean,并通过 `ScheduleService` 创建 Cron 或一次性任务。调度触发采用至少一次语义,Handler 需要使用 `scheduleId + scheduledFireTime` 或立即触发的 `invocationId` 实现业务幂等。应用关闭超过等待上限后会向 Handler 线程发送协作式中断;长耗时 Handler 必须正确响应线程中断,忽略中断的业务代码仍可能继续占用 Quartz Worker。完整建表说明见 `easy-agents-scheduler/easy-agents-scheduler-quartz/SCHEMA.md`。
|
||||
|
||||
当前 Provider 固定使用 Quartz `2.5.2`,`easy-agents-bom` 已同步管理该传递依赖。若业务项目还引入了其他 BOM 或显式 Quartz 版本,接入时应执行 `mvn dependency:tree -Dincludes=org.quartz-scheduler:quartz`,确认最终解析版本仍为 `2.5.2`。
|
||||
|
||||
@@ -23,6 +23,16 @@
|
||||
<artifactId>agentscope</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.anthropic</groupId>
|
||||
<artifactId>anthropic-java</artifactId>
|
||||
|
||||
@@ -2,11 +2,13 @@ package com.easyagents.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec;
|
||||
import com.easyagents.agent.runtime.memory.AgentMemoryPolicy;
|
||||
import com.easyagents.agent.runtime.mcp.McpSpec;
|
||||
import com.easyagents.agent.runtime.model.AgentGenerationOptions;
|
||||
import com.easyagents.agent.runtime.model.AgentModelSpec;
|
||||
import com.easyagents.agent.runtime.persistence.AgentPersistencePolicy;
|
||||
import com.easyagents.agent.runtime.skill.AgentSkillBoxSpec;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||
import com.easyagents.agent.runtime.tool.operate.AgentOperateToolSpec;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -27,6 +29,8 @@ public class AgentDefinition {
|
||||
private AgentGenerationOptions generationOptions = new AgentGenerationOptions();
|
||||
private AgentExecutionOptions executionOptions = new AgentExecutionOptions();
|
||||
private List<AgentToolSpec> toolSpecs = new ArrayList<>();
|
||||
private List<McpSpec> mcpSpecs = new ArrayList<>();
|
||||
private List<AgentOperateToolSpec> operateToolSpecs = new ArrayList<>();
|
||||
private List<AgentKnowledgeSpec> knowledgeSpecs = new ArrayList<>();
|
||||
private AgentMemoryPolicy memoryPolicy = AgentMemoryPolicy.autoContext();
|
||||
private AgentPersistencePolicy persistencePolicy = AgentPersistencePolicy.disabled();
|
||||
@@ -177,6 +181,42 @@ public class AgentDefinition {
|
||||
this.toolSpecs = toolSpecs == null ? new ArrayList<>() : new ArrayList<>(toolSpecs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MCP 声明。
|
||||
*
|
||||
* @return MCP 声明
|
||||
*/
|
||||
public List<McpSpec> getMcpSpecs() {
|
||||
return mcpSpecs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 MCP 声明。
|
||||
*
|
||||
* @param mcpSpecs MCP 声明
|
||||
*/
|
||||
public void setMcpSpecs(List<McpSpec> mcpSpecs) {
|
||||
this.mcpSpecs = mcpSpecs == null ? new ArrayList<>() : new ArrayList<>(mcpSpecs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作类工具定义。
|
||||
*
|
||||
* @return 操作类工具定义
|
||||
*/
|
||||
public List<AgentOperateToolSpec> getOperateToolSpecs() {
|
||||
return operateToolSpecs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置操作类工具定义。
|
||||
*
|
||||
* @param operateToolSpecs 操作类工具定义
|
||||
*/
|
||||
public void setOperateToolSpecs(List<AgentOperateToolSpec> operateToolSpecs) {
|
||||
this.operateToolSpecs = operateToolSpecs == null ? new ArrayList<>() : new ArrayList<>(operateToolSpecs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库定义。
|
||||
*
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
package com.easyagents.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRegistration;
|
||||
import com.easyagents.agent.runtime.media.AgentMediaResolver;
|
||||
import com.easyagents.agent.runtime.memory.AgentMemorySnapshot;
|
||||
import com.easyagents.agent.runtime.persistence.conversation.AgentConversationRecorder;
|
||||
import com.easyagents.agent.runtime.persistence.conversation.noop.NoopAgentConversationRecorder;
|
||||
import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
|
||||
import com.easyagents.agent.runtime.persistence.session.noop.NoopAgentSessionStore;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolInvoker;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 智能体初始化创建请求参数
|
||||
*/
|
||||
public class AgentInitRequest {
|
||||
|
||||
/**
|
||||
* 会话ID,用于从会话存储中加载和保存当前智能体的上下文状态。
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 智能体定义,包含模型、系统提示词、工具、知识库、记忆策略等静态配置。
|
||||
*/
|
||||
private AgentDefinition agentDefinition;
|
||||
|
||||
/**
|
||||
* 会话状态存储实现,可以实现此接口以管理 session
|
||||
*/
|
||||
private AgentSessionStore sessionStore = NoopAgentSessionStore.INSTANCE;
|
||||
|
||||
/**
|
||||
* 运行时上下文,传递租户、用户、链路等调用环境信息。
|
||||
*/
|
||||
private AgentRuntimeContext runtimeContext = new AgentRuntimeContext();
|
||||
|
||||
/**
|
||||
* 工具集合。
|
||||
*/
|
||||
private Map<String, AgentToolInvoker> toolInvokers = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 知识库集合,实现AgentKnowledgeRetriever接口以进行知识检索动作。
|
||||
*/
|
||||
private List<AgentKnowledgeRegistration> knowledgeRegistrations = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 首次构建 Agent 时装载的对话历史快照。
|
||||
*/
|
||||
private AgentMemorySnapshot memorySnapshot = new AgentMemorySnapshot();
|
||||
|
||||
/**
|
||||
* 对话事件记录器,用于记录运行时事件流。
|
||||
*/
|
||||
private AgentConversationRecorder conversationRecorder = NoopAgentConversationRecorder.INSTANCE;
|
||||
|
||||
/**
|
||||
* 初始化元数据,用于传递业务侧扩展信息。
|
||||
*/
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 媒体引用解析器,仅在模型调用前解析稳定引用。
|
||||
*/
|
||||
private AgentMediaResolver mediaResolver;
|
||||
|
||||
/**
|
||||
* 获取会话ID。
|
||||
*
|
||||
* @return 会话ID
|
||||
*/
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置会话ID。
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
*/
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取智能体定义。
|
||||
*
|
||||
* @return 智能体定义
|
||||
*/
|
||||
public AgentDefinition getAgentDefinition() {
|
||||
return agentDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置智能体定义。
|
||||
*
|
||||
* @param agentDefinition 智能体定义
|
||||
*/
|
||||
public void setAgentDefinition(AgentDefinition agentDefinition) {
|
||||
this.agentDefinition = agentDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话存储。
|
||||
*
|
||||
* @return 会话存储
|
||||
*/
|
||||
public AgentSessionStore getSessionStore() {
|
||||
return sessionStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置会话存储。
|
||||
*
|
||||
* @param sessionStore 会话存储
|
||||
*/
|
||||
public void setSessionStore(AgentSessionStore sessionStore) {
|
||||
this.sessionStore = sessionStore == null ? NoopAgentSessionStore.INSTANCE : sessionStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行时上下文。
|
||||
*
|
||||
* @return 运行时上下文
|
||||
*/
|
||||
public AgentRuntimeContext getRuntimeContext() {
|
||||
return runtimeContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置运行时上下文。
|
||||
*
|
||||
* @param runtimeContext 运行时上下文
|
||||
*/
|
||||
public void setRuntimeContext(AgentRuntimeContext runtimeContext) {
|
||||
this.runtimeContext = runtimeContext == null ? new AgentRuntimeContext() : runtimeContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具调用器。
|
||||
*
|
||||
* @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 List<AgentKnowledgeRegistration> getKnowledgeRegistrations() {
|
||||
return knowledgeRegistrations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置知识库检索器。
|
||||
*
|
||||
* @param knowledgeRegistrations 知识库运行时绑定
|
||||
*/
|
||||
public void setKnowledgeRegistrations(List<AgentKnowledgeRegistration> knowledgeRegistrations) {
|
||||
this.knowledgeRegistrations = knowledgeRegistrations == null
|
||||
? new ArrayList<>()
|
||||
: new ArrayList<>(knowledgeRegistrations);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取首次构建 Agent 时的对话历史快照。
|
||||
*
|
||||
* @return 对话历史快照
|
||||
*/
|
||||
public AgentMemorySnapshot getMemorySnapshot() {
|
||||
return memorySnapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置首次构建 Agent 时的对话历史快照。
|
||||
*
|
||||
* @param memorySnapshot 对话历史快照
|
||||
*/
|
||||
public void setMemorySnapshot(AgentMemorySnapshot memorySnapshot) {
|
||||
this.memorySnapshot = memorySnapshot == null ? new AgentMemorySnapshot() : memorySnapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话记录器。
|
||||
*
|
||||
* @return 会话记录器
|
||||
*/
|
||||
public AgentConversationRecorder getConversationRecorder() {
|
||||
return conversationRecorder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置会话记录器。
|
||||
*
|
||||
* @param conversationRecorder 会话记录器
|
||||
*/
|
||||
public void setConversationRecorder(AgentConversationRecorder conversationRecorder) {
|
||||
this.conversationRecorder = conversationRecorder == null
|
||||
? NoopAgentConversationRecorder.INSTANCE
|
||||
: conversationRecorder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
*
|
||||
* @return 元数据
|
||||
*/
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置元数据。
|
||||
*
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取媒体引用解析器。
|
||||
*
|
||||
* @return 媒体引用解析器
|
||||
*/
|
||||
public AgentMediaResolver getMediaResolver() {
|
||||
return mediaResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置媒体引用解析器。
|
||||
*
|
||||
* @param mediaResolver 媒体引用解析器
|
||||
*/
|
||||
public void setMediaResolver(AgentMediaResolver mediaResolver) {
|
||||
this.mediaResolver = mediaResolver;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.easyagents.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.hitl.AgentResumeToken;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 智能体挂起运行恢复请求。
|
||||
*/
|
||||
public class AgentResumeRequest {
|
||||
|
||||
/**
|
||||
* 挂起运行的恢复令牌。
|
||||
*/
|
||||
private AgentResumeToken resumeToken;
|
||||
|
||||
/**
|
||||
* 是否批准继续执行。
|
||||
*/
|
||||
private boolean approved;
|
||||
|
||||
/**
|
||||
* 拒绝继续执行时的原因。
|
||||
*/
|
||||
private String rejectReason;
|
||||
|
||||
/**
|
||||
* 调用方传入的恢复元数据。
|
||||
*/
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 恢复请求是否已由调用方的持久化 pending store 完成校验和一次性消费。
|
||||
*
|
||||
* <p>该字段仅供服务端集成层使用。普通调用方不应设置该标记;设置后 runtime 会跳过
|
||||
* 当前进程内 {@code AgentToolApprovalCoordinator} 的 token 存在性校验,用于服务重启或跨节点后
|
||||
* 从 AgentScope session 中继续 pending tool。批准请求必须在 metadata 中提供
|
||||
* {@code toolCallId/toolName/toolInput},多个调用使用 {@code approvedToolCalls} 列表,
|
||||
* 以便 runtime 将持久化审批结果绑定到实际工具调用。</p>
|
||||
*/
|
||||
private boolean trusted;
|
||||
|
||||
/**
|
||||
* 获取恢复令牌。
|
||||
*
|
||||
* @return 恢复令牌
|
||||
*/
|
||||
public AgentResumeToken getResumeToken() {
|
||||
return resumeToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置恢复令牌。
|
||||
*
|
||||
* @param resumeToken 恢复令牌
|
||||
*/
|
||||
public void setResumeToken(AgentResumeToken resumeToken) {
|
||||
this.resumeToken = resumeToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回是否批准继续执行。
|
||||
*
|
||||
* @return 批准时为 true
|
||||
*/
|
||||
public boolean isApproved() {
|
||||
return approved;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否批准继续执行。
|
||||
*
|
||||
* @param approved 批准标记
|
||||
*/
|
||||
public void setApproved(boolean approved) {
|
||||
this.approved = approved;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取拒绝原因。
|
||||
*
|
||||
* @return 拒绝原因
|
||||
*/
|
||||
public String getRejectReason() {
|
||||
return rejectReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置拒绝原因。
|
||||
*
|
||||
* @param rejectReason 拒绝原因
|
||||
*/
|
||||
public void setRejectReason(String rejectReason) {
|
||||
this.rejectReason = rejectReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取恢复元数据。
|
||||
*
|
||||
* @return 恢复元数据
|
||||
*/
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置恢复元数据。
|
||||
*
|
||||
* @param metadata 恢复元数据
|
||||
*/
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回恢复请求是否已由调用方持久化层校验。
|
||||
*
|
||||
* @return 已校验时为 true
|
||||
*/
|
||||
public boolean isTrusted() {
|
||||
return trusted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置恢复请求是否已由调用方持久化层校验。
|
||||
*
|
||||
* @param trusted 已校验标记
|
||||
*/
|
||||
public void setTrusted(boolean trusted) {
|
||||
this.trusted = trusted;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.easyagents.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* 可取消的单次智能体运行句柄。
|
||||
*/
|
||||
public interface AgentRunHandle {
|
||||
|
||||
/**
|
||||
* 获取本次运行的事件流。
|
||||
*
|
||||
* @return 事件流
|
||||
*/
|
||||
Flux<AgentRuntimeEvent> stream();
|
||||
|
||||
/**
|
||||
* 取消本次运行。
|
||||
*/
|
||||
void cancel();
|
||||
|
||||
/**
|
||||
* 提交工具审批结果。
|
||||
*
|
||||
* @param response 审批响应
|
||||
*/
|
||||
void submitToolApproval(AgentToolApprovalResponse response);
|
||||
}
|
||||
@@ -1,26 +1,40 @@
|
||||
package com.easyagents.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.message.AgentMessage;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* 执行声明式 ReAct 智能体并流式输出运行事件。
|
||||
* 有状态智能体运行器。
|
||||
*/
|
||||
public interface AgentRuntime {
|
||||
|
||||
/**
|
||||
* 启动单次智能体运行并返回可取消句柄。
|
||||
* 初始化智能体运行器。
|
||||
*
|
||||
* @param request 包含定义、输入、记忆和适配器的运行请求
|
||||
* @return 本次运行句柄
|
||||
* @param request 初始化请求
|
||||
*/
|
||||
AgentRunHandle start(AgentRunRequest request);
|
||||
void init(AgentInitRequest request);
|
||||
|
||||
/**
|
||||
* 启动单次智能体运行。
|
||||
* 发送用户消息并流式输出运行事件。
|
||||
*
|
||||
* @param request 包含定义、输入、记忆和适配器的运行请求
|
||||
* @return 本次运行的事件流
|
||||
* @param userMessage 用户消息
|
||||
* @return 运行事件流
|
||||
*/
|
||||
Flux<AgentRuntimeEvent> stream(AgentRunRequest request);
|
||||
Flux<AgentRuntimeEvent> stream(AgentMessage userMessage);
|
||||
|
||||
/**
|
||||
* 恢复一次已挂起的运行。
|
||||
*
|
||||
* @param request 恢复请求
|
||||
* @return 运行事件流
|
||||
*/
|
||||
Flux<AgentRuntimeEvent> resume(AgentResumeRequest request);
|
||||
|
||||
/**
|
||||
* 关闭运行器并释放底层资源。
|
||||
*/
|
||||
default void close() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,87 @@
|
||||
package com.easyagents.agent.runtime;
|
||||
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetriever;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRegistration;
|
||||
import com.easyagents.agent.runtime.memory.AgentMemorySnapshot;
|
||||
import com.easyagents.agent.runtime.message.AgentMessage;
|
||||
import com.easyagents.agent.runtime.persistence.AgentConversationRecorder;
|
||||
import com.easyagents.agent.runtime.persistence.AgentSessionStore;
|
||||
import com.easyagents.agent.runtime.persistence.noop.NoopAgentConversationRecorder;
|
||||
import com.easyagents.agent.runtime.persistence.noop.NoopAgentSessionStore;
|
||||
import com.easyagents.agent.runtime.persistence.conversation.AgentConversationRecorder;
|
||||
import com.easyagents.agent.runtime.persistence.conversation.noop.NoopAgentConversationRecorder;
|
||||
import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
|
||||
import com.easyagents.agent.runtime.persistence.session.noop.NoopAgentSessionStore;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolInvoker;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 一次智能体运行请求。
|
||||
* 单轮智能体运行的内部执行上下文。
|
||||
*/
|
||||
public class AgentRunRequest {
|
||||
public class AgentRuntimeExecutionContext {
|
||||
|
||||
/**
|
||||
* 本轮运行请求ID。
|
||||
*/
|
||||
private String requestId;
|
||||
|
||||
/**
|
||||
* 本轮运行链路ID。
|
||||
*/
|
||||
private String traceId;
|
||||
|
||||
/**
|
||||
* 当前会话ID。
|
||||
*/
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 智能体定义。
|
||||
*/
|
||||
private AgentDefinition agentDefinition;
|
||||
|
||||
/**
|
||||
* 业务运行时上下文。
|
||||
*/
|
||||
private AgentRuntimeContext runtimeContext = new AgentRuntimeContext();
|
||||
|
||||
/**
|
||||
* 本轮用户消息。
|
||||
*/
|
||||
private AgentMessage userMessage;
|
||||
|
||||
/**
|
||||
* 本轮初始记忆快照。
|
||||
*/
|
||||
private AgentMemorySnapshot memorySnapshot = new AgentMemorySnapshot();
|
||||
|
||||
/**
|
||||
* 按工具名称索引的工具调用器。
|
||||
*/
|
||||
private Map<String, AgentToolInvoker> toolInvokers = new LinkedHashMap<>();
|
||||
private Map<String, AgentKnowledgeRetriever> knowledgeRetrievers = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 按知识库ID索引的检索器。
|
||||
*/
|
||||
private List<AgentKnowledgeRegistration> knowledgeRegistrations = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 会话状态存储。
|
||||
*/
|
||||
private AgentSessionStore sessionStore = NoopAgentSessionStore.INSTANCE;
|
||||
|
||||
/**
|
||||
* 对话事件记录器。
|
||||
*/
|
||||
private AgentConversationRecorder conversationRecorder = NoopAgentConversationRecorder.INSTANCE;
|
||||
|
||||
/**
|
||||
* 运行元数据。
|
||||
*/
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 运行取消原因。
|
||||
*/
|
||||
private String cancelReason;
|
||||
|
||||
/**
|
||||
@@ -158,7 +211,7 @@ public class AgentRunRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取tool invokers by tool name。
|
||||
* 获取工具调用器。
|
||||
*
|
||||
* @return 工具调用器
|
||||
*/
|
||||
@@ -176,21 +229,23 @@ public class AgentRunRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取knowledge retrievers by knowledge id。
|
||||
* 获取知识库检索器。
|
||||
*
|
||||
* @return 知识库检索器
|
||||
*/
|
||||
public Map<String, AgentKnowledgeRetriever> getKnowledgeRetrievers() {
|
||||
return knowledgeRetrievers;
|
||||
public List<AgentKnowledgeRegistration> getKnowledgeRegistrations() {
|
||||
return knowledgeRegistrations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置知识库检索器。
|
||||
*
|
||||
* @param knowledgeRetrievers 知识库检索器
|
||||
* @param knowledgeRegistrations 知识库运行时绑定
|
||||
*/
|
||||
public void setKnowledgeRetrievers(Map<String, AgentKnowledgeRetriever> knowledgeRetrievers) {
|
||||
this.knowledgeRetrievers = knowledgeRetrievers == null ? new LinkedHashMap<>() : knowledgeRetrievers;
|
||||
public void setKnowledgeRegistrations(List<AgentKnowledgeRegistration> knowledgeRegistrations) {
|
||||
this.knowledgeRegistrations = knowledgeRegistrations == null
|
||||
? new ArrayList<>()
|
||||
: new ArrayList<>(knowledgeRegistrations);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,36 +267,38 @@ public class AgentRunRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话记录器。
|
||||
* 获取对话记录器。
|
||||
*
|
||||
* @return 会话记录器
|
||||
* @return 对话记录器
|
||||
*/
|
||||
public AgentConversationRecorder getConversationRecorder() {
|
||||
return conversationRecorder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置会话记录器。
|
||||
* 设置对话记录器。
|
||||
*
|
||||
* @param conversationRecorder 会话记录器
|
||||
* @param conversationRecorder 对话记录器
|
||||
*/
|
||||
public void setConversationRecorder(AgentConversationRecorder conversationRecorder) {
|
||||
this.conversationRecorder = conversationRecorder == null ? NoopAgentConversationRecorder.INSTANCE : conversationRecorder;
|
||||
this.conversationRecorder = conversationRecorder == null
|
||||
? NoopAgentConversationRecorder.INSTANCE
|
||||
: conversationRecorder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
* 获取运行元数据。
|
||||
*
|
||||
* @return 元数据
|
||||
* @return 运行元数据
|
||||
*/
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置元数据。
|
||||
* 设置运行元数据。
|
||||
*
|
||||
* @param metadata 元数据
|
||||
* @param metadata 运行元数据
|
||||
*/
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.easyagents.agent.runtime.agentscope;
|
||||
|
||||
import io.agentscope.core.formatter.openai.DeepSeekFormatter;
|
||||
import io.agentscope.core.formatter.openai.dto.OpenAIMessage;
|
||||
import io.agentscope.core.message.Msg;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 保留 DeepSeek 专用规则并将全部消息 content 规范为内容块数组。
|
||||
*/
|
||||
public final class AgentDeepSeekChatFormatter extends DeepSeekFormatter {
|
||||
|
||||
/**
|
||||
* 转换 DeepSeek 消息并在供应商规则之后统一 content 格式。
|
||||
*
|
||||
* @param messages AgentScope 消息
|
||||
* @return OpenAI 请求消息
|
||||
*/
|
||||
@Override
|
||||
protected List<OpenAIMessage> doFormat(List<Msg> messages) {
|
||||
return AgentOpenAIChatFormatter.normalizeContent(super.doFormat(messages));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.easyagents.agent.runtime.agentscope;
|
||||
|
||||
import io.agentscope.core.formatter.openai.GLMFormatter;
|
||||
import io.agentscope.core.formatter.openai.dto.OpenAIMessage;
|
||||
import io.agentscope.core.message.Msg;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 保留 GLM 专用规则并将全部消息 content 规范为内容块数组。
|
||||
*/
|
||||
public final class AgentGLMChatFormatter extends GLMFormatter {
|
||||
|
||||
/**
|
||||
* 转换 GLM 消息并在供应商规则之后统一 content 格式。
|
||||
*
|
||||
* @param messages AgentScope 消息
|
||||
* @return OpenAI 请求消息
|
||||
*/
|
||||
@Override
|
||||
protected List<OpenAIMessage> doFormat(List<Msg> messages) {
|
||||
return AgentOpenAIChatFormatter.normalizeContent(super.doFormat(messages));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.easyagents.agent.runtime.agentscope;
|
||||
|
||||
import com.easyagents.agent.runtime.model.AgentHttpVersionPolicy;
|
||||
import io.agentscope.core.model.transport.HttpTransport;
|
||||
import io.agentscope.core.model.transport.HttpTransportConfig;
|
||||
import io.agentscope.core.model.transport.HttpTransportFactory;
|
||||
import io.agentscope.core.model.transport.HttpVersion;
|
||||
import io.agentscope.core.model.transport.JdkHttpTransport;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 按 HTTP 版本策略提供进程级共享的 AgentScope Transport。
|
||||
*/
|
||||
public final class AgentHttpTransportProvider {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AgentHttpTransportProvider.class);
|
||||
private static final AgentHttpTransportProvider SHARED = new AgentHttpTransportProvider();
|
||||
|
||||
private final Map<AgentHttpVersionPolicy, HttpTransport> transports = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 创建 Transport 提供器。
|
||||
*/
|
||||
private AgentHttpTransportProvider() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取进程级共享提供器。
|
||||
*
|
||||
* @return 共享提供器
|
||||
*/
|
||||
public static AgentHttpTransportProvider shared() {
|
||||
return SHARED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定策略与基础 URL 对应的共享 Transport。
|
||||
*
|
||||
* @param policy HTTP 版本策略
|
||||
* @param baseUrl 最终生效的模型基础 URL
|
||||
* @return 共享 Transport
|
||||
*/
|
||||
public HttpTransport getTransport(AgentHttpVersionPolicy policy, String baseUrl) {
|
||||
AgentHttpVersionPolicy effectivePolicy = resolveEffectivePolicy(policy, baseUrl);
|
||||
if (effectivePolicy == AgentHttpVersionPolicy.AUTO) {
|
||||
return HttpTransportFactory.getDefault();
|
||||
}
|
||||
return transports.computeIfAbsent(effectivePolicy, this::createTransport);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析请求实际使用的 HTTP 策略。
|
||||
*
|
||||
* <p>明文 HTTP 固定使用 HTTP/1.1,避免 JDK 客户端发起 h2c Upgrade;HTTPS
|
||||
* 保持 HTTP/2 优先并允许底层通过 ALPN 回退。显式策略始终覆盖 URL 判断。</p>
|
||||
*
|
||||
* @param policy 配置的 HTTP 版本策略
|
||||
* @param baseUrl 最终生效的模型基础 URL
|
||||
* @return 实际生效策略;无法识别 URL 时返回 AUTO
|
||||
*/
|
||||
public static AgentHttpVersionPolicy resolveEffectivePolicy(AgentHttpVersionPolicy policy, String baseUrl) {
|
||||
AgentHttpVersionPolicy safePolicy = policy == null ? AgentHttpVersionPolicy.AUTO : policy;
|
||||
if (safePolicy != AgentHttpVersionPolicy.AUTO) {
|
||||
return safePolicy;
|
||||
}
|
||||
if (baseUrl == null || baseUrl.isBlank()) {
|
||||
LOG.warn("Agent HTTP AUTO policy cannot infer protocol because base URL is missing; fallback to default transport");
|
||||
return AgentHttpVersionPolicy.AUTO;
|
||||
}
|
||||
try {
|
||||
String scheme = URI.create(baseUrl.trim()).getScheme();
|
||||
if (scheme == null) {
|
||||
return AgentHttpVersionPolicy.AUTO;
|
||||
}
|
||||
String normalizedScheme = scheme.toLowerCase(Locale.ROOT);
|
||||
if ("http".equals(normalizedScheme)) {
|
||||
return AgentHttpVersionPolicy.HTTP_1_1;
|
||||
}
|
||||
if ("https".equals(normalizedScheme)) {
|
||||
return AgentHttpVersionPolicy.HTTP_2_PREFERRED;
|
||||
}
|
||||
LOG.warn("Agent HTTP AUTO policy does not support URL scheme '{}'; fallback to default transport",
|
||||
normalizedScheme);
|
||||
return AgentHttpVersionPolicy.AUTO;
|
||||
} catch (IllegalArgumentException exception) {
|
||||
LOG.warn("Agent HTTP AUTO policy cannot parse base URL; fallback to default transport");
|
||||
return AgentHttpVersionPolicy.AUTO;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并注册受 AgentScope 生命周期管理的 JDK Transport。
|
||||
*
|
||||
* @param policy HTTP 版本策略
|
||||
* @return 新 Transport
|
||||
*/
|
||||
private HttpTransport createTransport(AgentHttpVersionPolicy policy) {
|
||||
HttpVersion httpVersion = resolveHttpVersion(policy);
|
||||
HttpTransportConfig config = HttpTransportConfig.builder()
|
||||
.httpVersion(httpVersion)
|
||||
.build();
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.version(httpVersion.toJdkHttpVersion())
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.connectTimeout(config.getConnectTimeout())
|
||||
.build();
|
||||
HttpTransport transport = new JdkHttpTransport(httpClient, config);
|
||||
// 注册后由 AgentScope JVM shutdown hook 统一关闭,避免每次 Agent 运行创建连接池。
|
||||
HttpTransportFactory.register(transport);
|
||||
return transport;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将中立策略映射为 AgentScope HTTP 版本。
|
||||
*
|
||||
* @param policy HTTP 版本策略
|
||||
* @return AgentScope HTTP 版本
|
||||
*/
|
||||
static HttpVersion resolveHttpVersion(AgentHttpVersionPolicy policy) {
|
||||
if (policy == AgentHttpVersionPolicy.HTTP_1_1) {
|
||||
return HttpVersion.HTTP_1_1;
|
||||
}
|
||||
return HttpVersion.HTTP_2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.easyagents.agent.runtime.agentscope;
|
||||
|
||||
import io.agentscope.core.formatter.openai.OpenAIChatFormatter;
|
||||
import io.agentscope.core.formatter.openai.dto.OpenAIContentPart;
|
||||
import io.agentscope.core.formatter.openai.dto.OpenAIMessage;
|
||||
import io.agentscope.core.message.Msg;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 为 OpenAI-compatible 模型补充全部消息 content 内容块数组兼容能力。
|
||||
*/
|
||||
public final class AgentOpenAIChatFormatter extends OpenAIChatFormatter {
|
||||
|
||||
/**
|
||||
* 将 AgentScope 消息转换为 OpenAI 消息,并规范全部角色的 content 格式。
|
||||
*
|
||||
* @param messages AgentScope 消息
|
||||
* @return OpenAI 请求消息
|
||||
*/
|
||||
@Override
|
||||
protected List<OpenAIMessage> doFormat(List<Msg> messages) {
|
||||
return normalizeContent(super.doFormat(messages));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 OpenAI 消息中的 content 统一规范为内容块数组。
|
||||
*
|
||||
* @param formattedMessages 已完成供应商规则转换的 OpenAI 消息
|
||||
* @return content 已规范为数组的原消息列表
|
||||
*/
|
||||
static List<OpenAIMessage> normalizeContent(List<OpenAIMessage> formattedMessages) {
|
||||
for (OpenAIMessage message : formattedMessages) {
|
||||
Object content = message.getContent();
|
||||
if (content instanceof String text) {
|
||||
message.setContent(List.of(OpenAIContentPart.text(text)));
|
||||
} else if (content == null) {
|
||||
message.setContent(List.of(OpenAIContentPart.text("")));
|
||||
} else if (!(content instanceof List<?>)) {
|
||||
throw new IllegalStateException(
|
||||
"Unsupported OpenAI message content type: " + content.getClass().getName());
|
||||
}
|
||||
}
|
||||
return formattedMessages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.easyagents.agent.runtime.agentscope;
|
||||
|
||||
import io.agentscope.core.message.*;
|
||||
import io.agentscope.core.model.ChatResponse;
|
||||
import io.agentscope.core.model.GenerateOptions;
|
||||
import io.agentscope.core.model.Model;
|
||||
import io.agentscope.core.model.ToolSchema;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 面向 AutoContext 摘要调用的模型包装器。
|
||||
*/
|
||||
class AgentScopeAutoContextCompressionModel implements Model {
|
||||
|
||||
private final Model delegate;
|
||||
|
||||
/**
|
||||
* 创建 AutoContext 摘要模型包装器。
|
||||
*
|
||||
* @param delegate 实际调用的模型
|
||||
*/
|
||||
AgentScopeAutoContextCompressionModel(Model delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对 AutoContext 压缩摘要输入做协议安全化后转发给实际模型。
|
||||
*
|
||||
* @param messages AgentScope 消息
|
||||
* @param tools 工具声明
|
||||
* @param options 生成参数
|
||||
* @return 模型响应流
|
||||
*/
|
||||
@Override
|
||||
public Flux<ChatResponse> stream(List<Msg> messages, List<ToolSchema> tools, GenerateOptions options) {
|
||||
return delegate.stream(sanitizeCompressionMessages(messages), tools, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型名称。
|
||||
*
|
||||
* @return 模型名称
|
||||
*/
|
||||
@Override
|
||||
public String getModelName() {
|
||||
return delegate.getModelName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 AutoContext 摘要材料中的工具协议消息转换为普通文本消息。
|
||||
*
|
||||
* @param messages 原始摘要输入
|
||||
* @return 协议安全的摘要输入
|
||||
*/
|
||||
List<Msg> sanitizeCompressionMessages(List<Msg> messages) {
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
return messages;
|
||||
}
|
||||
List<Msg> sanitized = new ArrayList<>(messages.size());
|
||||
for (Msg message : messages) {
|
||||
if (message == null) {
|
||||
continue;
|
||||
}
|
||||
if (requiresPlainText(message)) {
|
||||
sanitized.add(toPlainTextUserMessage(message));
|
||||
} else {
|
||||
sanitized.add(message);
|
||||
}
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private boolean requiresPlainText(Msg message) {
|
||||
return message.getRole() == MsgRole.TOOL
|
||||
|| message.hasContentBlocks(ToolUseBlock.class)
|
||||
|| message.hasContentBlocks(ToolResultBlock.class);
|
||||
}
|
||||
|
||||
private Msg toPlainTextUserMessage(Msg message) {
|
||||
return Msg.builder()
|
||||
.id(message.getId())
|
||||
.role(MsgRole.USER)
|
||||
.name("context")
|
||||
.content(TextBlock.builder().text(renderMessage(message)).build())
|
||||
.metadata(message.getMetadata())
|
||||
.timestamp(message.getTimestamp())
|
||||
.build();
|
||||
}
|
||||
|
||||
private String renderMessage(Msg message) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("Context message role=").append(message.getRole());
|
||||
if (message.getName() != null && !message.getName().isBlank()) {
|
||||
builder.append(", name=").append(message.getName());
|
||||
}
|
||||
builder.append('\n');
|
||||
for (ContentBlock block : message.getContent()) {
|
||||
appendBlock(builder, block);
|
||||
}
|
||||
return builder.toString().trim();
|
||||
}
|
||||
|
||||
private void appendBlock(StringBuilder builder, ContentBlock block) {
|
||||
if (block instanceof TextBlock textBlock) {
|
||||
appendSection(builder, "text", textBlock.getText());
|
||||
return;
|
||||
}
|
||||
if (block instanceof ThinkingBlock thinkingBlock) {
|
||||
appendSection(builder, "thinking", thinkingBlock.getThinking());
|
||||
return;
|
||||
}
|
||||
if (block instanceof ToolUseBlock toolUseBlock) {
|
||||
appendSection(builder, "tool_use.id", toolUseBlock.getId());
|
||||
appendSection(builder, "tool_use.name", toolUseBlock.getName());
|
||||
appendSection(builder, "tool_use.input", renderMap(toolUseBlock.getInput()));
|
||||
return;
|
||||
}
|
||||
if (block instanceof ToolResultBlock toolResultBlock) {
|
||||
appendSection(builder, "tool_result.id", toolResultBlock.getId());
|
||||
appendSection(builder, "tool_result.name", toolResultBlock.getName());
|
||||
appendSection(builder, "tool_result.output", renderBlocks(toolResultBlock.getOutput()));
|
||||
return;
|
||||
}
|
||||
appendSection(builder, "content", String.valueOf(block));
|
||||
}
|
||||
|
||||
private String renderBlocks(List<ContentBlock> blocks) {
|
||||
if (blocks == null || blocks.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (ContentBlock block : blocks) {
|
||||
if (block instanceof TextBlock textBlock) {
|
||||
builder.append(textBlock.getText());
|
||||
} else {
|
||||
builder.append(String.valueOf(block));
|
||||
}
|
||||
builder.append('\n');
|
||||
}
|
||||
return builder.toString().trim();
|
||||
}
|
||||
|
||||
private String renderMap(Map<String, Object> map) {
|
||||
return map == null ? "{}" : map.toString();
|
||||
}
|
||||
|
||||
private void appendSection(StringBuilder builder, String label, String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return;
|
||||
}
|
||||
builder.append(label).append(": ").append(value).append('\n');
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.easyagents.agent.runtime.agentscope;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRunRequest;
|
||||
import io.agentscope.core.hook.Hook;
|
||||
import io.agentscope.core.hook.HookEvent;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 从 AgentScope Hook 回调发射不在主事件流中的运行时事件。
|
||||
*/
|
||||
public class AgentScopeEventHook implements Hook {
|
||||
|
||||
private final AgentRunRequest request;
|
||||
|
||||
/**
|
||||
* 创建 Hook。
|
||||
*
|
||||
* @param request 运行请求
|
||||
*/
|
||||
public AgentScopeEventHook(AgentRunRequest request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends HookEvent> Mono<T> onEvent(T event) {
|
||||
return Mono.just(event);
|
||||
}
|
||||
}
|
||||
@@ -1,240 +1,453 @@
|
||||
package com.easyagents.agent.runtime.agentscope;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRunRequest;
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||
import com.easyagents.agent.runtime.knowledge.*;
|
||||
import io.agentscope.core.message.TextBlock;
|
||||
import io.agentscope.core.rag.Knowledge;
|
||||
import io.agentscope.core.rag.model.Document;
|
||||
import io.agentscope.core.rag.model.DocumentMetadata;
|
||||
import io.agentscope.core.rag.model.RetrieveConfig;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeTurnContextHolder;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalCoordinator;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeDocument;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRegistration;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetrievalRequest;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetrievalResult;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeToolNames;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolCategory;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolContext;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolResult;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||
import io.agentscope.core.tool.Toolkit;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 将运行时知识库检索器适配为一个聚合 AgentScope Knowledge。
|
||||
* 将中立知识库绑定适配为 AgentScope 一库一工具。
|
||||
*/
|
||||
public class AgentScopeKnowledgeAdapter {
|
||||
|
||||
/**
|
||||
* 创建聚合 Knowledge。
|
||||
* 根据 Agent 知识库声明创建模型可见工具定义。
|
||||
*
|
||||
* @param request 运行请求
|
||||
* @return 聚合 Knowledge;未配置知识库时返回 null
|
||||
* @param context 运行时上下文
|
||||
* @return 知识库工具定义
|
||||
* @throws AgentRuntimeException 声明、运行名或 Retriever 绑定不合法时抛出
|
||||
*/
|
||||
public Knowledge createAggregateKnowledge(AgentRunRequest request) {
|
||||
return createAggregateKnowledge(request, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带事件 sink 的聚合 Knowledge。
|
||||
*
|
||||
* @param request 运行请求
|
||||
* @param eventSink 事件 sink
|
||||
* @return 聚合 Knowledge;未配置知识库时返回 null
|
||||
*/
|
||||
public Knowledge createAggregateKnowledge(AgentRunRequest request, Sinks.Many<AgentRuntimeEvent> eventSink) {
|
||||
if (request.getAgentDefinition().getKnowledgeSpecs().isEmpty()) {
|
||||
return null;
|
||||
public List<AgentToolSpec> createToolSpecs(AgentRuntimeExecutionContext context) {
|
||||
if (context == null || context.getAgentDefinition() == null) {
|
||||
throw new AgentRuntimeException("Agent runtime context and definition are required for knowledge tools.");
|
||||
}
|
||||
return new AggregateKnowledge(request, eventSink);
|
||||
List<AgentKnowledgeSpec> knowledgeSpecs = context.getAgentDefinition().getKnowledgeSpecs();
|
||||
List<AgentKnowledgeRegistration> registrations = context.getKnowledgeRegistrations();
|
||||
if (knowledgeSpecs == null || knowledgeSpecs.isEmpty()) {
|
||||
if (registrations != null && !registrations.isEmpty()) {
|
||||
throw new AgentRuntimeException("Knowledge registrations require matching knowledge specs.");
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
Map<String, AgentKnowledgeRegistration> registrationIndex = registrationIndex(registrations);
|
||||
if (registrationIndex.size() != knowledgeSpecs.size()) {
|
||||
throw new AgentRuntimeException("Knowledge specs and registrations must match one-to-one.");
|
||||
}
|
||||
List<AgentToolSpec> toolSpecs = new ArrayList<>(knowledgeSpecs.size());
|
||||
Set<String> toolNames = new LinkedHashSet<>();
|
||||
for (AgentKnowledgeSpec knowledgeSpec : knowledgeSpecs) {
|
||||
validateKnowledgeSpec(knowledgeSpec);
|
||||
AgentKnowledgeRegistration registration = registrationIndex.get(knowledgeSpec.getKnowledgeId());
|
||||
if (registration == null) {
|
||||
throw new AgentRuntimeException(
|
||||
"Knowledge retriever is required: " + knowledgeSpec.getKnowledgeId());
|
||||
}
|
||||
String toolName = AgentKnowledgeToolNames.build(knowledgeSpec.getRuntimeName());
|
||||
if (!toolNames.add(toolName)) {
|
||||
throw new AgentRuntimeException("Duplicate knowledge tool name: " + toolName);
|
||||
}
|
||||
toolSpecs.add(toolSpec(knowledgeSpec, toolName));
|
||||
}
|
||||
return toolSpecs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运行时文档转换为 AgentScope 文档。
|
||||
* 将知识库工具注册到现有 AgentScope Toolkit。
|
||||
*
|
||||
* @param documents 运行时文档
|
||||
* @return AgentScope 文档
|
||||
* @param context 运行时上下文
|
||||
* @param toolSpecs 知识库工具定义
|
||||
* @param toolkit AgentScope Toolkit
|
||||
* @param toolAdapter 中立工具适配器
|
||||
* @param approvalCoordinator 工具审批协调器
|
||||
* @param turnContextHolder 当前运行轮次上下文持有器
|
||||
*/
|
||||
public List<Document> toDocuments(List<AgentKnowledgeDocument> documents) {
|
||||
List<Document> converted = new ArrayList<>();
|
||||
public void registerTools(AgentRuntimeExecutionContext context,
|
||||
List<AgentToolSpec> toolSpecs,
|
||||
Toolkit toolkit,
|
||||
AgentScopeToolAdapter toolAdapter,
|
||||
AgentToolApprovalCoordinator approvalCoordinator,
|
||||
AgentRuntimeTurnContextHolder turnContextHolder) {
|
||||
Objects.requireNonNull(toolkit, "toolkit");
|
||||
Objects.requireNonNull(toolAdapter, "toolAdapter");
|
||||
if (toolSpecs == null || toolSpecs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, AgentKnowledgeRegistration> registrationIndex =
|
||||
registrationIndex(context.getKnowledgeRegistrations());
|
||||
for (AgentToolSpec toolSpec : toolSpecs) {
|
||||
String knowledgeId = stringValue(toolSpec.getMetadata().get("knowledgeId"));
|
||||
AgentKnowledgeRegistration registration = registrationIndex.get(knowledgeId);
|
||||
if (registration == null) {
|
||||
throw new AgentRuntimeException("Knowledge registration is required: " + knowledgeId);
|
||||
}
|
||||
toolkit.registerAgentTool(toolAdapter.adapt(
|
||||
toolSpec,
|
||||
(input, toolContext) -> retrieve(registration, input, toolContext),
|
||||
context,
|
||||
approvalCoordinator,
|
||||
turnContextHolder,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
false));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按知识库 ID 建立运行时绑定索引并拒绝重复绑定。
|
||||
*
|
||||
* @param registrations 知识库运行时绑定
|
||||
* @return 以知识库 ID 为键的绑定索引
|
||||
*/
|
||||
private Map<String, AgentKnowledgeRegistration> registrationIndex(
|
||||
List<AgentKnowledgeRegistration> registrations) {
|
||||
Map<String, AgentKnowledgeRegistration> index = new LinkedHashMap<>();
|
||||
if (registrations == null) {
|
||||
return index;
|
||||
}
|
||||
for (AgentKnowledgeRegistration registration : registrations) {
|
||||
if (registration == null || registration.getKnowledgeSpec() == null) {
|
||||
throw new AgentRuntimeException("Knowledge registration and spec are required.");
|
||||
}
|
||||
String knowledgeId = registration.getKnowledgeSpec().getKnowledgeId();
|
||||
if (knowledgeId == null || knowledgeId.isBlank()) {
|
||||
throw new AgentRuntimeException("Knowledge id is required.");
|
||||
}
|
||||
if (index.putIfAbsent(knowledgeId, registration) != null) {
|
||||
throw new AgentRuntimeException("Duplicate knowledge registration: " + knowledgeId);
|
||||
}
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验知识库声明的标识和英文运行名。
|
||||
*
|
||||
* @param knowledgeSpec 知识库声明
|
||||
* @throws AgentRuntimeException 声明不合法时抛出
|
||||
*/
|
||||
private void validateKnowledgeSpec(AgentKnowledgeSpec knowledgeSpec) {
|
||||
if (knowledgeSpec == null) {
|
||||
throw new AgentRuntimeException("Knowledge spec is required.");
|
||||
}
|
||||
if (knowledgeSpec.getKnowledgeId() == null || knowledgeSpec.getKnowledgeId().isBlank()) {
|
||||
throw new AgentRuntimeException("Knowledge id is required.");
|
||||
}
|
||||
AgentKnowledgeToolNames.build(knowledgeSpec.getRuntimeName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单个知识库对应的模型工具声明。
|
||||
*
|
||||
* @param knowledgeSpec 知识库声明
|
||||
* @param toolName 已规范化的工具名
|
||||
* @return 模型可见工具声明
|
||||
*/
|
||||
private AgentToolSpec toolSpec(AgentKnowledgeSpec knowledgeSpec, String toolName) {
|
||||
AgentToolSpec toolSpec = new AgentToolSpec();
|
||||
toolSpec.setName(toolName);
|
||||
toolSpec.setDescription(toolDescription(knowledgeSpec));
|
||||
toolSpec.setCategory(AgentToolCategory.KNOWLEDGE);
|
||||
toolSpec.setParametersSchema(Map.of(
|
||||
"type", "object",
|
||||
"properties", Map.of(
|
||||
"query", Map.of(
|
||||
"type", "string",
|
||||
"description", "A standalone search query containing all context needed to retrieve relevant knowledge."
|
||||
)
|
||||
),
|
||||
"required", List.of("query"),
|
||||
"additionalProperties", false
|
||||
));
|
||||
toolSpec.getMetadata().putAll(knowledgeSpec.getMetadata());
|
||||
toolSpec.getMetadata().put("knowledgeId", knowledgeSpec.getKnowledgeId());
|
||||
toolSpec.getMetadata().put("knowledgeName", knowledgeSpec.getName());
|
||||
toolSpec.getMetadata().put("knowledgeRuntimeName", knowledgeSpec.getRuntimeName());
|
||||
toolSpec.getMetadata().put("toolDisplayName", knowledgeSpec.getName());
|
||||
return toolSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成包含知识库名称、范围和查询要求的工具描述。
|
||||
*
|
||||
* @param knowledgeSpec 知识库声明
|
||||
* @return 模型可见工具描述
|
||||
*/
|
||||
private String toolDescription(AgentKnowledgeSpec knowledgeSpec) {
|
||||
String knowledgeName = hasText(knowledgeSpec.getName())
|
||||
? knowledgeSpec.getName().trim()
|
||||
: knowledgeSpec.getRuntimeName().trim();
|
||||
StringBuilder description = new StringBuilder()
|
||||
.append("Search the knowledge base \"")
|
||||
.append(knowledgeName)
|
||||
.append("\" for relevant and grounded information.");
|
||||
if (hasText(knowledgeSpec.getDescription())) {
|
||||
description.append(" Knowledge scope: ")
|
||||
.append(knowledgeSpec.getDescription().trim())
|
||||
.append('.');
|
||||
}
|
||||
description.append(" Build a standalone query from the current question and necessary conversation context.");
|
||||
return description.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单库检索并生成模型证据及旁路检索事件。
|
||||
*
|
||||
* @param registration 知识库运行时绑定
|
||||
* @param input Function Call 输入
|
||||
* @param toolContext 工具执行上下文
|
||||
* @return 模型可见工具结果
|
||||
*/
|
||||
private AgentToolResult retrieve(AgentKnowledgeRegistration registration,
|
||||
Map<String, Object> input,
|
||||
AgentToolContext toolContext) {
|
||||
AgentKnowledgeSpec knowledgeSpec = registration.getKnowledgeSpec();
|
||||
String query = input == null ? null : stringValue(input.get("query"));
|
||||
if (query == null || query.isBlank()) {
|
||||
throw new AgentRuntimeException("Knowledge tool query is required: "
|
||||
+ AgentKnowledgeToolNames.build(knowledgeSpec.getRuntimeName()));
|
||||
}
|
||||
String normalizedQuery = query.trim();
|
||||
AgentKnowledgeRetrievalRequest retrievalRequest = new AgentKnowledgeRetrievalRequest();
|
||||
retrievalRequest.setQuery(normalizedQuery);
|
||||
retrievalRequest.setLimit(knowledgeSpec.getLimit());
|
||||
retrievalRequest.setScoreThreshold(knowledgeSpec.getScoreThreshold());
|
||||
retrievalRequest.setKnowledgeSpec(knowledgeSpec);
|
||||
retrievalRequest.setRuntimeContext(toolContext.getRuntimeContext());
|
||||
retrievalRequest.getMetadata().put("requestId", toolContext.getRequestId());
|
||||
retrievalRequest.getMetadata().put("traceId", toolContext.getTraceId());
|
||||
retrievalRequest.getMetadata().put("sessionId", toolContext.getSessionId());
|
||||
retrievalRequest.getMetadata().put("toolCallId", toolContext.getToolCallId());
|
||||
AgentKnowledgeRetrievalResult retrievalResult = registration.getRetriever().retrieve(retrievalRequest);
|
||||
if (retrievalResult == null) {
|
||||
throw new AgentRuntimeException("Knowledge retriever returned null result: "
|
||||
+ knowledgeSpec.getKnowledgeId());
|
||||
}
|
||||
List<AgentKnowledgeDocument> documents = finalDocuments(knowledgeSpec, retrievalResult.getDocuments());
|
||||
toolContext.emitEvent(retrievalEvent(toolContext, knowledgeSpec, retrievalRequest, documents));
|
||||
|
||||
List<Map<String, Object>> summaries = documentSummaries(documents);
|
||||
AgentToolResult toolResult = AgentToolResult.success(modelContent(knowledgeSpec, normalizedQuery, documents));
|
||||
toolResult.setDisplayContent(summaries);
|
||||
toolResult.getMetadata().putAll(retrievalResult.getMetadata());
|
||||
toolResult.getMetadata().put("knowledgeId", knowledgeSpec.getKnowledgeId());
|
||||
toolResult.getMetadata().put("knowledgeName", knowledgeSpec.getName());
|
||||
toolResult.getMetadata().put("query", normalizedQuery);
|
||||
toolResult.getMetadata().put("documentCount", documents.size());
|
||||
toolResult.getMetadata().put("documents", summaries);
|
||||
return toolResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对检索结果执行统一阈值过滤、降序排序和绑定数量截断。
|
||||
*
|
||||
* @param knowledgeSpec 知识库声明
|
||||
* @param documents 原始检索文档
|
||||
* @return 最终进入模型上下文的文档
|
||||
*/
|
||||
private List<AgentKnowledgeDocument> finalDocuments(AgentKnowledgeSpec knowledgeSpec,
|
||||
List<AgentKnowledgeDocument> documents) {
|
||||
List<AgentKnowledgeDocument> finalDocuments = new ArrayList<>();
|
||||
if (documents == null) {
|
||||
return converted;
|
||||
return finalDocuments;
|
||||
}
|
||||
for (AgentKnowledgeDocument document : documents) {
|
||||
converted.add(toDocument(document));
|
||||
if (document == null || !passesThreshold(document, knowledgeSpec.getScoreThreshold())) {
|
||||
continue;
|
||||
}
|
||||
preserveKnowledgeMetadata(knowledgeSpec, document);
|
||||
finalDocuments.add(document);
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
|
||||
private Document toDocument(AgentKnowledgeDocument document) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("documentId", document.getDocumentId());
|
||||
payload.put("documentName", document.getDocumentName());
|
||||
payload.put("chunkId", document.getChunkId());
|
||||
payload.put("sourceUri", document.getSourceUri());
|
||||
payload.put("knowledgeMetadata", document.getKnowledgeMetadata());
|
||||
payload.put("documentMetadata", document.getMetadata());
|
||||
payload.putAll(document.getMetadata());
|
||||
DocumentMetadata metadata = DocumentMetadata.builder()
|
||||
.content(TextBlock.builder().text(document.getContent()).build())
|
||||
.docId(document.getDocumentId())
|
||||
.chunkId(document.getChunkId())
|
||||
.payload(payload)
|
||||
.build();
|
||||
Document converted = new Document(metadata);
|
||||
converted.setScore(document.getScore());
|
||||
return converted;
|
||||
finalDocuments.sort(Comparator.comparing(
|
||||
AgentKnowledgeDocument::getScore,
|
||||
Comparator.nullsLast(Comparator.reverseOrder())));
|
||||
int limit = Math.max(knowledgeSpec.getLimit(), 1);
|
||||
if (finalDocuments.size() > limit) {
|
||||
return new ArrayList<>(finalDocuments.subList(0, limit));
|
||||
}
|
||||
return finalDocuments;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将检索调用分发到多个知识源的聚合 Knowledge 实现。
|
||||
* 判断文档最终分数是否达到绑定阈值。
|
||||
*
|
||||
* @param document 检索文档
|
||||
* @param scoreThreshold 分数阈值
|
||||
* @return 达到阈值时为 true
|
||||
*/
|
||||
private class AggregateKnowledge implements Knowledge {
|
||||
|
||||
private final AgentRunRequest request;
|
||||
private final Sinks.Many<AgentRuntimeEvent> eventSink;
|
||||
|
||||
private AggregateKnowledge(AgentRunRequest request, Sinks.Many<AgentRuntimeEvent> eventSink) {
|
||||
this.request = request;
|
||||
this.eventSink = eventSink;
|
||||
private boolean passesThreshold(AgentKnowledgeDocument document, double scoreThreshold) {
|
||||
if (scoreThreshold <= 0D) {
|
||||
return true;
|
||||
}
|
||||
return document.getScore() != null && document.getScore() >= scoreThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* 忽略文档新增,因为知识库索引由 EasyFlow 负责。
|
||||
*
|
||||
* @param documents 文档列表
|
||||
* @return 完成信号
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> addDocuments(List<Document> documents) {
|
||||
return Mono.error(new UnsupportedOperationException(
|
||||
"Easy-Agents agent runtime knowledge does not support addDocuments. Use external knowledge service instead."));
|
||||
}
|
||||
/**
|
||||
* 将知识库归属信息合并到文档元数据中。
|
||||
*
|
||||
* @param knowledgeSpec 知识库声明
|
||||
* @param document 检索文档
|
||||
*/
|
||||
private void preserveKnowledgeMetadata(AgentKnowledgeSpec knowledgeSpec,
|
||||
AgentKnowledgeDocument document) {
|
||||
Map<String, Object> knowledgeMetadata = new LinkedHashMap<>(knowledgeSpec.getMetadata());
|
||||
knowledgeMetadata.put("knowledgeId", knowledgeSpec.getKnowledgeId());
|
||||
knowledgeMetadata.put("knowledgeName", knowledgeSpec.getName());
|
||||
knowledgeMetadata.put("knowledgeRuntimeName", knowledgeSpec.getRuntimeName());
|
||||
knowledgeMetadata.putAll(document.getKnowledgeMetadata());
|
||||
document.setKnowledgeMetadata(knowledgeMetadata);
|
||||
document.getMetadata().putIfAbsent("knowledgeId", knowledgeSpec.getKnowledgeId());
|
||||
document.getMetadata().putIfAbsent("knowledgeName", knowledgeSpec.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从已配置的知识源检索文档。
|
||||
*
|
||||
* @param query 查询
|
||||
* @param config 检索配置
|
||||
* @return 文档列表
|
||||
*/
|
||||
@Override
|
||||
public Mono<List<Document>> retrieve(String query, RetrieveConfig config) {
|
||||
return Mono.fromCallable(() -> retrieveAll(query, config));
|
||||
}
|
||||
/**
|
||||
* 创建与模型最终证据一致的知识库检索事件。
|
||||
*
|
||||
* @param toolContext 工具执行上下文
|
||||
* @param knowledgeSpec 知识库声明
|
||||
* @param retrievalRequest 检索请求
|
||||
* @param documents 最终文档
|
||||
* @return 检索旁路事件
|
||||
*/
|
||||
private AgentRuntimeEvent retrievalEvent(AgentToolContext toolContext,
|
||||
AgentKnowledgeSpec knowledgeSpec,
|
||||
AgentKnowledgeRetrievalRequest retrievalRequest,
|
||||
List<AgentKnowledgeDocument> documents) {
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL);
|
||||
event.setToolCallId(toolContext.getToolCallId());
|
||||
event.getPayload().put("query", retrievalRequest.getQuery());
|
||||
event.getPayload().put("knowledgeId", knowledgeSpec.getKnowledgeId());
|
||||
event.getPayload().put("knowledgeName", knowledgeSpec.getName());
|
||||
event.getPayload().put("knowledgeType", knowledgeSpec.getMetadata().get("knowledgeType"));
|
||||
event.getPayload().put("faqCollection", knowledgeSpec.getMetadata().get("faqCollection"));
|
||||
event.getPayload().put("limit", retrievalRequest.getLimit());
|
||||
event.getPayload().put("scoreThreshold", retrievalRequest.getScoreThreshold());
|
||||
event.getPayload().put("documentCount", documents.size());
|
||||
event.getPayload().put("documents", documentSummaries(documents));
|
||||
event.getMetadata().put("toolName", AgentKnowledgeToolNames.build(knowledgeSpec.getRuntimeName()));
|
||||
event.getMetadata().put("knowledgeRuntimeName", knowledgeSpec.getRuntimeName());
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检索并合并所有已配置的知识源。
|
||||
*
|
||||
* @param query 查询
|
||||
* @param config 检索配置
|
||||
* @return 合并后的文档
|
||||
*/
|
||||
private List<Document> retrieveAll(String query, RetrieveConfig config) {
|
||||
List<AgentKnowledgeDocument> allDocuments = new ArrayList<>();
|
||||
int globalLimit = config == null || config.getLimit() <= 0 ? 5 : config.getLimit();
|
||||
double globalThreshold = config == null ? 0D : config.getScoreThreshold();
|
||||
for (AgentKnowledgeSpec spec : request.getAgentDefinition().getKnowledgeSpecs()) {
|
||||
AgentKnowledgeRetriever retriever = request.getKnowledgeRetrievers().get(spec.getKnowledgeId());
|
||||
if (retriever == null) {
|
||||
throw new AgentRuntimeException("Knowledge retriever is required: " + spec.getKnowledgeId());
|
||||
}
|
||||
AgentKnowledgeRetrievalRequest retrievalRequest = new AgentKnowledgeRetrievalRequest();
|
||||
retrievalRequest.setQuery(query);
|
||||
retrievalRequest.setLimit(spec.getLimit());
|
||||
retrievalRequest.setScoreThreshold(Math.max(spec.getScoreThreshold(), globalThreshold));
|
||||
retrievalRequest.setKnowledgeSpec(spec);
|
||||
retrievalRequest.setRuntimeContext(request.getRuntimeContext());
|
||||
retrievalRequest.getMetadata().put("traceId", request.getTraceId());
|
||||
retrievalRequest.getMetadata().put("sessionId", request.getSessionId());
|
||||
AgentKnowledgeRetrievalResult result = retriever.retrieve(retrievalRequest);
|
||||
if (result == null || result.getDocuments() == null) {
|
||||
emitKnowledgeRetrievalEvent(query, spec, retrievalRequest, new ArrayList<>());
|
||||
continue;
|
||||
}
|
||||
emitKnowledgeRetrievalEvent(query, spec, retrievalRequest, result.getDocuments());
|
||||
for (AgentKnowledgeDocument document : result.getDocuments()) {
|
||||
preserveKnowledgeMetadata(spec, document);
|
||||
allDocuments.add(document);
|
||||
}
|
||||
}
|
||||
allDocuments.sort(Comparator.comparing(
|
||||
AgentKnowledgeDocument::getScore,
|
||||
Comparator.nullsLast(Comparator.reverseOrder())
|
||||
));
|
||||
if (allDocuments.size() > globalLimit) {
|
||||
allDocuments = new ArrayList<>(allDocuments.subList(0, globalLimit));
|
||||
}
|
||||
return toDocuments(allDocuments);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在单条文档上保留知识库级元数据。
|
||||
*
|
||||
* @param spec 知识库声明
|
||||
* @param document 文档
|
||||
*/
|
||||
private void preserveKnowledgeMetadata(AgentKnowledgeSpec spec, AgentKnowledgeDocument document) {
|
||||
Map<String, Object> knowledgeMetadata = new LinkedHashMap<>(spec.getMetadata());
|
||||
knowledgeMetadata.put("knowledgeId", spec.getKnowledgeId());
|
||||
knowledgeMetadata.put("knowledgeName", spec.getName());
|
||||
knowledgeMetadata.put("retrievalMode", spec.getRetrievalMode().name());
|
||||
knowledgeMetadata.putAll(document.getKnowledgeMetadata());
|
||||
document.setKnowledgeMetadata(knowledgeMetadata);
|
||||
document.getMetadata().putIfAbsent("knowledgeId", spec.getKnowledgeId());
|
||||
document.getMetadata().putIfAbsent("knowledgeName", spec.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发射知识库检索事件,供聊天界面展示检索过程。
|
||||
*
|
||||
* @param query 查询
|
||||
* @param spec 知识库声明
|
||||
* @param retrievalRequest 检索请求
|
||||
* @param documents 检索文档
|
||||
*/
|
||||
private void emitKnowledgeRetrievalEvent(String query,
|
||||
AgentKnowledgeSpec spec,
|
||||
AgentKnowledgeRetrievalRequest retrievalRequest,
|
||||
List<AgentKnowledgeDocument> documents) {
|
||||
if (eventSink == null) {
|
||||
return;
|
||||
}
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL);
|
||||
event.setTraceId(request.getTraceId());
|
||||
event.setSessionId(request.getSessionId());
|
||||
event.setAgentId(request.getAgentDefinition().getAgentId());
|
||||
event.getMetadata().put("requestId", request.getRequestId());
|
||||
event.getPayload().put("query", query);
|
||||
event.getPayload().put("knowledgeId", spec.getKnowledgeId());
|
||||
event.getPayload().put("knowledgeName", spec.getName());
|
||||
event.getPayload().put("limit", retrievalRequest.getLimit());
|
||||
event.getPayload().put("scoreThreshold", retrievalRequest.getScoreThreshold());
|
||||
event.getPayload().put("documentCount", documents == null ? 0 : documents.size());
|
||||
event.getPayload().put("documents", documentSummaries(documents));
|
||||
Sinks.EmitResult result = eventSink.tryEmitNext(event);
|
||||
if (result.isFailure()) {
|
||||
throw new AgentRuntimeException("Failed to emit knowledge retrieval event: " + result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用于事件展示的文档摘要,避免把全文内容放入事件。
|
||||
*
|
||||
* @param documents 检索文档
|
||||
* @return 文档摘要列表
|
||||
*/
|
||||
private List<Map<String, Object>> documentSummaries(List<AgentKnowledgeDocument> documents) {
|
||||
List<Map<String, Object>> summaries = new ArrayList<>();
|
||||
if (documents == null) {
|
||||
return summaries;
|
||||
}
|
||||
for (AgentKnowledgeDocument document : documents) {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("documentId", document.getDocumentId());
|
||||
summary.put("documentName", document.getDocumentName());
|
||||
summary.put("chunkId", document.getChunkId());
|
||||
summary.put("score", document.getScore());
|
||||
summary.put("sourceUri", document.getSourceUri());
|
||||
summary.put("metadata", document.getMetadata());
|
||||
summaries.add(summary);
|
||||
}
|
||||
/**
|
||||
* 将最终文档转换为 UI 和完成消息引用可消费的稳定摘要。
|
||||
*
|
||||
* @param documents 最终文档
|
||||
* @return 文档摘要列表
|
||||
*/
|
||||
private List<Map<String, Object>> documentSummaries(List<AgentKnowledgeDocument> documents) {
|
||||
List<Map<String, Object>> summaries = new ArrayList<>();
|
||||
if (documents == null) {
|
||||
return summaries;
|
||||
}
|
||||
for (AgentKnowledgeDocument document : documents) {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("documentId", document.getDocumentId());
|
||||
summary.put("documentName", document.getDocumentName());
|
||||
summary.put("chunkId", document.getChunkId());
|
||||
summary.put("chunkContent", document.getContent());
|
||||
summary.put("score", document.getScore());
|
||||
summary.put("sourceUri", document.getSourceUri());
|
||||
summary.put("metadata", document.getMetadata());
|
||||
summaries.add(summary);
|
||||
}
|
||||
return summaries;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化模型可见的结构化检索证据。
|
||||
*
|
||||
* @param knowledgeSpec 知识库声明
|
||||
* @param query 实际检索词
|
||||
* @param documents 最终文档
|
||||
* @return 模型上下文文本
|
||||
*/
|
||||
private String modelContent(AgentKnowledgeSpec knowledgeSpec,
|
||||
String query,
|
||||
List<AgentKnowledgeDocument> documents) {
|
||||
String knowledgeName = hasText(knowledgeSpec.getName())
|
||||
? knowledgeSpec.getName().trim()
|
||||
: knowledgeSpec.getRuntimeName().trim();
|
||||
if (documents.isEmpty()) {
|
||||
return "No relevant documents were found in knowledge base \""
|
||||
+ knowledgeName + "\" for query: " + query;
|
||||
}
|
||||
StringBuilder content = new StringBuilder()
|
||||
.append("Retrieved evidence from knowledge base \"")
|
||||
.append(knowledgeName)
|
||||
.append("\" for query: ")
|
||||
.append(query)
|
||||
.append("\n\n");
|
||||
for (int index = 0; index < documents.size(); index++) {
|
||||
AgentKnowledgeDocument document = documents.get(index);
|
||||
content.append("[Evidence ").append(index + 1).append("]\n");
|
||||
appendField(content, "Document", document.getDocumentName());
|
||||
appendField(content, "Document ID", document.getDocumentId());
|
||||
appendField(content, "Chunk ID", document.getChunkId());
|
||||
appendField(content, "Source", document.getSourceUri());
|
||||
if (document.getScore() != null) {
|
||||
content.append("Score: ").append(document.getScore()).append('\n');
|
||||
}
|
||||
content.append("Content:\n")
|
||||
.append(document.getContent() == null ? "" : document.getContent())
|
||||
.append("\n\n");
|
||||
}
|
||||
return content.toString().stripTrailing();
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加非空证据字段。
|
||||
*
|
||||
* @param content 输出缓冲区
|
||||
* @param label 字段标签
|
||||
* @param value 字段值
|
||||
*/
|
||||
private void appendField(StringBuilder content, String label, String value) {
|
||||
if (hasText(value)) {
|
||||
content.append(label).append(": ").append(value.trim()).append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否包含非空白文本。
|
||||
*
|
||||
* @param value 待判断字符串
|
||||
* @return 包含文本时为 true
|
||||
*/
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.isBlank();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将可空值转换为字符串。
|
||||
*
|
||||
* @param value 原始值
|
||||
* @return 字符串;原始值为空时返回 null
|
||||
*/
|
||||
private String stringValue(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,16 +28,35 @@ public class AgentScopeMemoryAdapter {
|
||||
* @return AgentScope 记忆
|
||||
*/
|
||||
public Memory createMemory(AgentMemorySnapshot snapshot, AgentMemoryPolicy policy, Model model) {
|
||||
return createMemoryResult(snapshot, policy, model).getMemory();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建记忆并返回 AutoContext 配置快照。
|
||||
*
|
||||
* <p>有状态 runtime 需要把同一份 {@link AutoContextConfig} 交给 AutoContext干预器,
|
||||
* 用于判断是否进入压缩流程并发出旁路事件。非 AutoContext 记忆不会返回配置。</p>
|
||||
*
|
||||
* @param snapshot 记忆快照
|
||||
* @param policy 记忆策略
|
||||
* @param model 用于自动压缩的模型
|
||||
* @return 记忆构建结果
|
||||
*/
|
||||
public AgentScopeMemoryBuildResult createMemoryResult(AgentMemorySnapshot snapshot,
|
||||
AgentMemoryPolicy policy,
|
||||
Model model) {
|
||||
AgentMemoryPolicy safePolicy = policy == null ? AgentMemoryPolicy.autoContext() : policy;
|
||||
Memory memory;
|
||||
AutoContextConfig autoContextConfig = null;
|
||||
if (safePolicy.getType() == com.easyagents.agent.runtime.memory.AgentMemoryType.AUTO_CONTEXT
|
||||
&& safePolicy.getCompressionParameter().isEnabled()) {
|
||||
memory = new AutoContextMemory(toAutoContextConfig(safePolicy.getCompressionParameter()), model);
|
||||
autoContextConfig = toAutoContextConfig(safePolicy.getCompressionParameter());
|
||||
memory = new AutoContextMemory(autoContextConfig, new AgentScopeAutoContextCompressionModel(model));
|
||||
} else {
|
||||
memory = new InMemoryMemory();
|
||||
}
|
||||
attachMessages(memory, snapshot, safePolicy.getMaxAttachedMessageCount());
|
||||
return memory;
|
||||
return new AgentScopeMemoryBuildResult(memory, autoContextConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,13 +67,14 @@ public class AgentScopeMemoryAdapter {
|
||||
*/
|
||||
public AutoContextConfig toAutoContextConfig(AgentMemoryCompressionParameter parameter) {
|
||||
AgentMemoryCompressionParameter safeParameter = parameter == null ? new AgentMemoryCompressionParameter() : parameter;
|
||||
Integer compressionThreshold = safeParameter.getMinCompressionTokenThreshold();
|
||||
return AutoContextConfig.builder()
|
||||
.msgThreshold(safeParameter.getMsgThreshold())
|
||||
.msgThreshold(safeParameter.getMsgThreshold() == null ? Integer.MAX_VALUE : safeParameter.getMsgThreshold())
|
||||
.lastKeep(safeParameter.getLastKeep())
|
||||
.tokenRatio(safeParameter.getTokenRatio())
|
||||
.maxToken(safeParameter.getMaxToken())
|
||||
.tokenRatio(1.0D)
|
||||
.maxToken(compressionThreshold == null ? Long.MAX_VALUE : compressionThreshold)
|
||||
.largePayloadThreshold(safeParameter.getLargePayloadThreshold())
|
||||
.minCompressionTokenThreshold(safeParameter.getMinCompressionTokenThreshold())
|
||||
.minCompressionTokenThreshold(compressionThreshold == null ? Integer.MAX_VALUE : compressionThreshold)
|
||||
.currentRoundCompressionRatio(safeParameter.getCurrentRoundCompressionRatio())
|
||||
.minConsecutiveToolMessages(safeParameter.getMinConsecutiveToolMessages())
|
||||
.build();
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.easyagents.agent.runtime.agentscope;
|
||||
|
||||
import io.agentscope.core.memory.Memory;
|
||||
import io.agentscope.core.memory.autocontext.AutoContextConfig;
|
||||
|
||||
/**
|
||||
* AgentScope 记忆构建结果。
|
||||
*
|
||||
* <p>AutoContext 的压缩入口判断必须使用创建 {@code AutoContextMemory} 时的同一份
|
||||
* {@link AutoContextConfig}。该结果对象将记忆实例和配置快照一起返回,避免运行时
|
||||
* 干预器重新推导配置导致事件触发条件与 AgentScope 主线路不一致。</p>
|
||||
*/
|
||||
public class AgentScopeMemoryBuildResult {
|
||||
|
||||
private final Memory memory;
|
||||
private final AutoContextConfig autoContextConfig;
|
||||
|
||||
/**
|
||||
* 创建记忆构建结果。
|
||||
*
|
||||
* @param memory AgentScope 记忆实例
|
||||
* @param autoContextConfig AutoContext 配置,非 AutoContext 记忆时为 null
|
||||
*/
|
||||
public AgentScopeMemoryBuildResult(Memory memory, AutoContextConfig autoContextConfig) {
|
||||
this.memory = memory;
|
||||
this.autoContextConfig = autoContextConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AgentScope 记忆实例。
|
||||
*
|
||||
* @return AgentScope 记忆实例
|
||||
*/
|
||||
public Memory getMemory() {
|
||||
return memory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AutoContext 配置。
|
||||
*
|
||||
* @return AutoContext 配置,非 AutoContext 记忆时为 null
|
||||
*/
|
||||
public AutoContextConfig getAutoContextConfig() {
|
||||
return autoContextConfig;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ import io.agentscope.core.message.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -15,6 +17,9 @@ import java.util.Map;
|
||||
*/
|
||||
public class AgentScopeMessageAdapter {
|
||||
|
||||
/** AgentScope URLSource 中用于持久化业务媒体引用的私有协议。 */
|
||||
public static final String MEDIA_REFERENCE_SCHEME = "easyagents-media://";
|
||||
|
||||
/**
|
||||
* 将运行时消息转换为 AgentScope 消息。
|
||||
*
|
||||
@@ -276,6 +281,11 @@ public class AgentScopeMessageAdapter {
|
||||
.data(block.getData())
|
||||
.build();
|
||||
}
|
||||
if (block.getReference() != null && !block.getReference().isBlank()) {
|
||||
String encodedReference = Base64.getUrlEncoder().withoutPadding()
|
||||
.encodeToString(block.getReference().getBytes(StandardCharsets.UTF_8));
|
||||
return URLSource.builder().url(MEDIA_REFERENCE_SCHEME + encodedReference).build();
|
||||
}
|
||||
return URLSource.builder().url(block.getUrl()).build();
|
||||
}
|
||||
|
||||
@@ -286,7 +296,30 @@ public class AgentScopeMessageAdapter {
|
||||
return;
|
||||
}
|
||||
if (source instanceof URLSource urlSource) {
|
||||
block.setUrl(urlSource.getUrl());
|
||||
String url = urlSource.getUrl();
|
||||
if (url != null && url.startsWith(MEDIA_REFERENCE_SCHEME)) {
|
||||
block.setReference(decodeReference(url));
|
||||
} else {
|
||||
block.setUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 AgentScope 私有 URLSource 解码业务媒体引用。
|
||||
*
|
||||
* @param url 私有协议 URL
|
||||
* @return 原始业务媒体引用
|
||||
*/
|
||||
public static String decodeReference(String url) {
|
||||
if (url == null || !url.startsWith(MEDIA_REFERENCE_SCHEME)) {
|
||||
return null;
|
||||
}
|
||||
String encoded = url.substring(MEDIA_REFERENCE_SCHEME.length());
|
||||
try {
|
||||
return new String(Base64.getUrlDecoder().decode(encoded), StandardCharsets.UTF_8);
|
||||
} catch (IllegalArgumentException error) {
|
||||
throw new IllegalArgumentException("Invalid agent media reference.", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ package com.easyagents.agent.runtime.agentscope;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import com.easyagents.agent.runtime.model.AgentGenerationOptions;
|
||||
import com.easyagents.agent.runtime.model.AgentHttpVersionPolicy;
|
||||
import com.easyagents.agent.runtime.model.AgentModelFactory;
|
||||
import com.easyagents.agent.runtime.model.AgentModelProviderType;
|
||||
import com.easyagents.agent.runtime.model.AgentModelSpec;
|
||||
import com.easyagents.agent.runtime.model.AgentMessageContentFormat;
|
||||
import io.agentscope.core.formatter.openai.DeepSeekFormatter;
|
||||
import io.agentscope.core.formatter.openai.GLMFormatter;
|
||||
import io.agentscope.core.model.*;
|
||||
@@ -24,6 +26,24 @@ public class AgentScopeModelFactory implements AgentModelFactory<Model> {
|
||||
private static final String ARK_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
|
||||
private static final String SILICONFLOW_BASE_URL = "https://api.siliconflow.cn/v1";
|
||||
|
||||
private final AgentHttpTransportProvider httpTransportProvider;
|
||||
|
||||
/**
|
||||
* 使用进程级共享 Transport 提供器创建模型工厂。
|
||||
*/
|
||||
public AgentScopeModelFactory() {
|
||||
this(AgentHttpTransportProvider.shared());
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定 Transport 提供器创建模型工厂。
|
||||
*
|
||||
* @param httpTransportProvider Transport 提供器
|
||||
*/
|
||||
AgentScopeModelFactory(AgentHttpTransportProvider httpTransportProvider) {
|
||||
this.httpTransportProvider = httpTransportProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Model create(AgentModelSpec modelSpec, AgentGenerationOptions generationOptions) {
|
||||
if (modelSpec == null) {
|
||||
@@ -31,6 +51,7 @@ public class AgentScopeModelFactory implements AgentModelFactory<Model> {
|
||||
}
|
||||
GenerateOptions options = toGenerateOptions(modelSpec, generationOptions);
|
||||
AgentModelProviderType providerType = modelSpec.getProviderType();
|
||||
validateHttpTransportSupport(providerType, modelSpec.getHttpVersionPolicy());
|
||||
if (providerType == AgentModelProviderType.OLLAMA) {
|
||||
return buildOllama(modelSpec, options);
|
||||
}
|
||||
@@ -124,13 +145,18 @@ public class AgentScopeModelFactory implements AgentModelFactory<Model> {
|
||||
* @return 模型
|
||||
*/
|
||||
private Model buildOpenAiCompatible(AgentModelSpec modelSpec, GenerateOptions options, String defaultBaseUrl) {
|
||||
String baseUrl = resolveBaseUrl(modelSpec, defaultBaseUrl);
|
||||
OpenAIChatModel.Builder builder = OpenAIChatModel.builder()
|
||||
.apiKey(modelSpec.getApiKey())
|
||||
.modelName(modelSpec.getModelName())
|
||||
.baseUrl(resolveBaseUrl(modelSpec, defaultBaseUrl))
|
||||
.baseUrl(baseUrl)
|
||||
.endpointPath(modelSpec.getEndpointPath())
|
||||
.stream(Boolean.TRUE.equals(options.getStream()))
|
||||
.httpTransport(httpTransportProvider.getTransport(modelSpec.getHttpVersionPolicy(), baseUrl))
|
||||
.generateOptions(options);
|
||||
if (modelSpec.getMessageContentFormat() == AgentMessageContentFormat.TEXT_PARTS) {
|
||||
builder.formatter(new AgentOpenAIChatFormatter());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@@ -176,13 +202,17 @@ public class AgentScopeModelFactory implements AgentModelFactory<Model> {
|
||||
* @return 模型
|
||||
*/
|
||||
private Model buildDeepSeek(AgentModelSpec modelSpec, GenerateOptions options) {
|
||||
String baseUrl = resolveBaseUrl(modelSpec, DEEPSEEK_BASE_URL);
|
||||
OpenAIChatModel.Builder builder = OpenAIChatModel.builder()
|
||||
.apiKey(modelSpec.getApiKey())
|
||||
.modelName(modelSpec.getModelName())
|
||||
.baseUrl(resolveBaseUrl(modelSpec, DEEPSEEK_BASE_URL))
|
||||
.baseUrl(baseUrl)
|
||||
.endpointPath(modelSpec.getEndpointPath())
|
||||
.stream(Boolean.TRUE.equals(options.getStream()))
|
||||
.formatter(new DeepSeekFormatter())
|
||||
.httpTransport(httpTransportProvider.getTransport(modelSpec.getHttpVersionPolicy(), baseUrl))
|
||||
.formatter(modelSpec.getMessageContentFormat() == AgentMessageContentFormat.TEXT_PARTS
|
||||
? new AgentDeepSeekChatFormatter()
|
||||
: new DeepSeekFormatter())
|
||||
.generateOptions(options);
|
||||
return builder.build();
|
||||
}
|
||||
@@ -195,13 +225,17 @@ public class AgentScopeModelFactory implements AgentModelFactory<Model> {
|
||||
* @return 模型
|
||||
*/
|
||||
private Model buildGlm(AgentModelSpec modelSpec, GenerateOptions options) {
|
||||
String baseUrl = resolveBaseUrl(modelSpec, GLM_BASE_URL);
|
||||
OpenAIChatModel.Builder builder = OpenAIChatModel.builder()
|
||||
.apiKey(modelSpec.getApiKey())
|
||||
.modelName(modelSpec.getModelName())
|
||||
.baseUrl(resolveBaseUrl(modelSpec, GLM_BASE_URL))
|
||||
.baseUrl(baseUrl)
|
||||
.endpointPath(modelSpec.getEndpointPath())
|
||||
.stream(Boolean.TRUE.equals(options.getStream()))
|
||||
.formatter(new GLMFormatter())
|
||||
.httpTransport(httpTransportProvider.getTransport(modelSpec.getHttpVersionPolicy(), baseUrl))
|
||||
.formatter(modelSpec.getMessageContentFormat() == AgentMessageContentFormat.TEXT_PARTS
|
||||
? new AgentGLMChatFormatter()
|
||||
: new GLMFormatter())
|
||||
.generateOptions(options);
|
||||
return builder.build();
|
||||
}
|
||||
@@ -231,16 +265,39 @@ public class AgentScopeModelFactory implements AgentModelFactory<Model> {
|
||||
*/
|
||||
private Model buildDashScope(AgentModelSpec modelSpec, AgentGenerationOptions generationOptions, GenerateOptions options) {
|
||||
Boolean thinkingEnabled = generationOptions == null ? null : generationOptions.getThinkingEnabled();
|
||||
String baseUrl = modelSpec.getBaseUrl();
|
||||
return DashScopeChatModel.builder()
|
||||
.apiKey(modelSpec.getApiKey())
|
||||
.modelName(modelSpec.getModelName())
|
||||
.baseUrl(modelSpec.getBaseUrl())
|
||||
.baseUrl(baseUrl)
|
||||
.stream(Boolean.TRUE.equals(options.getStream()))
|
||||
.enableThinking(thinkingEnabled)
|
||||
.httpTransport(httpTransportProvider.getTransport(modelSpec.getHttpVersionPolicy(), baseUrl))
|
||||
.defaultOptions(options)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验当前 Provider 是否支持显式 Agent HTTP Transport。
|
||||
*
|
||||
* @param providerType 模型供应商类型
|
||||
* @param policy HTTP 版本策略
|
||||
*/
|
||||
private void validateHttpTransportSupport(AgentModelProviderType providerType,
|
||||
AgentHttpVersionPolicy policy) {
|
||||
if (policy == null || policy == AgentHttpVersionPolicy.AUTO) {
|
||||
return;
|
||||
}
|
||||
if (providerType == AgentModelProviderType.ANTHROPIC
|
||||
|| providerType == AgentModelProviderType.GEMINI
|
||||
|| providerType == AgentModelProviderType.OLLAMA) {
|
||||
throw new AgentRuntimeException(
|
||||
"Agent HTTP transport policy " + policy
|
||||
+ " is not supported by provider " + providerType
|
||||
+ "; use AUTO for this provider.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先使用调用方传入的基础 URL,未传入时使用供应商默认地址。
|
||||
*
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
package com.easyagents.agent.runtime.agentscope;
|
||||
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeObservationManager;
|
||||
import io.agentscope.core.hook.Hook;
|
||||
import io.agentscope.core.hook.HookEvent;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* AgentScope Hook 的统一入口。
|
||||
*/
|
||||
public class AgentScopeRuntimeHook implements Hook {
|
||||
|
||||
private final AgentRuntimeObservationManager observationManager;
|
||||
|
||||
/**
|
||||
* 创建统一 Hook 入口。
|
||||
*
|
||||
* @param observationManager 观察和干预调度器
|
||||
*/
|
||||
public AgentScopeRuntimeHook(AgentRuntimeObservationManager observationManager) {
|
||||
this.observationManager = observationManager == null
|
||||
? AgentRuntimeObservationManager.empty()
|
||||
: observationManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 AgentScope Hook 事件。
|
||||
*
|
||||
* @param event Hook 事件
|
||||
* @param <T> Hook 事件类型
|
||||
* @return 处理后的 Hook 事件
|
||||
*/
|
||||
@Override
|
||||
public <T extends HookEvent> Mono<T> onEvent(T event) {
|
||||
return observationManager.handle(event);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
package com.easyagents.agent.runtime.agentscope;
|
||||
|
||||
import com.easyagents.agent.runtime.persistence.AgentPersistencePolicy;
|
||||
import com.easyagents.agent.runtime.persistence.AgentRuntimeState;
|
||||
import com.easyagents.agent.runtime.persistence.AgentSessionStore;
|
||||
import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
|
||||
import io.agentscope.core.session.Session;
|
||||
import io.agentscope.core.state.SessionKey;
|
||||
import io.agentscope.core.state.State;
|
||||
import io.agentscope.core.state.StatePersistence;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -32,7 +30,7 @@ public class AgentScopeSessionAdapter implements Session {
|
||||
|
||||
@Override
|
||||
public void save(SessionKey sessionKey, String name, State state) {
|
||||
sessionStore.save(toKey(sessionKey), name, AgentRuntimeState.of(name, state));
|
||||
sessionStore.save(toKey(sessionKey), name, state);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,13 +42,7 @@ public class AgentScopeSessionAdapter implements Session {
|
||||
*/
|
||||
@Override
|
||||
public void save(SessionKey sessionKey, String name, List<? extends State> states) {
|
||||
List<AgentRuntimeState> converted = new ArrayList<>();
|
||||
if (states != null) {
|
||||
for (State state : states) {
|
||||
converted.add(AgentRuntimeState.of(name, state));
|
||||
}
|
||||
}
|
||||
sessionStore.saveList(toKey(sessionKey), name, converted);
|
||||
sessionStore.saveList(toKey(sessionKey), name, states);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,10 +56,7 @@ public class AgentScopeSessionAdapter implements Session {
|
||||
*/
|
||||
@Override
|
||||
public <T extends State> Optional<T> get(SessionKey sessionKey, String name, Class<T> clazz) {
|
||||
return sessionStore.get(toKey(sessionKey), name)
|
||||
.map(AgentRuntimeState::getValue)
|
||||
.filter(clazz::isInstance)
|
||||
.map(clazz::cast);
|
||||
return sessionStore.get(toKey(sessionKey), name, clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,11 +70,7 @@ public class AgentScopeSessionAdapter implements Session {
|
||||
*/
|
||||
@Override
|
||||
public <T extends State> List<T> getList(SessionKey sessionKey, String name, Class<T> clazz) {
|
||||
return sessionStore.getList(toKey(sessionKey), name).stream()
|
||||
.map(AgentRuntimeState::getValue)
|
||||
.filter(clazz::isInstance)
|
||||
.map(clazz::cast)
|
||||
.collect(Collectors.toList());
|
||||
return sessionStore.getList(toKey(sessionKey), name, clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +114,7 @@ public class AgentScopeSessionAdapter implements Session {
|
||||
*/
|
||||
public static StatePersistence toStatePersistence(AgentPersistencePolicy policy) {
|
||||
if (policy == null || !policy.isEnabled()) {
|
||||
return StatePersistence.none();
|
||||
return StatePersistence.memoryOnly();
|
||||
}
|
||||
return StatePersistence.builder()
|
||||
.memoryManaged(policy.isMemoryManaged())
|
||||
@@ -154,24 +139,17 @@ public class AgentScopeSessionAdapter implements Session {
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时支撑的 AgentScope 会话键。
|
||||
*/
|
||||
private static class RuntimeSessionKey implements SessionKey {
|
||||
|
||||
private final String value;
|
||||
|
||||
private RuntimeSessionKey(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
* 运行时支撑的 AgentScope 会话键。
|
||||
*/
|
||||
private record RuntimeSessionKey(String value) implements SessionKey {
|
||||
/**
|
||||
* 将键转换为稳定标识符。
|
||||
* 将键转换为稳定标识符。
|
||||
*
|
||||
* @return 标识符
|
||||
*/
|
||||
@Override
|
||||
public String toIdentifier() {
|
||||
return value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
package com.easyagents.agent.runtime.agentscope;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import com.easyagents.agent.runtime.skill.AgentSkillBoxSpec;
|
||||
import com.easyagents.agent.runtime.skill.AgentSkillCompiler;
|
||||
import com.easyagents.agent.runtime.skill.AgentSkillSpec;
|
||||
import com.easyagents.agent.runtime.mcp.McpSkillRegistration;
|
||||
import io.agentscope.core.skill.AgentSkill;
|
||||
import io.agentscope.core.skill.SkillBox;
|
||||
import io.agentscope.core.tool.AgentTool;
|
||||
import io.agentscope.core.tool.Toolkit;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -19,18 +21,14 @@ public class AgentScopeSkillAdapter implements AgentSkillCompiler<AgentSkill> {
|
||||
|
||||
@Override
|
||||
public AgentSkill compile(AgentSkillSpec skillSpec) {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>(skillSpec.getMetadata());
|
||||
metadata.put("skillId", skillSpec.getSkillId());
|
||||
metadata.put("name", skillSpec.getName());
|
||||
metadata.put("description", skillSpec.getDescription());
|
||||
return AgentSkill.builder()
|
||||
.name(skillSpec.getName())
|
||||
.description(skillSpec.getDescription())
|
||||
.skillContent(skillSpec.getSkillContent())
|
||||
.metadata(metadata)
|
||||
.resources(skillSpec.getResources())
|
||||
.source(skillSpec.getSource())
|
||||
.build();
|
||||
validateSkillSpec(skillSpec);
|
||||
return new EasyAgentsAgentSkill(
|
||||
skillSpec.getSkillId(),
|
||||
skillSpec.getName(),
|
||||
skillSpec.getDescription(),
|
||||
skillSpec.getSkillContent(),
|
||||
skillSpec.getResources(),
|
||||
skillSpec.getSource());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,6 +69,22 @@ public class AgentScopeSkillAdapter implements AgentSkillCompiler<AgentSkill> {
|
||||
* @return SkillBox;未配置 Skill 时返回 null
|
||||
*/
|
||||
public SkillBox createSkillBox(AgentSkillBoxSpec spec, Toolkit toolkit, Map<String, List<AgentTool>> skillTools) {
|
||||
return createSkillBox(spec, toolkit, skillTools, List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并绑定静态工具及 MCP 工具的 AgentScope SkillBox。
|
||||
*
|
||||
* @param spec SkillBox 声明
|
||||
* @param toolkit Toolkit 实例
|
||||
* @param skillTools 按 Skill ID 分组的静态工具
|
||||
* @param skillMcpRegistrations 按 Skill 延迟激活的 MCP client
|
||||
* @return SkillBox;未配置 Skill 时返回 null
|
||||
*/
|
||||
public SkillBox createSkillBox(AgentSkillBoxSpec spec,
|
||||
Toolkit toolkit,
|
||||
Map<String, List<AgentTool>> skillTools,
|
||||
List<McpSkillRegistration> skillMcpRegistrations) {
|
||||
if (spec == null || spec.getSkills().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
@@ -78,10 +92,15 @@ public class AgentScopeSkillAdapter implements AgentSkillCompiler<AgentSkill> {
|
||||
? new SkillBox(toolkit)
|
||||
: new SkillBox(toolkit, spec.getSkillBoxId());
|
||||
skillBox.setExposeAllSkillMetadata(spec.isExposeAllSkillMetadata());
|
||||
Map<String, List<McpSkillRegistration>> mcpBySkill = groupMcpRegistrations(skillMcpRegistrations);
|
||||
for (AgentSkillSpec skillSpec : spec.getSkills()) {
|
||||
AgentSkill skill = compile(skillSpec);
|
||||
List<AgentTool> tools = skillTools == null ? List.of() : skillTools.getOrDefault(skillSpec.getSkillId(), List.of());
|
||||
if (tools.isEmpty()) {
|
||||
List<McpSkillRegistration> mcpRegistrations = mcpBySkill.remove(skillSpec.getSkillId());
|
||||
if (mcpRegistrations == null) {
|
||||
mcpRegistrations = List.of();
|
||||
}
|
||||
if (tools.isEmpty() && mcpRegistrations.isEmpty()) {
|
||||
skillBox.registration()
|
||||
.skill(skill)
|
||||
.toolkit(toolkit)
|
||||
@@ -101,8 +120,106 @@ public class AgentScopeSkillAdapter implements AgentSkillCompiler<AgentSkill> {
|
||||
.agentTool(tool)
|
||||
.apply();
|
||||
}
|
||||
for (McpSkillRegistration mcpRegistration : mcpRegistrations) {
|
||||
skillBox.registration()
|
||||
.skill(skill)
|
||||
.toolkit(toolkit)
|
||||
.enableTools(mcpRegistration.getEnableTools())
|
||||
.disableTools(mcpRegistration.getDisableTools())
|
||||
.presetParameters(mcpRegistration.getPresetParameters())
|
||||
.mcpClient(mcpRegistration.getClient())
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
if (!mcpBySkill.isEmpty()) {
|
||||
throw new AgentRuntimeException("Skill-bound MCP references unknown skill: "
|
||||
+ mcpBySkill.keySet().iterator().next());
|
||||
}
|
||||
skillBox.syncToolGroupStates();
|
||||
return skillBox;
|
||||
}
|
||||
|
||||
private Map<String, List<McpSkillRegistration>> groupMcpRegistrations(
|
||||
List<McpSkillRegistration> registrations) {
|
||||
Map<String, List<McpSkillRegistration>> grouped = new LinkedHashMap<>();
|
||||
if (registrations == null) {
|
||||
return grouped;
|
||||
}
|
||||
for (McpSkillRegistration registration : registrations) {
|
||||
if (registration == null || registration.getSkillId() == null
|
||||
|| registration.getSkillId().isBlank()) {
|
||||
throw new AgentRuntimeException("Skill-bound MCP skill id is required.");
|
||||
}
|
||||
grouped.computeIfAbsent(registration.getSkillId(), key -> new java.util.ArrayList<>())
|
||||
.add(registration);
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Skill 声明是否具备 AgentScope 注册和模型提示所需的必要信息。
|
||||
*
|
||||
* @param skillSpec Skill 声明
|
||||
* @throws AgentRuntimeException Skill 声明为空或核心字段缺失时抛出
|
||||
*/
|
||||
private void validateSkillSpec(AgentSkillSpec skillSpec) {
|
||||
if (skillSpec == null) {
|
||||
throw new AgentRuntimeException("Agent skill spec is required.");
|
||||
}
|
||||
if (skillSpec.getSkillId() == null || skillSpec.getSkillId().isBlank()) {
|
||||
throw new AgentRuntimeException("Agent skill id is required.");
|
||||
}
|
||||
if (skillSpec.getName() == null || skillSpec.getName().isBlank()) {
|
||||
throw new AgentRuntimeException("Agent skill name is required: " + skillSpec.getSkillId());
|
||||
}
|
||||
if (skillSpec.getDescription() == null || skillSpec.getDescription().isBlank()) {
|
||||
throw new AgentRuntimeException("Agent skill description is required: " + skillSpec.getSkillId());
|
||||
}
|
||||
if (skillSpec.getSkillContent() == null || skillSpec.getSkillContent().isBlank()) {
|
||||
throw new AgentRuntimeException("Agent skill content is required: " + skillSpec.getSkillId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保持 Easy-Agents Skill ID 与 AgentScope Skill ID 完全一致。
|
||||
*
|
||||
* <p>AgentScope 默认用 {@code name + "_" + source} 生成 Skill ID,而 Easy-Agents
|
||||
* 的工具绑定、旁路事件和调用层配置都以 {@link AgentSkillSpec#getSkillId()} 为准。
|
||||
* 如果不覆盖这里,模型在 prompt 中看到的 skill-id 会和 Easy 侧绑定 key 不一致,
|
||||
* 后续 Skill 状态监听也无法做到精准归属。</p>
|
||||
*/
|
||||
private static class EasyAgentsAgentSkill extends AgentSkill {
|
||||
|
||||
private final String skillId;
|
||||
|
||||
/**
|
||||
* 创建 ID 对齐的 AgentScope Skill。
|
||||
*
|
||||
* @param skillId Easy-Agents Skill ID
|
||||
* @param name Skill 展示名称
|
||||
* @param description Skill 描述
|
||||
* @param skillContent Skill 内容
|
||||
* @param resources Skill 资源
|
||||
* @param source Skill 来源
|
||||
*/
|
||||
EasyAgentsAgentSkill(String skillId,
|
||||
String name,
|
||||
String description,
|
||||
String skillContent,
|
||||
Map<String, String> resources,
|
||||
String source) {
|
||||
super(name, description, skillContent, resources, source);
|
||||
this.skillId = skillId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Easy-Agents 声明的 Skill ID。
|
||||
*
|
||||
* @return Easy-Agents Skill ID
|
||||
*/
|
||||
@Override
|
||||
public String getSkillId() {
|
||||
return skillId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.easyagents.agent.runtime.agentscope;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRunRequest;
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||
import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
|
||||
import com.easyagents.agent.runtime.event.*;
|
||||
import com.easyagents.agent.runtime.hitl.AgentPendingState;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalCoordinator;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRejectedException;
|
||||
@@ -39,8 +38,9 @@ public class AgentScopeToolAdapter {
|
||||
* @param request 运行请求
|
||||
* @return AgentScope 工具
|
||||
*/
|
||||
public AgentTool adapt(AgentToolSpec toolSpec, AgentToolInvoker invoker, AgentRunRequest request) {
|
||||
return adapt(toolSpec, invoker, request, AgentToolApprovalCoordinator.disabled(), null);
|
||||
public AgentTool adapt(AgentToolSpec toolSpec, AgentToolInvoker invoker, AgentRuntimeExecutionContext request) {
|
||||
return adapt(toolSpec, invoker, request, AgentToolApprovalCoordinator.disabled(),
|
||||
(Sinks.Many<AgentRuntimeEvent>) null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,9 +54,10 @@ public class AgentScopeToolAdapter {
|
||||
*/
|
||||
public AgentTool adapt(AgentToolSpec toolSpec,
|
||||
AgentToolInvoker invoker,
|
||||
AgentRunRequest request,
|
||||
AgentRuntimeExecutionContext request,
|
||||
Sinks.Many<AgentRuntimeEvent> eventSink) {
|
||||
return adapt(toolSpec, invoker, request, AgentToolApprovalCoordinator.disabled(), eventSink);
|
||||
return adapt(toolSpec, invoker, request, AgentToolApprovalCoordinator.disabled(), fixedHolder(request, eventSink),
|
||||
null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,10 +72,10 @@ public class AgentScopeToolAdapter {
|
||||
*/
|
||||
public AgentTool adapt(AgentToolSpec toolSpec,
|
||||
AgentToolInvoker invoker,
|
||||
AgentRunRequest request,
|
||||
AgentRuntimeExecutionContext request,
|
||||
AgentToolApprovalCoordinator approvalCoordinator,
|
||||
Sinks.Many<AgentRuntimeEvent> eventSink) {
|
||||
return adapt(toolSpec, invoker, request, approvalCoordinator, eventSink, null);
|
||||
return adapt(toolSpec, invoker, request, approvalCoordinator, fixedHolder(request, eventSink), null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,11 +91,11 @@ public class AgentScopeToolAdapter {
|
||||
*/
|
||||
public AgentTool adapt(AgentToolSpec toolSpec,
|
||||
AgentToolInvoker invoker,
|
||||
AgentRunRequest request,
|
||||
AgentRuntimeExecutionContext request,
|
||||
AgentToolApprovalCoordinator approvalCoordinator,
|
||||
Sinks.Many<AgentRuntimeEvent> eventSink,
|
||||
AgentSkillBinding skillBinding) {
|
||||
return adapt(toolSpec, invoker, request, approvalCoordinator, eventSink, null, skillBinding);
|
||||
return adapt(toolSpec, invoker, request, approvalCoordinator, fixedHolder(request, eventSink), null, skillBinding);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,27 +112,173 @@ public class AgentScopeToolAdapter {
|
||||
*/
|
||||
public AgentTool adapt(AgentToolSpec toolSpec,
|
||||
AgentToolInvoker invoker,
|
||||
AgentRunRequest request,
|
||||
AgentRuntimeExecutionContext request,
|
||||
AgentToolApprovalCoordinator approvalCoordinator,
|
||||
Sinks.Many<AgentRuntimeEvent> eventSink,
|
||||
AgentSkillRuntimeContext skillContext,
|
||||
AgentSkillBinding skillBinding) {
|
||||
return adapt(toolSpec, invoker, request, approvalCoordinator, fixedHolder(request, eventSink), skillContext, skillBinding);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运行时工具声明和调用器转换为可读取当前运行轮次的 AgentScope AgentTool。
|
||||
*
|
||||
* @param toolSpec 工具声明
|
||||
* @param invoker 工具调用器
|
||||
* @param request 运行时级上下文
|
||||
* @param approvalCoordinator 审批协调器
|
||||
* @param turnContextHolder 当前运行轮次上下文持有器
|
||||
* @param skillContext Skill 运行时上下文
|
||||
* @param skillBinding Skill 静态绑定关系
|
||||
* @return AgentScope 工具
|
||||
*/
|
||||
public AgentTool adapt(AgentToolSpec toolSpec,
|
||||
AgentToolInvoker invoker,
|
||||
AgentRuntimeExecutionContext request,
|
||||
AgentToolApprovalCoordinator approvalCoordinator,
|
||||
AgentRuntimeTurnContextHolder turnContextHolder,
|
||||
AgentSkillRuntimeContext skillContext,
|
||||
AgentSkillBinding skillBinding) {
|
||||
return adapt(toolSpec, invoker, request, approvalCoordinator, turnContextHolder, skillContext, skillBinding, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运行时工具声明和调用器转换为可读取当前运行轮次的 AgentScope AgentTool。
|
||||
*
|
||||
* @param toolSpec 工具声明
|
||||
* @param invoker 工具调用器
|
||||
* @param request 运行时级上下文
|
||||
* @param approvalCoordinator 审批协调器
|
||||
* @param turnContextHolder 当前运行轮次上下文持有器
|
||||
* @param skillContext Skill 运行时上下文
|
||||
* @param skillBinding Skill 静态绑定关系
|
||||
* @param emitNormalToolResult 是否由 adapter 发出普通工具结果旁路事件
|
||||
* @return AgentScope 工具
|
||||
*/
|
||||
public AgentTool adapt(AgentToolSpec toolSpec,
|
||||
AgentToolInvoker invoker,
|
||||
AgentRuntimeExecutionContext request,
|
||||
AgentToolApprovalCoordinator approvalCoordinator,
|
||||
AgentRuntimeTurnContextHolder turnContextHolder,
|
||||
AgentSkillRuntimeContext skillContext,
|
||||
AgentSkillBinding skillBinding,
|
||||
boolean emitNormalToolResult) {
|
||||
if (toolSpec == null || toolSpec.getName() == null) {
|
||||
throw new AgentRuntimeException("Agent tool spec and name are required.");
|
||||
}
|
||||
if (invoker == null) {
|
||||
throw new AgentRuntimeException("Agent tool invoker is required: " + toolSpec.getName());
|
||||
}
|
||||
return new RuntimeAgentTool(toolSpec, invoker, request, approvalCoordinator, eventSink, skillContext, skillBinding);
|
||||
return new RuntimeAgentTool(toolSpec, invoker, request, approvalCoordinator, turnContextHolder,
|
||||
skillContext, skillBinding, emitNormalToolResult, true, true,
|
||||
resolveInvocationClassLoader(invoker));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运行时工具声明和调用器转换为可读取当前运行轮次的 AgentScope AgentTool。
|
||||
*
|
||||
* @param toolSpec 工具声明
|
||||
* @param invoker 工具调用器
|
||||
* @param request 运行时级上下文
|
||||
* @param approvalCoordinator 审批协调器
|
||||
* @param turnContextHolder 当前运行轮次上下文持有器
|
||||
* @param skillContext Skill 运行时上下文
|
||||
* @param skillBinding Skill 静态绑定关系
|
||||
* @param emitNormalToolResult 是否由 adapter 发出普通工具结果旁路事件
|
||||
* @param emitSkillStep 是否由 adapter 发出 Skill 步骤旁路事件
|
||||
* @return AgentScope 工具
|
||||
*/
|
||||
public AgentTool adapt(AgentToolSpec toolSpec,
|
||||
AgentToolInvoker invoker,
|
||||
AgentRuntimeExecutionContext request,
|
||||
AgentToolApprovalCoordinator approvalCoordinator,
|
||||
AgentRuntimeTurnContextHolder turnContextHolder,
|
||||
AgentSkillRuntimeContext skillContext,
|
||||
AgentSkillBinding skillBinding,
|
||||
boolean emitNormalToolResult,
|
||||
boolean emitSkillStep) {
|
||||
if (toolSpec == null || toolSpec.getName() == null) {
|
||||
throw new AgentRuntimeException("Agent tool spec and name are required.");
|
||||
}
|
||||
if (invoker == null) {
|
||||
throw new AgentRuntimeException("Agent tool invoker is required: " + toolSpec.getName());
|
||||
}
|
||||
return new RuntimeAgentTool(toolSpec, invoker, request, approvalCoordinator, turnContextHolder,
|
||||
skillContext, skillBinding, emitNormalToolResult, emitSkillStep, true,
|
||||
resolveInvocationClassLoader(invoker));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运行时工具声明和调用器转换为可读取当前运行轮次的 AgentScope AgentTool。
|
||||
*
|
||||
* @param toolSpec 工具声明
|
||||
* @param invoker 工具调用器
|
||||
* @param request 运行时级上下文
|
||||
* @param approvalCoordinator 审批协调器
|
||||
* @param turnContextHolder 当前运行轮次上下文持有器
|
||||
* @param skillContext Skill 运行时上下文
|
||||
* @param skillBinding Skill 静态绑定关系
|
||||
* @param emitNormalToolResult 是否由 adapter 发出普通工具结果旁路事件
|
||||
* @param emitSkillStep 是否由 adapter 发出 Skill 步骤旁路事件
|
||||
* @param handleApprovalInTool 是否在工具执行阶段处理审批
|
||||
* @return AgentScope 工具
|
||||
*/
|
||||
public AgentTool adapt(AgentToolSpec toolSpec,
|
||||
AgentToolInvoker invoker,
|
||||
AgentRuntimeExecutionContext request,
|
||||
AgentToolApprovalCoordinator approvalCoordinator,
|
||||
AgentRuntimeTurnContextHolder turnContextHolder,
|
||||
AgentSkillRuntimeContext skillContext,
|
||||
AgentSkillBinding skillBinding,
|
||||
boolean emitNormalToolResult,
|
||||
boolean emitSkillStep,
|
||||
boolean handleApprovalInTool) {
|
||||
if (toolSpec == null || toolSpec.getName() == null) {
|
||||
throw new AgentRuntimeException("Agent tool spec and name are required.");
|
||||
}
|
||||
if (invoker == null) {
|
||||
throw new AgentRuntimeException("Agent tool invoker is required: " + toolSpec.getName());
|
||||
}
|
||||
return new RuntimeAgentTool(toolSpec, invoker, request, approvalCoordinator, turnContextHolder,
|
||||
skillContext, skillBinding, emitNormalToolResult, emitSkillStep, handleApprovalInTool,
|
||||
resolveInvocationClassLoader(invoker));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析工具执行时应使用的应用类加载器。
|
||||
*
|
||||
* <p>AgentScope 可能在 Reactor 工作线程执行工具。Spring Boot 可执行包中的业务类依赖
|
||||
* 注册工具时的应用类加载器,不能依赖工作线程可能继承到的系统类加载器。</p>
|
||||
*
|
||||
* @param invoker 工具调用器
|
||||
* @return 工具调用器所属类加载器;无法取得时回退到当前线程上下文类加载器
|
||||
*/
|
||||
private ClassLoader resolveInvocationClassLoader(AgentToolInvoker invoker) {
|
||||
ClassLoader invokerClassLoader = invoker.getClass().getClassLoader();
|
||||
return invokerClassLoader == null
|
||||
? Thread.currentThread().getContextClassLoader()
|
||||
: invokerClassLoader;
|
||||
}
|
||||
|
||||
private AgentRuntimeTurnContextHolder fixedHolder(AgentRuntimeExecutionContext request,
|
||||
Sinks.Many<AgentRuntimeEvent> eventSink) {
|
||||
AgentRuntimeTurnContextHolder holder = new AgentRuntimeTurnContextHolder();
|
||||
AgentRuntimeEventBridge bridge = new AgentRuntimeEventBridge(request, holder);
|
||||
holder.set(new AgentRuntimeTurnContext(null, eventSink, bridge));
|
||||
return holder;
|
||||
}
|
||||
|
||||
private record RuntimeAgentTool(AgentToolSpec toolSpec,
|
||||
AgentToolInvoker invoker,
|
||||
AgentRunRequest request,
|
||||
AgentRuntimeExecutionContext request,
|
||||
AgentToolApprovalCoordinator approvalCoordinator,
|
||||
Sinks.Many<AgentRuntimeEvent> eventSink,
|
||||
AgentRuntimeTurnContextHolder turnContextHolder,
|
||||
AgentSkillRuntimeContext skillContext,
|
||||
AgentSkillBinding skillBinding) implements AgentTool {
|
||||
AgentSkillBinding skillBinding,
|
||||
boolean emitNormalToolResult,
|
||||
boolean emitSkillStep,
|
||||
boolean handleApprovalInTool,
|
||||
ClassLoader invocationClassLoader) implements AgentTool {
|
||||
|
||||
/**
|
||||
* 获取工具名称。
|
||||
@@ -181,11 +328,14 @@ public class AgentScopeToolAdapter {
|
||||
*/
|
||||
@Override
|
||||
public Mono<ToolResultBlock> callAsync(ToolCallParam param) {
|
||||
emit(toolCallEvent(param == null ? null : param.getToolUseBlock()));
|
||||
AgentRuntimeEvent startEvent = toolExecutionStartEvent(param == null ? null : param.getToolUseBlock());
|
||||
if (startEvent != null) {
|
||||
emit(startEvent);
|
||||
}
|
||||
Map<String, Object> input = param == null || param.getInput() == null
|
||||
? new LinkedHashMap<>()
|
||||
: new LinkedHashMap<>(param.getInput());
|
||||
if (toolSpec.isApprovalRequired()) {
|
||||
if (handleApprovalInTool && toolSpec.isApprovalRequired()) {
|
||||
if (approvalCoordinator == null) {
|
||||
throw new AgentRuntimeException("Agent tool approval coordinator is required: " + toolSpec.getName());
|
||||
}
|
||||
@@ -224,15 +374,17 @@ public class AgentScopeToolAdapter {
|
||||
* @return 工具上下文
|
||||
*/
|
||||
private AgentToolContext buildContext(ToolCallParam param) {
|
||||
AgentRuntimeExecutionContext currentRequest = currentRequest();
|
||||
AgentToolContext context = new AgentToolContext();
|
||||
context.setRequestId(request.getRequestId());
|
||||
context.setTraceId(request.getTraceId());
|
||||
context.setSessionId(request.getSessionId());
|
||||
context.setAgentId(request.getAgentDefinition().getAgentId());
|
||||
context.setRuntimeContext(request.getRuntimeContext());
|
||||
context.setRequestId(currentRequest.getRequestId());
|
||||
context.setTraceId(currentRequest.getTraceId());
|
||||
context.setSessionId(currentRequest.getSessionId());
|
||||
context.setAgentId(currentRequest.getAgentDefinition().getAgentId());
|
||||
context.setRuntimeContext(currentRequest.getRuntimeContext());
|
||||
if (param != null && param.getToolUseBlock() != null) {
|
||||
context.setToolCallId(param.getToolUseBlock().getId());
|
||||
}
|
||||
context.setEventEmitter(this::emit);
|
||||
context.getMetadata().put("toolName", toolSpec.getName());
|
||||
context.getMetadata().put("category", toolSpec.getCategory());
|
||||
appendSkillPayload(context.getMetadata(), activeSkillBinding());
|
||||
@@ -241,7 +393,7 @@ public class AgentScopeToolAdapter {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将运行时结果转换为 AgentScope 结果块。
|
||||
* 将 AgentToolResult 转换为 AgentScope 结果块。
|
||||
*
|
||||
* @param param 工具调用参数
|
||||
* @param result 运行时结果
|
||||
@@ -270,38 +422,55 @@ public class AgentScopeToolAdapter {
|
||||
* @return 工具结果块
|
||||
*/
|
||||
private ToolResultBlock invokeTool(ToolCallParam param, Map<String, Object> input) {
|
||||
AgentToolContext context = buildContext(param);
|
||||
AgentToolResult result = invoker.invoke(input, context);
|
||||
ToolResultBlock block = toToolResultBlock(param, result);
|
||||
emit(toolResultEvent(block));
|
||||
return block;
|
||||
}
|
||||
|
||||
/**
|
||||
* 存在 sink 时发射一条事件。
|
||||
*
|
||||
* @param event 事件
|
||||
*/
|
||||
private void emit(AgentRuntimeEvent event) {
|
||||
if (eventSink != null && event != null) {
|
||||
Sinks.EmitResult result = eventSink.tryEmitNext(event);
|
||||
if (result.isFailure()) {
|
||||
throw new AgentRuntimeException("Failed to emit agent runtime event: " + result);
|
||||
Thread currentThread = Thread.currentThread();
|
||||
ClassLoader originalClassLoader = currentThread.getContextClassLoader();
|
||||
boolean switchClassLoader = invocationClassLoader != null
|
||||
&& invocationClassLoader != originalClassLoader;
|
||||
if (switchClassLoader) {
|
||||
currentThread.setContextClassLoader(invocationClassLoader);
|
||||
}
|
||||
try {
|
||||
AgentToolContext context = buildContext(param);
|
||||
AgentToolResult result = invoker.invoke(input, context);
|
||||
ToolResultBlock block = toToolResultBlock(param, result);
|
||||
// 有状态 runtime 中,普通工具结果由 AgentScope 原生 PostActingEvent
|
||||
// 旁路观察器统一发出;旧 sink 辅助路径没有统一 hook,因此仍允许 adapter 兼容发射。
|
||||
if (emitNormalToolResult || (emitSkillStep && activeSkillBinding() != null)) {
|
||||
emit(toolResultEvent(block));
|
||||
}
|
||||
return block;
|
||||
} finally {
|
||||
if (switchClassLoader) {
|
||||
currentThread.setContextClassLoader(originalClassLoader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建工具调用事件。
|
||||
* 通过旁路事件桥发射事件。
|
||||
*
|
||||
* <p>这里发射的是 Easy-Agents 对调用方的监察/交互事件,不是 AgentScope
|
||||
* 主线路消息。主线路的 tool_call/tool_result 顺序仍由 AgentScope 自己维护。</p>
|
||||
*
|
||||
* @param event 事件
|
||||
*/
|
||||
private void emit(AgentRuntimeEvent event) {
|
||||
currentEventBridge().emit(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建工具执行开始事件。
|
||||
* 普通工具调用由 AgentScope stream 的 ToolUseBlock 表达,这里仅为已激活 Skill 发射步骤事件。
|
||||
*
|
||||
* @param block 工具使用块
|
||||
* @return 运行时事件
|
||||
* @return Skill 步骤事件,普通工具返回 null
|
||||
*/
|
||||
private AgentRuntimeEvent toolCallEvent(ToolUseBlock block) {
|
||||
private AgentRuntimeEvent toolExecutionStartEvent(ToolUseBlock block) {
|
||||
AgentSkillBinding activeBinding = activeSkillBinding();
|
||||
AgentRuntimeEvent event = baseEvent(activeBinding == null
|
||||
? AgentRuntimeEventType.TOOL_CALL
|
||||
: AgentRuntimeEventType.SKILL_STEP);
|
||||
if (!emitSkillStep || activeBinding == null) {
|
||||
return null;
|
||||
}
|
||||
AgentRuntimeEvent event = baseEvent(AgentRuntimeEventType.SKILL_STEP);
|
||||
if (block != null) {
|
||||
event.setToolCallId(block.getId());
|
||||
event.getPayload().put("name", block.getName());
|
||||
@@ -359,6 +528,7 @@ public class AgentScopeToolAdapter {
|
||||
* @return 工具审批事件
|
||||
*/
|
||||
private AgentRuntimeEvent toolApprovalRequiredEvent(ToolUseBlock toolUseBlock, AgentPendingState pendingState) {
|
||||
AgentRuntimeExecutionContext currentRequest = currentRequest();
|
||||
AgentRuntimeEvent event = baseEvent(AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED);
|
||||
if (pendingState != null && pendingState.getToolCallId() != null) {
|
||||
event.setToolCallId(pendingState.getToolCallId());
|
||||
@@ -367,8 +537,8 @@ public class AgentScopeToolAdapter {
|
||||
payload.put("resumeToken", pendingState == null || pendingState.getResumeToken() == null
|
||||
? UUID.randomUUID().toString()
|
||||
: pendingState.getResumeToken().getValue());
|
||||
payload.put("sessionId", request.getSessionId());
|
||||
payload.put("agentId", request.getAgentDefinition().getAgentId());
|
||||
payload.put("sessionId", currentRequest.getSessionId());
|
||||
payload.put("agentId", currentRequest.getAgentDefinition().getAgentId());
|
||||
payload.put("approvalPrompt", approvalPrompt(pendingState == null ? null : pendingState.getApprovalPrompt()));
|
||||
payload.put("approvalMetadata", pendingState == null ? new LinkedHashMap<>() : pendingState.getMetadata());
|
||||
payload.put("toolInput", pendingState == null ? new LinkedHashMap<>() : pendingState.getToolInput());
|
||||
@@ -404,11 +574,8 @@ public class AgentScopeToolAdapter {
|
||||
* @return 运行时事件
|
||||
*/
|
||||
private AgentRuntimeEvent baseEvent(AgentRuntimeEventType type) {
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(type);
|
||||
event.setTraceId(request.getTraceId());
|
||||
event.setSessionId(request.getSessionId());
|
||||
event.setAgentId(request.getAgentDefinition().getAgentId());
|
||||
event.getMetadata().put("requestId", request.getRequestId());
|
||||
AgentRuntimeExecutionContext currentRequest = currentRequest();
|
||||
AgentRuntimeEvent event = currentEventBridge().event(type);
|
||||
event.getMetadata().put("toolCategory", toolSpec.getCategory().name());
|
||||
event.getMetadata().put("visibility", toolSpec.getVisibility().name());
|
||||
appendSkillPayload(event.getMetadata(), activeSkillBinding());
|
||||
@@ -427,6 +594,7 @@ public class AgentScopeToolAdapter {
|
||||
}
|
||||
target.put("skillId", binding.getSkillId());
|
||||
target.put("skillName", binding.getSkillName());
|
||||
target.put("skillDisplayName", binding.getSkillDisplayName());
|
||||
target.put("skillBoxId", binding.getSkillBoxId());
|
||||
}
|
||||
|
||||
@@ -455,5 +623,16 @@ public class AgentScopeToolAdapter {
|
||||
Object success = event.getMetadata().get("success");
|
||||
return !(success instanceof Boolean) || Boolean.TRUE.equals(success);
|
||||
}
|
||||
|
||||
private AgentRuntimeExecutionContext currentRequest() {
|
||||
return turnContextHolder == null ? request : turnContextHolder.executionContext(request);
|
||||
}
|
||||
|
||||
private AgentRuntimeEventBridge currentEventBridge() {
|
||||
if (turnContextHolder != null && turnContextHolder.eventBridge().isPresent()) {
|
||||
return turnContextHolder.eventBridge().get();
|
||||
}
|
||||
return new AgentRuntimeEventBridge(request, turnContextHolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.easyagents.agent.runtime.event;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
|
||||
import reactor.core.publisher.Sinks;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* AgentScope 运行时的旁路事件桥。
|
||||
*
|
||||
* <p>该类只负责 Easy-Agents 自己的旁路监察事件,不负责 AgentScope 主线路事件。
|
||||
* 主线路事件来自 {@code agent.stream(...)},会进入模型会话、工具结果和最终输出的正常顺序;
|
||||
* 旁路事件只给调用方观察运行过程,例如知识库检索、自动上下文压缩、工具审批和 Skill 步骤。</p>
|
||||
*
|
||||
* <p>旁路事件不会写入 AgentScope memory/session,也不会修改 AgentScope {@code Msg}。仅作观察与展示使用</p>
|
||||
*/
|
||||
public class AgentRuntimeEventBridge {
|
||||
|
||||
private final AgentRuntimeExecutionContext fallbackContext;
|
||||
private final AgentRuntimeTurnContextHolder turnContextHolder;
|
||||
|
||||
/**
|
||||
* 创建旁路事件桥。
|
||||
*
|
||||
* @param fallbackContext 运行时级上下文,当前轮次未设置时作为身份信息来源
|
||||
* @param turnContextHolder 当前运行轮次上下文持有器
|
||||
*/
|
||||
public AgentRuntimeEventBridge(AgentRuntimeExecutionContext fallbackContext,
|
||||
AgentRuntimeTurnContextHolder turnContextHolder) {
|
||||
this.fallbackContext = fallbackContext;
|
||||
this.turnContextHolder = turnContextHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建固定 sink 的旁路事件桥,主要用于旧测试和旧辅助构造路径。
|
||||
*
|
||||
* @param fallbackContext 运行时级上下文
|
||||
* @param eventSink 旁路事件 sink
|
||||
* @return 旁路事件桥
|
||||
*/
|
||||
public static AgentRuntimeEventBridge fixed(AgentRuntimeExecutionContext fallbackContext,
|
||||
Sinks.Many<AgentRuntimeEvent> eventSink) {
|
||||
AgentRuntimeTurnContextHolder holder = new AgentRuntimeTurnContextHolder();
|
||||
AgentRuntimeEventBridge bridge = new AgentRuntimeEventBridge(fallbackContext, holder);
|
||||
holder.set(new AgentRuntimeTurnContext(null, eventSink, bridge));
|
||||
return bridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建指定类型的旁路事件,并自动补齐公共运行身份信息。
|
||||
*
|
||||
* @param type 旁路事件类型
|
||||
* @return 已补齐公共字段的运行时事件
|
||||
*/
|
||||
public AgentRuntimeEvent event(AgentRuntimeEventType type) {
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(type);
|
||||
enrich(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发射一条旁路事件。
|
||||
*
|
||||
* <p>当前没有运行轮次或没有旁路 sink 时直接忽略。这允许 adapter 在非流式测试、
|
||||
* 初始化阶段或主线路未建立旁路订阅时保持无副作用。</p>
|
||||
*
|
||||
* @param event 旁路事件
|
||||
*/
|
||||
public void emit(AgentRuntimeEvent event) {
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
Optional<Sinks.Many<AgentRuntimeEvent>> sink = eventSink();
|
||||
if (sink.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
enrich(event);
|
||||
Sinks.EmitResult result = sink.get().tryEmitNext(event);
|
||||
if (result.isFailure()) {
|
||||
throw new AgentRuntimeException("Failed to emit agent runtime side event: " + result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前轮次合并后的执行上下文。
|
||||
*
|
||||
* @return 当前执行上下文
|
||||
*/
|
||||
public AgentRuntimeExecutionContext executionContext() {
|
||||
return turnContextHolder == null ? fallbackContext : turnContextHolder.executionContext(fallbackContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 补齐旁路事件的公共身份信息。
|
||||
*
|
||||
* @param event 旁路事件
|
||||
*/
|
||||
public void enrich(AgentRuntimeEvent event) {
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
AgentRuntimeExecutionContext context = executionContext();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
if (event.getTraceId() == null || event.getTraceId().isBlank()) {
|
||||
event.setTraceId(context.getTraceId());
|
||||
}
|
||||
if (event.getSessionId() == null || event.getSessionId().isBlank()) {
|
||||
event.setSessionId(context.getSessionId());
|
||||
}
|
||||
if ((event.getAgentId() == null || event.getAgentId().isBlank())
|
||||
&& context.getAgentDefinition() != null) {
|
||||
event.setAgentId(context.getAgentDefinition().getAgentId());
|
||||
}
|
||||
if (context.getRequestId() != null && !context.getRequestId().isBlank()) {
|
||||
event.getMetadata().putIfAbsent("requestId", context.getRequestId());
|
||||
}
|
||||
Map<String, Object> metadata = context.getMetadata();
|
||||
if (metadata != null && !metadata.isEmpty()) {
|
||||
metadata.forEach(event.getMetadata()::putIfAbsent);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Sinks.Many<AgentRuntimeEvent>> eventSink() {
|
||||
return turnContextHolder == null ? Optional.empty() : turnContextHolder.eventSink();
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,16 @@ public enum AgentRuntimeEventType {
|
||||
*/
|
||||
REASONING_DELTA,
|
||||
|
||||
/**
|
||||
* 智能体开始一次模型推理。
|
||||
*/
|
||||
REASONING_STARTED,
|
||||
|
||||
/**
|
||||
* 智能体完成一次模型推理。
|
||||
*/
|
||||
REASONING_COMPLETED,
|
||||
|
||||
/**
|
||||
* 流式输出内容,用于流式展示聊天内容。
|
||||
*/
|
||||
@@ -29,11 +39,51 @@ public enum AgentRuntimeEventType {
|
||||
*/
|
||||
TOOL_RESULT,
|
||||
|
||||
/**
|
||||
* 异步工具已提交任务。
|
||||
*/
|
||||
ASYNC_TOOL_SUBMITTED,
|
||||
|
||||
/**
|
||||
* 异步工具已观察任务状态。
|
||||
*/
|
||||
ASYNC_TOOL_OBSERVED,
|
||||
|
||||
/**
|
||||
* 异步工具已读取任务结果。
|
||||
*/
|
||||
ASYNC_TOOL_RESULT,
|
||||
|
||||
/**
|
||||
* 异步工具已请求取消任务。
|
||||
*/
|
||||
ASYNC_TOOL_CANCELLED,
|
||||
|
||||
/**
|
||||
* 异步工具已查询任务列表。
|
||||
*/
|
||||
ASYNC_TOOL_LISTED,
|
||||
|
||||
/**
|
||||
* 异步工具执行失败。
|
||||
*/
|
||||
ASYNC_TOOL_FAILED,
|
||||
|
||||
/**
|
||||
* 知识库检索完成并返回文档摘要。
|
||||
*/
|
||||
KNOWLEDGE_RETRIEVAL,
|
||||
|
||||
/**
|
||||
* 自动上下文压缩已开始。
|
||||
*/
|
||||
MEMORY_COMPRESSION_STARTED,
|
||||
|
||||
/**
|
||||
* 自动上下文压缩已完成。
|
||||
*/
|
||||
MEMORY_COMPRESSION_COMPLETED,
|
||||
|
||||
/**
|
||||
* 工具执行前需要人工审批。
|
||||
*/
|
||||
@@ -64,6 +114,11 @@ public enum AgentRuntimeEventType {
|
||||
*/
|
||||
COMPLETED,
|
||||
|
||||
/**
|
||||
* 智能体运行已暂停,等待外部输入后继续。
|
||||
*/
|
||||
SUSPENDED,
|
||||
|
||||
/**
|
||||
* 智能体运行失败。
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.easyagents.agent.runtime.event;
|
||||
|
||||
import io.agentscope.core.hook.HookEvent;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* AgentScope Hook 事件的主线路干预器。对接 AgentScope 的原生生命周期hook
|
||||
*
|
||||
* <p>干预器允许修改 AgentScope HookEvent,因此会影响主线路执行。
|
||||
* 典型场景包括 AutoContext 在推理前改写输入消息,或后续 HITL 在推理后调用
|
||||
* {@code stopAgent()} 暂停工具执行。普通运行状态通知应使用 {@link AgentRuntimeObserver}。</p>
|
||||
* <p>警告:本干预器会影响到主线路agent 交互,谨慎使用
|
||||
* </p>
|
||||
*/
|
||||
public interface AgentRuntimeInterceptor {
|
||||
|
||||
/**
|
||||
* 处理并返回可能被修改的 AgentScope Hook 事件。
|
||||
*
|
||||
* @param event AgentScope Hook 事件
|
||||
* @param <T> Hook 事件类型
|
||||
* @return 处理后的 Hook 事件
|
||||
*/
|
||||
<T extends HookEvent> Mono<T> intercept(T event);
|
||||
|
||||
/**
|
||||
* 获取干预器执行优先级。
|
||||
*
|
||||
* @return 优先级,数值越小越先执行
|
||||
*/
|
||||
default int priority() {
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.easyagents.agent.runtime.event;
|
||||
|
||||
import io.agentscope.core.hook.HookEvent;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AgentScope Hook 事件的统一观察和干预调度器。
|
||||
*
|
||||
* <p>处理顺序固定为:先执行干预器,再执行观察器。干预器属于主线路能力,
|
||||
* 可以修改 AgentScope HookEvent;观察器属于旁线路能力,只能查看事件并通过
|
||||
* {@link AgentRuntimeEventBridge} 发射对外监察事件。</p>
|
||||
*/
|
||||
public class AgentRuntimeObservationManager {
|
||||
|
||||
private final List<AgentRuntimeInterceptor> interceptors;
|
||||
private final List<AgentRuntimeObserver> observers;
|
||||
|
||||
/**
|
||||
* 创建观察调度器。
|
||||
*
|
||||
* @param interceptors 主线路干预器
|
||||
* @param observers 旁路观察器
|
||||
*/
|
||||
public AgentRuntimeObservationManager(List<AgentRuntimeInterceptor> interceptors,
|
||||
List<AgentRuntimeObserver> observers) {
|
||||
this.interceptors = sortInterceptors(interceptors);
|
||||
this.observers = sortObservers(observers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建空观察调度器。
|
||||
*
|
||||
* @return 空调度器
|
||||
*/
|
||||
public static AgentRuntimeObservationManager empty() {
|
||||
return new AgentRuntimeObservationManager(List.of(), List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 AgentScope Hook 事件。
|
||||
*
|
||||
* @param event Hook 事件
|
||||
* @param <T> Hook 事件类型
|
||||
* @return 处理后的 Hook 事件
|
||||
*/
|
||||
public <T extends HookEvent> Mono<T> handle(T event) {
|
||||
Mono<T> chain = Mono.just(event);
|
||||
for (AgentRuntimeInterceptor interceptor : interceptors) {
|
||||
chain = chain.flatMap(interceptor::intercept);
|
||||
}
|
||||
for (AgentRuntimeObserver observer : observers) {
|
||||
chain = chain.flatMap(current -> observer.observe(current).thenReturn(current));
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
private List<AgentRuntimeInterceptor> sortInterceptors(List<AgentRuntimeInterceptor> source) {
|
||||
List<AgentRuntimeInterceptor> sorted = new ArrayList<>(source == null ? List.of() : source);
|
||||
sorted.sort(Comparator.comparingInt(AgentRuntimeInterceptor::priority));
|
||||
return List.copyOf(sorted);
|
||||
}
|
||||
|
||||
private List<AgentRuntimeObserver> sortObservers(List<AgentRuntimeObserver> source) {
|
||||
List<AgentRuntimeObserver> sorted = new ArrayList<>(source == null ? List.of() : source);
|
||||
sorted.sort(Comparator.comparingInt(AgentRuntimeObserver::priority));
|
||||
return List.copyOf(sorted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.easyagents.agent.runtime.event;
|
||||
|
||||
import io.agentscope.core.hook.HookEvent;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* AgentScope Hook 事件的旁路观察器。对接 AgentScope 的原生生命周期hook
|
||||
*
|
||||
* <p>观察器只做监察和旁路事件发射,不允许修改 AgentScope HookEvent。
|
||||
* 如果能力需要影响主线路,例如修改输入消息、暂停 agent 或替换工具结果,应实现
|
||||
* {@link AgentRuntimeInterceptor}。</p>
|
||||
*/
|
||||
public interface AgentRuntimeObserver {
|
||||
|
||||
/**
|
||||
* 观察 AgentScope Hook 事件。
|
||||
*
|
||||
* @param event AgentScope Hook 事件
|
||||
* @return 完成信号
|
||||
*/
|
||||
Mono<Void> observe(HookEvent event);
|
||||
|
||||
/**
|
||||
* 获取观察器执行优先级。
|
||||
*
|
||||
* @return 优先级,数值越小越先执行
|
||||
*/
|
||||
default int priority() {
|
||||
return 100;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.easyagents.agent.runtime.event;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
|
||||
import reactor.core.publisher.Sinks;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AgentScope 运行时的单轮执行上下文。
|
||||
*/
|
||||
public class AgentRuntimeTurnContext {
|
||||
|
||||
/**
|
||||
* 本轮运行上下文。
|
||||
*/
|
||||
private final AgentRuntimeExecutionContext executionContext;
|
||||
|
||||
/**
|
||||
* 本轮旁路事件 sink。
|
||||
*
|
||||
* <p>该字段只承载旁线路监察事件,不承载 AgentScope 主线路 stream 事件。</p>
|
||||
*/
|
||||
private final Sinks.Many<AgentRuntimeEvent> eventSink;
|
||||
|
||||
/**
|
||||
* 本轮旁路事件桥。
|
||||
*
|
||||
* <p>adapter 和 observer 应优先通过 bridge 发射旁路事件,避免各处重复拼接
|
||||
* trace/session/request 等公共字段。</p>
|
||||
*/
|
||||
private final AgentRuntimeEventBridge eventBridge;
|
||||
|
||||
/**
|
||||
* 创建单轮执行上下文。
|
||||
*
|
||||
* @param executionContext 本轮运行上下文
|
||||
* @param eventSink 本轮旁路事件 sink
|
||||
*/
|
||||
public AgentRuntimeTurnContext(AgentRuntimeExecutionContext executionContext,
|
||||
Sinks.Many<AgentRuntimeEvent> eventSink) {
|
||||
this(executionContext, eventSink, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单轮执行上下文。
|
||||
*
|
||||
* @param executionContext 本轮运行上下文
|
||||
* @param eventSink 本轮旁路事件 sink
|
||||
* @param eventBridge 本轮旁路事件桥
|
||||
*/
|
||||
public AgentRuntimeTurnContext(AgentRuntimeExecutionContext executionContext,
|
||||
Sinks.Many<AgentRuntimeEvent> eventSink,
|
||||
AgentRuntimeEventBridge eventBridge) {
|
||||
this.executionContext = executionContext;
|
||||
this.eventSink = eventSink;
|
||||
this.eventBridge = eventBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本轮运行上下文。
|
||||
*
|
||||
* @return 本轮运行上下文
|
||||
*/
|
||||
public AgentRuntimeExecutionContext getExecutionContext() {
|
||||
return executionContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本轮旁路事件 sink。
|
||||
*
|
||||
* @return 本轮旁路事件 sink
|
||||
*/
|
||||
public Sinks.Many<AgentRuntimeEvent> getEventSink() {
|
||||
return eventSink;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本轮旁路事件桥。
|
||||
*
|
||||
* @return 本轮旁路事件桥
|
||||
*/
|
||||
public AgentRuntimeEventBridge getEventBridge() {
|
||||
return eventBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建以当前轮信息覆盖运行时信息后的上下文。
|
||||
*
|
||||
* @param fallback 运行时级上下文
|
||||
* @return 当前轮上下文,未设置时返回运行时级上下文
|
||||
*/
|
||||
public AgentRuntimeExecutionContext mergeWith(AgentRuntimeExecutionContext fallback) {
|
||||
if (executionContext == null) {
|
||||
return fallback;
|
||||
}
|
||||
AgentRuntimeExecutionContext merged = new AgentRuntimeExecutionContext();
|
||||
merged.setRequestId(firstNonBlank(executionContext.getRequestId(), fallback == null ? null : fallback.getRequestId()));
|
||||
merged.setTraceId(firstNonBlank(executionContext.getTraceId(), fallback == null ? null : fallback.getTraceId()));
|
||||
merged.setSessionId(firstNonBlank(executionContext.getSessionId(), fallback == null ? null : fallback.getSessionId()));
|
||||
merged.setAgentDefinition(executionContext.getAgentDefinition() == null && fallback != null
|
||||
? fallback.getAgentDefinition()
|
||||
: executionContext.getAgentDefinition());
|
||||
merged.setRuntimeContext(executionContext.getRuntimeContext() == null && fallback != null
|
||||
? fallback.getRuntimeContext()
|
||||
: executionContext.getRuntimeContext());
|
||||
merged.setUserMessage(executionContext.getUserMessage());
|
||||
merged.setMemorySnapshot(executionContext.getMemorySnapshot());
|
||||
merged.setToolInvokers(fallback == null ? executionContext.getToolInvokers() : fallback.getToolInvokers());
|
||||
merged.setKnowledgeRegistrations(fallback == null
|
||||
? executionContext.getKnowledgeRegistrations()
|
||||
: fallback.getKnowledgeRegistrations());
|
||||
merged.setSessionStore(fallback == null ? executionContext.getSessionStore() : fallback.getSessionStore());
|
||||
merged.setConversationRecorder(fallback == null
|
||||
? executionContext.getConversationRecorder()
|
||||
: fallback.getConversationRecorder());
|
||||
merged.setMetadata(mergedMetadata(fallback, executionContext));
|
||||
merged.setCancelReason(executionContext.getCancelReason());
|
||||
return merged;
|
||||
}
|
||||
|
||||
private Map<String, Object> mergedMetadata(AgentRuntimeExecutionContext fallback,
|
||||
AgentRuntimeExecutionContext current) {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
if (fallback != null && fallback.getMetadata() != null) {
|
||||
metadata.putAll(fallback.getMetadata());
|
||||
}
|
||||
if (current != null && current.getMetadata() != null) {
|
||||
metadata.putAll(current.getMetadata());
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private String firstNonBlank(String first, String second) {
|
||||
return first == null || first.isBlank() ? second : first;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.easyagents.agent.runtime.event;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
|
||||
import reactor.core.publisher.Sinks;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* 保存当前运行轮次上下文。
|
||||
*/
|
||||
public class AgentRuntimeTurnContextHolder {
|
||||
|
||||
private final AtomicReference<AgentRuntimeTurnContext> current = new AtomicReference<>();
|
||||
|
||||
/**
|
||||
* 设置当前运行轮次上下文。
|
||||
*
|
||||
* @param context 当前轮次上下文
|
||||
*/
|
||||
public void set(AgentRuntimeTurnContext context) {
|
||||
current.set(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理当前运行轮次上下文。
|
||||
*/
|
||||
public void clear() {
|
||||
current.set(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前运行轮次上下文。
|
||||
*
|
||||
* @return 当前轮次上下文
|
||||
*/
|
||||
public Optional<AgentRuntimeTurnContext> current() {
|
||||
return Optional.ofNullable(current.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前轮次事件 sink。
|
||||
*
|
||||
* @return 当前轮次旁路事件 sink
|
||||
*/
|
||||
public Optional<Sinks.Many<AgentRuntimeEvent>> eventSink() {
|
||||
return current()
|
||||
.map(AgentRuntimeTurnContext::getEventSink)
|
||||
.filter(sink -> sink != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前轮次旁路事件桥。
|
||||
*
|
||||
* @return 当前轮次旁路事件桥
|
||||
*/
|
||||
public Optional<AgentRuntimeEventBridge> eventBridge() {
|
||||
return current()
|
||||
.map(AgentRuntimeTurnContext::getEventBridge)
|
||||
.filter(bridge -> bridge != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并当前轮次上下文和运行时级上下文。
|
||||
*
|
||||
* @param fallback 运行时级上下文
|
||||
* @return 合并后的上下文
|
||||
*/
|
||||
public AgentRuntimeExecutionContext executionContext(AgentRuntimeExecutionContext fallback) {
|
||||
return current()
|
||||
.map(context -> context.mergeWith(fallback))
|
||||
.orElse(fallback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
package com.easyagents.agent.runtime.event.interceptor;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeInterceptor;
|
||||
import io.agentscope.core.ReActAgent;
|
||||
import io.agentscope.core.agent.Agent;
|
||||
import io.agentscope.core.hook.HookEvent;
|
||||
import io.agentscope.core.hook.PreCallEvent;
|
||||
import io.agentscope.core.hook.PreReasoningEvent;
|
||||
import io.agentscope.core.memory.Memory;
|
||||
import io.agentscope.core.memory.autocontext.*;
|
||||
import io.agentscope.core.message.Msg;
|
||||
import io.agentscope.core.message.MsgRole;
|
||||
import io.agentscope.core.message.TextBlock;
|
||||
import io.agentscope.core.plan.PlanNotebook;
|
||||
import io.agentscope.core.tool.Toolkit;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* AutoContext 主线路干预器。
|
||||
*
|
||||
* <p><strong>特殊约束:</strong>本干预器是对 AgentScope 官方
|
||||
* {@code AutoContextHook} 的替代实现,不能与官方 {@code AutoContextHook} 同时注册。
|
||||
* 两者同时存在会导致 {@link AutoContextMemory#compressIfNeeded()}、上下文重写以及
|
||||
* {@link ContextOffloadTool} 注册被重复执行。</p>
|
||||
*
|
||||
* <p>本类承担两类职责。第一类是主线路干预:在 {@link PreCallEvent} 中注册
|
||||
* AutoContext 工具能力,在 {@link PreReasoningEvent} 中触发记忆压缩并改写
|
||||
* LLM 输入消息。第二类是旁路通知:通过 {@link AgentRuntimeEventBridge} 发出
|
||||
* {@link AgentRuntimeEventType#MEMORY_COMPRESSION_STARTED} 和
|
||||
* {@link AgentRuntimeEventType#MEMORY_COMPRESSION_COMPLETED},这些事件只用于调用方展示,
|
||||
* 不写入 AgentScope memory/session,也不参与 LLM 会话协议。</p>
|
||||
*/
|
||||
public class AutoContextInterceptor implements AgentRuntimeInterceptor {
|
||||
|
||||
private static final String STATUS_KEY = "memory-compression";
|
||||
private static final String AUTO_CONTEXT_SYSTEM_INSTRUCTION =
|
||||
"You may see compressed messages containing <!-- CONTEXT_OFFLOAD uuid=... -->.\n"
|
||||
+ "- Use the UUID to call context_reload if you need full details.\n"
|
||||
+ "- NEVER mention, quote, or refer to UUIDs, offload tags, or internal metadata in your response.";
|
||||
|
||||
private final AgentRuntimeEventBridge eventBridge;
|
||||
private final AutoContextConfig autoContextConfig;
|
||||
private final AtomicBoolean registered = new AtomicBoolean(false);
|
||||
|
||||
/**
|
||||
* 创建 AutoContext 主线路干预器。
|
||||
*
|
||||
* <p>传入的 {@link AutoContextConfig} 必须是创建目标 {@link AutoContextMemory}
|
||||
* 时使用的同一份配置。这样压缩开始事件的触发条件才能与 AgentScope
|
||||
* {@code compressIfNeeded()} 的入口判断保持一致。</p>
|
||||
*
|
||||
* @param eventBridge 旁路事件桥
|
||||
* @param autoContextConfig AutoContext 配置
|
||||
*/
|
||||
public AutoContextInterceptor(AgentRuntimeEventBridge eventBridge,
|
||||
AutoContextConfig autoContextConfig) {
|
||||
this.eventBridge = eventBridge;
|
||||
this.autoContextConfig = autoContextConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 AutoContext 相关 AgentScope Hook 事件。
|
||||
*
|
||||
* @param event AgentScope Hook 事件
|
||||
* @param <T> Hook 事件类型
|
||||
* @return 处理后的 Hook 事件
|
||||
*/
|
||||
@Override
|
||||
public <T extends HookEvent> Mono<T> intercept(T event) {
|
||||
if (event instanceof PreCallEvent preCallEvent) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Mono<T> result = (Mono<T>) handlePreCall(preCallEvent);
|
||||
return result;
|
||||
}
|
||||
if (event instanceof PreReasoningEvent preReasoningEvent) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Mono<T> result = (Mono<T>) handlePreReasoning(preReasoningEvent);
|
||||
return result;
|
||||
}
|
||||
return Mono.just(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取干预器优先级。
|
||||
*
|
||||
* @return 优先级,保持与官方 AutoContextHook 一致
|
||||
*/
|
||||
@Override
|
||||
public int priority() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 AutoContext 工具集成是否已经注册。
|
||||
*
|
||||
* @return 已注册时返回 true
|
||||
*/
|
||||
public boolean isRegistered() {
|
||||
return registered.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理调用前事件,注册 AutoContext 的上下文重载工具和计划本。
|
||||
*
|
||||
* @param event 调用前事件
|
||||
* @return 原事件
|
||||
*/
|
||||
private Mono<PreCallEvent> handlePreCall(PreCallEvent event) {
|
||||
if (registered.get()) {
|
||||
return Mono.just(event);
|
||||
}
|
||||
Agent agent = event.getAgent();
|
||||
if (!(agent instanceof ReActAgent reActAgent)) {
|
||||
return Mono.just(event);
|
||||
}
|
||||
Memory memory = reActAgent.getMemory();
|
||||
if (!(memory instanceof AutoContextMemory autoContextMemory)) {
|
||||
return Mono.just(event);
|
||||
}
|
||||
if (!registered.compareAndSet(false, true)) {
|
||||
return Mono.just(event);
|
||||
}
|
||||
try {
|
||||
Toolkit toolkit = reActAgent.getToolkit();
|
||||
if (toolkit != null) {
|
||||
toolkit.registerTool(new ContextOffloadTool(autoContextMemory));
|
||||
}
|
||||
PlanNotebook planNotebook = reActAgent.getPlanNotebook();
|
||||
if (planNotebook != null) {
|
||||
autoContextMemory.attachPlanNote(planNotebook);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
registered.set(false);
|
||||
throw new AgentRuntimeException("Failed to register AutoContext integration.", e);
|
||||
}
|
||||
return Mono.just(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理推理前事件,触发 AutoContext 压缩并改写 LLM 输入消息。
|
||||
*
|
||||
* <p>这是主线路干预逻辑:{@code compressIfNeeded()} 和 {@code setInputMessages(...)}
|
||||
* 会影响 AgentScope 本次 reasoning 输入。压缩开始/完成事件则是旁路通知,只发给调用方。</p>
|
||||
*
|
||||
* @param event 推理前事件
|
||||
* @return 改写输入消息后的事件
|
||||
*/
|
||||
private Mono<PreReasoningEvent> handlePreReasoning(PreReasoningEvent event) {
|
||||
Agent agent = event.getAgent();
|
||||
if (!(agent instanceof ReActAgent reActAgent)) {
|
||||
return Mono.just(event);
|
||||
}
|
||||
Memory memory = reActAgent.getMemory();
|
||||
if (!(memory instanceof AutoContextMemory autoContextMemory)) {
|
||||
return Mono.just(event);
|
||||
}
|
||||
// 判断是否达到压缩条件
|
||||
CompressionCheck compressionCheck = compressionCheck(autoContextMemory.getMessages());
|
||||
int beforeEventCount = compressionEventCount(autoContextMemory);
|
||||
if (compressionCheck.thresholdReached()) {
|
||||
emitCompressionStarted(compressionCheck);
|
||||
boolean compressed = autoContextMemory.compressIfNeeded();
|
||||
List<CompressionEvent> newEvents = newCompressionEvents(autoContextMemory, beforeEventCount);
|
||||
emitCompressionCompleted(compressionCheck, newEvents, compressed && !newEvents.isEmpty());
|
||||
}
|
||||
// 压缩完毕后自动进行当前的会话
|
||||
event.setInputMessages(buildInputMessages(event, autoContextMemory));
|
||||
return Mono.just(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算当前记忆是否达到 AutoContext 官方压缩入口条件。
|
||||
*
|
||||
* @param messages 当前工作记忆消息
|
||||
* @return 压缩入口检查结果
|
||||
*/
|
||||
private CompressionCheck compressionCheck(List<Msg> messages) {
|
||||
List<Msg> safeMessages = messages == null ? List.of() : messages;
|
||||
int messageCount = safeMessages.size();
|
||||
int tokenCount = TokenCounterUtil.calculateToken(safeMessages);
|
||||
int thresholdMessageCount = autoContextConfig == null ? Integer.MAX_VALUE : autoContextConfig.getMsgThreshold();
|
||||
long thresholdTokenCount = autoContextConfig == null
|
||||
? Long.MAX_VALUE
|
||||
: (long) (autoContextConfig.getMaxToken() * autoContextConfig.getTokenRatio());
|
||||
boolean messageThresholdReached = messageCount >= thresholdMessageCount;
|
||||
boolean tokenThresholdReached = tokenCount >= thresholdTokenCount;
|
||||
return new CompressionCheck(messageCount, tokenCount, thresholdMessageCount, thresholdTokenCount,
|
||||
messageThresholdReached, tokenThresholdReached);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前压缩事件数量。
|
||||
*
|
||||
* @param autoContextMemory AutoContext 记忆
|
||||
* @return 压缩事件数量
|
||||
*/
|
||||
private int compressionEventCount(AutoContextMemory autoContextMemory) {
|
||||
List<CompressionEvent> events = autoContextMemory.getCompressionEvents();
|
||||
return events == null ? 0 : events.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本次压缩新增的 AgentScope 压缩事件。
|
||||
*
|
||||
* @param autoContextMemory AutoContext 记忆
|
||||
* @param beforeEventCount 压缩前事件数量
|
||||
* @return 新增压缩事件
|
||||
*/
|
||||
private List<CompressionEvent> newCompressionEvents(AutoContextMemory autoContextMemory, int beforeEventCount) {
|
||||
List<CompressionEvent> events = autoContextMemory.getCompressionEvents();
|
||||
if (events == null || events.size() <= beforeEventCount) {
|
||||
return List.of();
|
||||
}
|
||||
return new ArrayList<>(events.subList(beforeEventCount, events.size()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 AutoContext 压缩后传给 LLM 的输入消息。
|
||||
*
|
||||
* @param event 推理前事件
|
||||
* @param autoContextMemory AutoContext 记忆
|
||||
* @return 更新后的输入消息
|
||||
*/
|
||||
private List<Msg> buildInputMessages(PreReasoningEvent event, AutoContextMemory autoContextMemory) {
|
||||
List<Msg> originalInputMessages = event.getInputMessages();
|
||||
List<Msg> newInputMessages = new ArrayList<>();
|
||||
if (!originalInputMessages.isEmpty() && originalInputMessages.get(0).getRole() == MsgRole.SYSTEM) {
|
||||
Msg originalSystemMsg = originalInputMessages.get(0);
|
||||
String originalSystemText = originalSystemMsg.getTextContent();
|
||||
String newSystemText = originalSystemText != null
|
||||
? originalSystemText + "\n\n" + AUTO_CONTEXT_SYSTEM_INSTRUCTION
|
||||
: AUTO_CONTEXT_SYSTEM_INSTRUCTION;
|
||||
newInputMessages.add(Msg.builder()
|
||||
.role(MsgRole.SYSTEM)
|
||||
.name(originalSystemMsg.getName())
|
||||
.content(TextBlock.builder().text(newSystemText).build())
|
||||
.metadata(originalSystemMsg.getMetadata())
|
||||
.build());
|
||||
} else {
|
||||
newInputMessages.add(Msg.builder()
|
||||
.role(MsgRole.SYSTEM)
|
||||
.name("system")
|
||||
.content(TextBlock.builder().text(AUTO_CONTEXT_SYSTEM_INSTRUCTION).build())
|
||||
.build());
|
||||
}
|
||||
newInputMessages.addAll(autoContextMemory.getMessages());
|
||||
return newInputMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发射上下文压缩开始旁路事件。
|
||||
*
|
||||
* @param compressionCheck 压缩入口检查结果
|
||||
*/
|
||||
private void emitCompressionStarted(CompressionCheck compressionCheck) {
|
||||
AgentRuntimeEvent event = eventBridge.event(AgentRuntimeEventType.MEMORY_COMPRESSION_STARTED);
|
||||
event.getPayload().put("statusKey", STATUS_KEY);
|
||||
event.getPayload().put("phase", "started");
|
||||
event.getPayload().put("status", "running");
|
||||
event.getPayload().put("label", "正在整理上下文");
|
||||
putCompressionCheckPayload(event, compressionCheck);
|
||||
eventBridge.emit(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发射上下文压缩完成旁路事件。
|
||||
*
|
||||
* @param compressionCheck 压缩入口检查结果
|
||||
* @param events 新增压缩事件
|
||||
*/
|
||||
private void emitCompressionCompleted(CompressionCheck compressionCheck,
|
||||
List<CompressionEvent> events,
|
||||
boolean compressed) {
|
||||
AgentRuntimeEvent event = eventBridge.event(AgentRuntimeEventType.MEMORY_COMPRESSION_COMPLETED);
|
||||
event.getPayload().put("statusKey", STATUS_KEY);
|
||||
event.getPayload().put("phase", "completed");
|
||||
event.getPayload().put("status", "done");
|
||||
event.getPayload().put("label", compressed ? "已整理上下文" : "无需压缩上下文");
|
||||
event.getPayload().put("compressed", compressed);
|
||||
event.getPayload().put("eventCount", events == null ? 0 : events.size());
|
||||
event.getPayload().put("events", toPayloadEvents(events));
|
||||
putCompressionCheckPayload(event, compressionCheck);
|
||||
eventBridge.emit(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充压缩入口判断相关载荷。
|
||||
*
|
||||
* @param event 运行时事件
|
||||
* @param compressionCheck 压缩入口检查结果
|
||||
*/
|
||||
private void putCompressionCheckPayload(AgentRuntimeEvent event, CompressionCheck compressionCheck) {
|
||||
event.getPayload().put("messageCount", compressionCheck.messageCount());
|
||||
event.getPayload().put("tokenCount", compressionCheck.tokenCount());
|
||||
event.getPayload().put("thresholdMessageCount", compressionCheck.thresholdMessageCount());
|
||||
event.getPayload().put("thresholdTokenCount", compressionCheck.thresholdTokenCount());
|
||||
event.getPayload().put("messageThresholdReached", compressionCheck.messageThresholdReached());
|
||||
event.getPayload().put("tokenThresholdReached", compressionCheck.tokenThresholdReached());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 AgentScope 压缩事件转换为旁路事件载荷。
|
||||
*
|
||||
* @param events AgentScope 压缩事件
|
||||
* @return 压缩事件载荷
|
||||
*/
|
||||
private List<Map<String, Object>> toPayloadEvents(List<CompressionEvent> events) {
|
||||
if (events == null || events.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<Map<String, Object>> payloadEvents = new ArrayList<>();
|
||||
for (CompressionEvent event : events) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("eventType", event.getEventType());
|
||||
payload.put("timestamp", event.getTimestamp());
|
||||
payload.put("compressedMessageCount", event.getCompressedMessageCount());
|
||||
payload.put("previousMessageId", event.getPreviousMessageId());
|
||||
payload.put("nextMessageId", event.getNextMessageId());
|
||||
payload.put("compressedMessageId", event.getCompressedMessageId());
|
||||
payload.put("tokenBefore", event.getTokenBefore());
|
||||
payload.put("tokenAfter", event.getTokenAfter());
|
||||
payload.put("tokenReduction", event.getTokenReduction());
|
||||
payload.put("inputToken", event.getCompressInputToken());
|
||||
payload.put("outputToken", event.getCompressOutputToken());
|
||||
payloadEvents.add(payload);
|
||||
}
|
||||
return payloadEvents;
|
||||
}
|
||||
|
||||
private record CompressionCheck(int messageCount,
|
||||
int tokenCount,
|
||||
int thresholdMessageCount,
|
||||
long thresholdTokenCount,
|
||||
boolean messageThresholdReached,
|
||||
boolean tokenThresholdReached) {
|
||||
|
||||
/**
|
||||
* 判断是否达到 AutoContext 官方压缩入口条件。
|
||||
*
|
||||
* @return 达到任一阈值时返回 true
|
||||
*/
|
||||
private boolean thresholdReached() {
|
||||
return messageThresholdReached || tokenThresholdReached;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package com.easyagents.agent.runtime.event.interceptor;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import com.easyagents.agent.runtime.agentscope.AgentScopeMessageAdapter;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeInterceptor;
|
||||
import com.easyagents.agent.runtime.media.AgentMediaResolver;
|
||||
import com.easyagents.agent.runtime.media.AgentMediaResource;
|
||||
import io.agentscope.core.hook.HookEvent;
|
||||
import io.agentscope.core.hook.PreReasoningEvent;
|
||||
import io.agentscope.core.message.*;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 在推理前将持久化的业务媒体引用解析为模型可用的 Base64 内容。
|
||||
*
|
||||
* <p>输入消息会被深拷贝,AgentScope memory/session 中仍保留小体积稳定引用。</p>
|
||||
*/
|
||||
public class MediaReferenceInterceptor implements AgentRuntimeInterceptor {
|
||||
|
||||
private final AgentMediaResolver mediaResolver;
|
||||
|
||||
/**
|
||||
* 创建媒体引用干预器。
|
||||
*
|
||||
* @param mediaResolver 媒体引用解析器
|
||||
*/
|
||||
public MediaReferenceInterceptor(AgentMediaResolver mediaResolver) {
|
||||
this.mediaResolver = mediaResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析推理输入中的内部媒体引用。
|
||||
*
|
||||
* @param event AgentScope Hook 事件
|
||||
* @param <T> Hook 事件类型
|
||||
* @return 处理后的事件
|
||||
*/
|
||||
@Override
|
||||
public <T extends HookEvent> Mono<T> intercept(T event) {
|
||||
if (event instanceof PreReasoningEvent preReasoningEvent) {
|
||||
preReasoningEvent.setInputMessages(resolveMessages(preReasoningEvent.getInputMessages()));
|
||||
}
|
||||
return Mono.just(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 AutoContext 完成消息重写后执行媒体解析。
|
||||
*
|
||||
* @return 执行优先级
|
||||
*/
|
||||
@Override
|
||||
public int priority() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
private List<Msg> resolveMessages(List<Msg> messages) {
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<Msg> resolved = new ArrayList<>(messages.size());
|
||||
for (Msg message : messages) {
|
||||
List<ContentBlock> content = resolveBlocks(message.getContent());
|
||||
resolved.add(Msg.builder()
|
||||
.id(message.getId())
|
||||
.name(message.getName())
|
||||
.role(message.getRole())
|
||||
.content(content)
|
||||
.metadata(message.getMetadata())
|
||||
.timestamp(message.getTimestamp())
|
||||
.build());
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private List<ContentBlock> resolveBlocks(List<ContentBlock> blocks) {
|
||||
if (blocks == null || blocks.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<ContentBlock> resolved = new ArrayList<>(blocks.size());
|
||||
for (ContentBlock block : blocks) {
|
||||
if (block instanceof ImageBlock imageBlock) {
|
||||
resolved.add(resolveImage(imageBlock));
|
||||
} else if (block instanceof AudioBlock audioBlock) {
|
||||
resolved.add(resolveAudio(audioBlock));
|
||||
} else if (block instanceof VideoBlock videoBlock) {
|
||||
resolved.add(resolveVideo(videoBlock));
|
||||
} else {
|
||||
resolved.add(block);
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private ImageBlock resolveImage(ImageBlock block) {
|
||||
AgentMediaResource resource = resolve(block.getSource());
|
||||
if (resource == null) {
|
||||
return block;
|
||||
}
|
||||
return ImageBlock.builder()
|
||||
.source(base64Source(resource))
|
||||
.minPixels(block.getMinPixels())
|
||||
.maxPixels(block.getMaxPixels())
|
||||
.build();
|
||||
}
|
||||
|
||||
private AudioBlock resolveAudio(AudioBlock block) {
|
||||
AgentMediaResource resource = resolve(block.getSource());
|
||||
return resource == null ? block : AudioBlock.builder().source(base64Source(resource)).build();
|
||||
}
|
||||
|
||||
private VideoBlock resolveVideo(VideoBlock block) {
|
||||
AgentMediaResource resource = resolve(block.getSource());
|
||||
if (resource == null) {
|
||||
return block;
|
||||
}
|
||||
return VideoBlock.builder()
|
||||
.source(base64Source(resource))
|
||||
.fps(block.getFps())
|
||||
.maxFrames(block.getMaxFrames())
|
||||
.minPixels(block.getMinPixels())
|
||||
.maxPixels(block.getMaxPixels())
|
||||
.totalPixels(block.getTotalPixels())
|
||||
.build();
|
||||
}
|
||||
|
||||
private AgentMediaResource resolve(Source source) {
|
||||
if (!(source instanceof URLSource urlSource)
|
||||
|| urlSource.getUrl() == null
|
||||
|| !urlSource.getUrl().startsWith(AgentScopeMessageAdapter.MEDIA_REFERENCE_SCHEME)) {
|
||||
return null;
|
||||
}
|
||||
if (mediaResolver == null) {
|
||||
throw new AgentRuntimeException("Agent media resolver is required for media references.");
|
||||
}
|
||||
String reference;
|
||||
try {
|
||||
reference = AgentScopeMessageAdapter.decodeReference(urlSource.getUrl());
|
||||
} catch (IllegalArgumentException error) {
|
||||
throw new AgentRuntimeException("Agent media reference is invalid.", error);
|
||||
}
|
||||
AgentMediaResource resource = mediaResolver.resolve(reference);
|
||||
if (resource == null || resource.bytes().length == 0 || resource.mimeType() == null
|
||||
|| resource.mimeType().isBlank()) {
|
||||
throw new AgentRuntimeException("Agent media resource is empty.");
|
||||
}
|
||||
return resource;
|
||||
}
|
||||
|
||||
private Base64Source base64Source(AgentMediaResource resource) {
|
||||
return Base64Source.builder()
|
||||
.mediaType(resource.mimeType())
|
||||
.data(Base64.getEncoder().encodeToString(resource.bytes()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,634 @@
|
||||
package com.easyagents.agent.runtime.event.interceptor;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeInterceptor;
|
||||
import com.easyagents.agent.runtime.hitl.AgentPendingState;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalEvaluation;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalCoordinator;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||
import io.agentscope.core.hook.HookEvent;
|
||||
import io.agentscope.core.hook.PostReasoningEvent;
|
||||
import io.agentscope.core.hook.PreActingEvent;
|
||||
import io.agentscope.core.message.ContentBlock;
|
||||
import io.agentscope.core.message.Msg;
|
||||
import io.agentscope.core.message.ToolUseBlock;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 工具 HITL 主线路干预器。
|
||||
*
|
||||
* <p>本 interceptor 专门处理“工具执行前人工审批”。通过 AgentScope 原生
|
||||
* {@link PostReasoningEvent} 建立审批批次,并在 {@link PreActingEvent} 消费一次性执行授权。</p>
|
||||
*
|
||||
* <p>这里包含三类动作:
|
||||
* <ul>
|
||||
* <li>主线路干预:发现待审批工具后调用 {@link PostReasoningEvent#stopAgent()},
|
||||
* 让 AgentScope 返回当前带 ToolUseBlock 的消息并暂停工具执行。</li>
|
||||
* <li>执行前校验:按工具调用身份消费一次性执行授权,阻止未批准或被篡改的调用。</li>
|
||||
* <li>旁路交互事件:通过 {@link AgentRuntimeEventBridge} 发出
|
||||
* {@link AgentRuntimeEventType#TOOL_APPROVAL_REQUIRED},通知调用方展示审批交互。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>注意:本 interceptor 不执行工具。后续 resume 流程应基于 AgentScope pending tool
|
||||
* 状态继续调用 agent stream/call,实际工具执行仍由 AgentScope Toolkit 完成。</p>
|
||||
*/
|
||||
public class ToolHitlInterceptor implements AgentRuntimeInterceptor {
|
||||
|
||||
private final AgentRuntimeEventBridge eventBridge;
|
||||
private final AgentToolApprovalCoordinator approvalCoordinator;
|
||||
private final Map<String, AgentToolSpec> toolSpecs;
|
||||
|
||||
/**
|
||||
* 创建工具 HITL 干预器。
|
||||
*
|
||||
* @param eventBridge 旁路事件桥
|
||||
* @param approvalCoordinator 工具审批协调器
|
||||
* @param toolSpecs 工具声明列表
|
||||
*/
|
||||
public ToolHitlInterceptor(AgentRuntimeEventBridge eventBridge,
|
||||
AgentToolApprovalCoordinator approvalCoordinator,
|
||||
List<AgentToolSpec> toolSpecs) {
|
||||
this.eventBridge = eventBridge;
|
||||
this.approvalCoordinator = approvalCoordinator;
|
||||
this.toolSpecs = (toolSpecs == null ? List.<AgentToolSpec>of() : toolSpecs).stream()
|
||||
.filter(spec -> spec != null && spec.getName() != null && !spec.getName().isBlank())
|
||||
.collect(Collectors.toMap(AgentToolSpec::getName, Function.identity(), (left, right) -> left,
|
||||
LinkedHashMap::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 AgentScope Hook 事件。
|
||||
*
|
||||
* @param event AgentScope Hook 事件
|
||||
* @param <T> Hook 事件类型
|
||||
* @return 处理后的 Hook 事件
|
||||
*/
|
||||
@Override
|
||||
public <T extends HookEvent> Mono<T> intercept(T event) {
|
||||
if (event instanceof PostReasoningEvent postReasoningEvent) {
|
||||
interceptPostReasoning(postReasoningEvent);
|
||||
} else if (event instanceof PreActingEvent preActingEvent) {
|
||||
interceptPreActing(preActingEvent);
|
||||
}
|
||||
return Mono.just(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回执行优先级。
|
||||
*
|
||||
* <p>工具审批需要在普通观察器发出 reasoning completed 后保持事件已被标记暂停,
|
||||
* 但不应早于 AutoContext 的 PreReasoning 干预。当前值用于主线路 reasoning 后检查。</p>
|
||||
*
|
||||
* @return 优先级
|
||||
*/
|
||||
@Override
|
||||
public int priority() {
|
||||
return 50;
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化待审批调用、创建审批批次并暂停 AgentScope。
|
||||
*
|
||||
* @param event 推理完成事件
|
||||
*/
|
||||
private void interceptPostReasoning(PostReasoningEvent event) {
|
||||
Msg reasoningMessage = normalizeApprovalToolUses(event.getReasoningMessage());
|
||||
if (reasoningMessage == null) {
|
||||
return;
|
||||
}
|
||||
if (reasoningMessage != event.getReasoningMessage()) {
|
||||
event.setReasoningMessage(reasoningMessage);
|
||||
}
|
||||
List<ToolUseBlock> approvalRequiredTools = approvalRequiredTools(reasoningMessage);
|
||||
if (approvalRequiredTools.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String approvalBatchId = approvalBatchId(reasoningMessage);
|
||||
for (ToolUseBlock toolUse : approvalRequiredTools) {
|
||||
AgentToolSpec toolSpec = toolSpecs.get(toolUse.getName());
|
||||
AgentPendingState pendingState = registerPendingState(toolSpec, toolUse, approvalBatchId);
|
||||
if (pendingState.getEventId() != null && !pendingState.getEventId().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
AgentRuntimeEvent approvalEvent = toolApprovalRequiredEvent(toolSpec, toolUse, pendingState);
|
||||
pendingState.setEventId(approvalEvent.getEventId());
|
||||
eventBridge.emit(approvalEvent);
|
||||
}
|
||||
event.stopAgent();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在工具实际执行前消费与调用身份绑定的一次性授权。
|
||||
*
|
||||
* @param event 工具执行前事件
|
||||
*/
|
||||
private void interceptPreActing(PreActingEvent event) {
|
||||
ToolUseBlock toolUse = event.getToolUse();
|
||||
AgentToolSpec toolSpec = toolUse == null ? null : toolSpecs.get(toolUse.getName());
|
||||
if (!requiresApproval(toolSpec, toolUse)) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> approvalMetadata = approvalMetadata(toolSpec, toolUse);
|
||||
if (!requiresForcedApproval(toolSpec, toolUse)
|
||||
&& approvalCoordinator.isReusableApprovalGranted(approvalMetadata)) {
|
||||
return;
|
||||
}
|
||||
// 执行授权与 toolCallId、工具名称及入参同时绑定,并且只能消费一次。
|
||||
approvalCoordinator.consumeExecutionAuthorization(
|
||||
toolUse.getId(),
|
||||
toolUse.getName(),
|
||||
toolUse.getInput());
|
||||
}
|
||||
|
||||
/**
|
||||
* 为缺少ID的审批调用补充稳定ID,并按 toolCallId 去除同轮重放调用。
|
||||
*
|
||||
* @param reasoningMessage 原始推理消息
|
||||
* @return 归一化后的推理消息
|
||||
*/
|
||||
private Msg normalizeApprovalToolUses(Msg reasoningMessage) {
|
||||
if (reasoningMessage == null || reasoningMessage.getContent() == null
|
||||
|| reasoningMessage.getContent().isEmpty()) {
|
||||
return reasoningMessage;
|
||||
}
|
||||
List<ContentBlock> normalizedContent = new ArrayList<>(reasoningMessage.getContent().size());
|
||||
Map<String, ToolCallSignature> seenApprovalCalls = new LinkedHashMap<>();
|
||||
boolean changed = false;
|
||||
for (int contentIndex = 0; contentIndex < reasoningMessage.getContent().size(); contentIndex++) {
|
||||
ContentBlock block = reasoningMessage.getContent().get(contentIndex);
|
||||
if (!(block instanceof ToolUseBlock toolUse) || !isApprovalRequired(toolUse)) {
|
||||
normalizedContent.add(block);
|
||||
continue;
|
||||
}
|
||||
ToolUseBlock normalizedToolUse = toolUse;
|
||||
if (toolUse.getId() == null || toolUse.getId().isBlank()) {
|
||||
normalizedToolUse = copyWithId(toolUse, stableToolCallId(reasoningMessage, contentIndex));
|
||||
changed = true;
|
||||
}
|
||||
ToolCallSignature signature = new ToolCallSignature(
|
||||
normalizedToolUse.getName(), normalizedToolUse.getInput());
|
||||
ToolCallSignature existing = seenApprovalCalls.putIfAbsent(normalizedToolUse.getId(), signature);
|
||||
if (existing != null) {
|
||||
if (!existing.equals(signature)) {
|
||||
throw new AgentRuntimeException(
|
||||
"Duplicate toolCallId is bound to a different tool call: " + normalizedToolUse.getId());
|
||||
}
|
||||
// 相同 toolCallId 表示同一协议调用被重复返回,只保留第一次出现。
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
normalizedContent.add(normalizedToolUse);
|
||||
}
|
||||
if (!changed) {
|
||||
return reasoningMessage;
|
||||
}
|
||||
return Msg.builder()
|
||||
.id(reasoningMessage.getId())
|
||||
.name(reasoningMessage.getName())
|
||||
.role(reasoningMessage.getRole())
|
||||
.content(normalizedContent)
|
||||
.metadata(reasoningMessage.getMetadata())
|
||||
.timestamp(reasoningMessage.getTimestamp())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 为缺少调用ID的工具生成跨同一推理消息重放稳定的调用ID。
|
||||
*
|
||||
* @param reasoningMessage 推理消息
|
||||
* @param contentIndex 工具块在消息内容中的位置
|
||||
* @return 稳定工具调用ID
|
||||
*/
|
||||
private String stableToolCallId(Msg reasoningMessage, int contentIndex) {
|
||||
String messageId = reasoningMessage.getId();
|
||||
if (messageId == null || messageId.isBlank()) {
|
||||
return "hitl-" + UUID.randomUUID();
|
||||
}
|
||||
return "hitl-" + messageId + "-" + contentIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为同一推理消息生成稳定审批批次ID。
|
||||
*
|
||||
* @param reasoningMessage 推理消息
|
||||
* @return 审批批次ID
|
||||
*/
|
||||
private String approvalBatchId(Msg reasoningMessage) {
|
||||
String messageId = reasoningMessage == null ? null : reasoningMessage.getId();
|
||||
if (messageId == null || messageId.isBlank()) {
|
||||
return "hitl-batch-" + UUID.randomUUID();
|
||||
}
|
||||
return "hitl-batch-" + messageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断工具调用是否要求人工审批。
|
||||
*
|
||||
* @param toolUse 工具调用
|
||||
* @return 要求审批时为 true
|
||||
*/
|
||||
private boolean isApprovalRequired(ToolUseBlock toolUse) {
|
||||
AgentToolSpec toolSpec = toolUse == null ? null : toolSpecs.get(toolUse.getName());
|
||||
return requiresApproval(toolSpec, toolUse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断工具声明或当前调用是否要求审批。
|
||||
*
|
||||
* @param toolSpec 工具声明
|
||||
* @param toolUse 当前工具调用
|
||||
* @return 需要审批时为 true
|
||||
*/
|
||||
private boolean requiresApproval(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
|
||||
if (toolSpec == null) {
|
||||
return false;
|
||||
}
|
||||
AgentToolApprovalEvaluation evaluation = approvalEvaluation(toolSpec, toolUse);
|
||||
if (evaluation != null && !evaluation.valid()) {
|
||||
// 无效命令直接进入工具并返回结构化拒绝,避免产生必然失败的审批请求。
|
||||
return false;
|
||||
}
|
||||
return toolSpec.isApprovalRequired()
|
||||
|| (evaluation != null && evaluation.approvalRequired())
|
||||
|| requiresLegacyForcedApproval(toolSpec, toolUse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据工具声明中的强制审批命令规则检查当前调用。
|
||||
*
|
||||
* <p>命令首词解析与受控 Shell 的引号、反斜杠规则保持一致,避免通过
|
||||
* {@code 'rm'} 或 {@code r\m} 绕过动态审批。畸形命令仍由 Shell 工具拒绝。</p>
|
||||
*
|
||||
* @param toolSpec 工具声明
|
||||
* @param toolUse 当前工具调用
|
||||
* @return 命中强制审批命令时为 true
|
||||
*/
|
||||
private boolean requiresForcedApproval(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
|
||||
AgentToolApprovalEvaluation evaluation = approvalEvaluation(toolSpec, toolUse);
|
||||
if (evaluation != null) {
|
||||
return evaluation.valid() && evaluation.forced();
|
||||
}
|
||||
return requiresLegacyForcedApproval(toolSpec, toolUse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用旧版元数据规则判断当前调用是否命中强制审批命令。
|
||||
*
|
||||
* @param toolSpec 工具声明
|
||||
* @param toolUse 当前工具调用
|
||||
* @return 命中旧版强制审批规则时为 true
|
||||
*/
|
||||
private boolean requiresLegacyForcedApproval(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
|
||||
if (toolSpec == null || toolUse == null || toolSpec.getMetadata() == null) {
|
||||
return false;
|
||||
}
|
||||
Object commandsValue = toolSpec.getMetadata().get("forceApprovalCommands");
|
||||
Object argumentValue = toolSpec.getMetadata().get("forceApprovalCommandArgument");
|
||||
if (!(commandsValue instanceof Iterable<?> commands) || !(argumentValue instanceof String argumentName)
|
||||
|| argumentName.isBlank() || toolUse.getInput() == null) {
|
||||
return false;
|
||||
}
|
||||
Object commandValue = toolUse.getInput().get(argumentName);
|
||||
if (!(commandValue instanceof String command)) {
|
||||
return false;
|
||||
}
|
||||
String executable = firstCommandToken(command);
|
||||
if (executable == null) {
|
||||
return false;
|
||||
}
|
||||
for (Object forcedCommand : commands) {
|
||||
if (forcedCommand instanceof String value && executable.equals(value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析受限命令行的首个参数。
|
||||
*
|
||||
* @param command 命令行
|
||||
* @return 首个参数;无有效参数时返回 null
|
||||
*/
|
||||
private String firstCommandToken(String command) {
|
||||
if (command == null || command.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder token = new StringBuilder();
|
||||
char quote = 0;
|
||||
boolean escaping = false;
|
||||
boolean started = false;
|
||||
for (int index = 0; index < command.length(); index++) {
|
||||
char character = command.charAt(index);
|
||||
if (!started && Character.isWhitespace(character)) {
|
||||
continue;
|
||||
}
|
||||
started = true;
|
||||
if (escaping) {
|
||||
token.append(character);
|
||||
escaping = false;
|
||||
} else if (character == '\\' && quote != '\'') {
|
||||
escaping = true;
|
||||
} else if (character == '\'' || character == '"') {
|
||||
if (quote == 0) {
|
||||
quote = character;
|
||||
} else if (quote == character) {
|
||||
quote = 0;
|
||||
} else {
|
||||
token.append(character);
|
||||
}
|
||||
} else if (Character.isWhitespace(character) && quote == 0) {
|
||||
break;
|
||||
} else {
|
||||
token.append(character);
|
||||
}
|
||||
}
|
||||
return token.isEmpty() ? null : token.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制工具调用并替换调用ID。
|
||||
*
|
||||
* @param toolUse 原始工具调用
|
||||
* @param toolCallId 新工具调用ID
|
||||
* @return 新工具调用
|
||||
*/
|
||||
private ToolUseBlock copyWithId(ToolUseBlock toolUse, String toolCallId) {
|
||||
return ToolUseBlock.builder()
|
||||
.id(toolCallId)
|
||||
.name(toolUse.getName())
|
||||
.input(toolUse.getInput())
|
||||
.content(toolUse.getContent())
|
||||
.metadata(toolUse.getMetadata())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推理消息中要求审批的工具调用。
|
||||
*
|
||||
* @param reasoningMessage 推理消息
|
||||
* @return 待审批工具调用
|
||||
*/
|
||||
private List<ToolUseBlock> approvalRequiredTools(Msg reasoningMessage) {
|
||||
List<ToolUseBlock> toolUses = reasoningMessage.getContentBlocks(ToolUseBlock.class);
|
||||
if (toolUses == null || toolUses.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<ToolUseBlock> approvalTools = new ArrayList<>();
|
||||
Set<String> pendingReusableScopes = new LinkedHashSet<>();
|
||||
for (ToolUseBlock toolUse : toolUses) {
|
||||
AgentToolSpec toolSpec = toolUse == null ? null : toolSpecs.get(toolUse.getName());
|
||||
if (!requiresApproval(toolSpec, toolUse)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> approvalMetadata = approvalMetadata(toolSpec, toolUse);
|
||||
if (!requiresForcedApproval(toolSpec, toolUse)
|
||||
&& approvalCoordinator.isReusableApprovalGranted(approvalMetadata)) {
|
||||
continue;
|
||||
}
|
||||
String reusableScope = approvalCoordinator.reusableApprovalScope(approvalMetadata);
|
||||
if (reusableScope != null && !pendingReusableScopes.add(reusableScope)) {
|
||||
// 同一推理消息中同一 MCP 的多个工具共享一个审批请求。
|
||||
continue;
|
||||
}
|
||||
approvalTools.add(toolUse);
|
||||
}
|
||||
return approvalTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册审批批次中的待审批状态。
|
||||
*
|
||||
* @param toolSpec 工具声明
|
||||
* @param toolUse 工具调用
|
||||
* @param approvalBatchId 审批批次ID
|
||||
* @return 待审批状态
|
||||
*/
|
||||
private AgentPendingState registerPendingState(AgentToolSpec toolSpec,
|
||||
ToolUseBlock toolUse,
|
||||
String approvalBatchId) {
|
||||
AgentRuntimeExecutionContext context = eventBridge.executionContext();
|
||||
AgentToolApprovalRequest approvalRequest = toolSpec.getApprovalRequest();
|
||||
Duration timeout = approvalRequest == null || approvalRequest.getTimeout() == null
|
||||
? Duration.ofMinutes(30)
|
||||
: approvalRequest.getTimeout();
|
||||
Map<String, Object> metadata = approvalRequest == null
|
||||
? new LinkedHashMap<>()
|
||||
: new LinkedHashMap<>(approvalRequest.getMetadata());
|
||||
metadata.putAll(toolUse.getMetadata() == null ? Map.of() : toolUse.getMetadata());
|
||||
if (toolSpec.getMetadata() != null && !toolSpec.getMetadata().isEmpty()) {
|
||||
// ToolSpec 由工具编译阶段生成,必须覆盖模型返回的同名治理字段(例如 toolType、mcpId)。
|
||||
metadata.putAll(toolSpec.getMetadata());
|
||||
}
|
||||
AgentToolApprovalEvaluation evaluation = approvalEvaluation(toolSpec, toolUse);
|
||||
if (evaluation != null && evaluation.metadata() != null) {
|
||||
// 动态策略由受信任工具实例计算,必须覆盖模型与静态声明中的同名字段。
|
||||
metadata.putAll(evaluation.metadata());
|
||||
}
|
||||
metadata.put("phase", "POST_REASONING");
|
||||
metadata.put("source", "TOOL_HITL_INTERCEPTOR");
|
||||
return approvalCoordinator.register(
|
||||
context == null ? null : context.getSessionId(),
|
||||
context == null || context.getAgentDefinition() == null ? null : context.getAgentDefinition().getAgentId(),
|
||||
toolUse.getId(),
|
||||
toolUse.getName(),
|
||||
approvalPrompt(approvalRequest),
|
||||
toolUse.getInput(),
|
||||
metadata,
|
||||
Instant.now().plus(timeout),
|
||||
approvalBatchId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用工具声明中的受信任动态审批策略。
|
||||
*
|
||||
* @param toolSpec 工具声明
|
||||
* @param toolUse 当前工具调用
|
||||
* @return 动态审批判定;未配置策略时返回 null
|
||||
*/
|
||||
private AgentToolApprovalEvaluation approvalEvaluation(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
|
||||
if (toolSpec == null || toolSpec.getApprovalPolicy() == null || toolUse == null) {
|
||||
return null;
|
||||
}
|
||||
return toolSpec.getApprovalPolicy().evaluate(
|
||||
toolUse.getInput() == null ? Map.of() : toolUse.getInput());
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并静态工具元数据与动态审批元数据。
|
||||
*
|
||||
* @param toolSpec 工具声明
|
||||
* @param toolUse 当前工具调用
|
||||
* @return 用于审批作用域判断的受信任元数据
|
||||
*/
|
||||
private Map<String, Object> approvalMetadata(AgentToolSpec toolSpec, ToolUseBlock toolUse) {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
if (toolSpec != null && toolSpec.getMetadata() != null) {
|
||||
metadata.putAll(toolSpec.getMetadata());
|
||||
}
|
||||
AgentToolApprovalEvaluation evaluation = approvalEvaluation(toolSpec, toolUse);
|
||||
if (evaluation != null && evaluation.metadata() != null) {
|
||||
metadata.putAll(evaluation.metadata());
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建工具审批请求事件。
|
||||
*
|
||||
* @param toolSpec 工具声明
|
||||
* @param toolUse 工具调用
|
||||
* @param pendingState 待审批状态
|
||||
* @return 审批请求事件
|
||||
*/
|
||||
private AgentRuntimeEvent toolApprovalRequiredEvent(AgentToolSpec toolSpec,
|
||||
ToolUseBlock toolUse,
|
||||
AgentPendingState pendingState) {
|
||||
AgentRuntimeExecutionContext context = eventBridge.executionContext();
|
||||
AgentRuntimeEvent event = eventBridge.event(AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED);
|
||||
event.setToolCallId(toolUse.getId());
|
||||
event.getPayload().putAll(pendingApprovalPayload(pendingState, toolUse));
|
||||
event.getPayload().put("sessionId", context == null ? null : context.getSessionId());
|
||||
event.getPayload().put("agentId", context == null || context.getAgentDefinition() == null
|
||||
? null
|
||||
: context.getAgentDefinition().getAgentId());
|
||||
event.getPayload().put("approvalPrompt", approvalPrompt(toolSpec.getApprovalRequest()));
|
||||
event.getPayload().put("approvalMetadata", pendingState.getMetadata());
|
||||
event.getPayload().put("toolDescription", toolSpec.getDescription());
|
||||
enrichToolPayload(event.getPayload(), toolSpec);
|
||||
event.getMetadata().put("source", "TOOL_HITL_INTERCEPTOR");
|
||||
event.getMetadata().put("phase", "POST_REASONING");
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建待审批工具的稳定事件载荷。
|
||||
*
|
||||
* @param pendingState 待审批状态
|
||||
* @param toolUse 工具调用
|
||||
* @return 待审批载荷
|
||||
*/
|
||||
private Map<String, Object> pendingApprovalPayload(AgentPendingState pendingState, ToolUseBlock toolUse) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("resumeToken", pendingState.getResumeToken().getValue());
|
||||
payload.put("toolCallId", pendingState.getToolCallId());
|
||||
payload.put("toolName", pendingState.getToolName());
|
||||
payload.put("toolInput", pendingState.getToolInput());
|
||||
payload.put("input", toolUse.getInput());
|
||||
payload.put("content", toolUse.getContent());
|
||||
payload.put("expiresAt", pendingState.getExpiresAt() == null ? null : pendingState.getExpiresAt().toString());
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将工具展示元数据补充到审批事件载荷。
|
||||
*
|
||||
* @param payload 审批事件载荷
|
||||
* @param toolSpec 工具声明
|
||||
*/
|
||||
private void enrichToolPayload(Map<String, Object> payload, AgentToolSpec toolSpec) {
|
||||
if (toolSpec == null || toolSpec.getMetadata() == null || toolSpec.getMetadata().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> metadata = toolSpec.getMetadata();
|
||||
putIfPresent(payload, metadata, "toolDisplayName");
|
||||
putIfPresent(payload, metadata, "rawMcpToolName");
|
||||
putIfPresent(payload, metadata, "mcpToolName");
|
||||
putIfPresent(payload, metadata, "mcpId");
|
||||
putIfPresent(payload, metadata, "mcpName");
|
||||
putIfPresent(payload, metadata, "mcpTitle");
|
||||
}
|
||||
|
||||
/**
|
||||
* 在元数据包含指定字段时复制到事件载荷。
|
||||
*
|
||||
* @param payload 事件载荷
|
||||
* @param metadata 工具元数据
|
||||
* @param key 字段名
|
||||
*/
|
||||
private void putIfPresent(Map<String, Object> payload, Map<String, Object> metadata, String key) {
|
||||
if (metadata.containsKey(key)) {
|
||||
payload.put(key, metadata.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审批提示文案。
|
||||
*
|
||||
* @param approvalRequest 审批配置
|
||||
* @return 审批提示文案
|
||||
*/
|
||||
private String approvalPrompt(AgentToolApprovalRequest approvalRequest) {
|
||||
if (approvalRequest != null
|
||||
&& approvalRequest.getApprovalPrompt() != null
|
||||
&& !approvalRequest.getApprovalPrompt().isBlank()) {
|
||||
return approvalRequest.getApprovalPrompt();
|
||||
}
|
||||
return "是否批准执行该工具?";
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具名称与输入组成的调用身份校验值。
|
||||
*/
|
||||
private static final class ToolCallSignature {
|
||||
private final String toolName;
|
||||
private final Map<String, Object> toolInput;
|
||||
|
||||
/**
|
||||
* 创建工具调用身份校验值。
|
||||
*
|
||||
* @param toolName 工具名称
|
||||
* @param toolInput 工具入参
|
||||
*/
|
||||
private ToolCallSignature(String toolName, Map<String, Object> toolInput) {
|
||||
this.toolName = toolName;
|
||||
this.toolInput = toolInput == null ? Map.of() : new LinkedHashMap<>(toolInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较工具调用语义是否一致。
|
||||
*
|
||||
* @param object 待比较对象
|
||||
* @return 语义一致时为 true
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (this == object) {
|
||||
return true;
|
||||
}
|
||||
if (!(object instanceof ToolCallSignature that)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(toolName, that.toolName)
|
||||
&& Objects.equals(toolInput, that.toolInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算工具调用语义哈希。
|
||||
*
|
||||
* @return 哈希值
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(toolName, toolInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.easyagents.agent.runtime.event.observer;
|
||||
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeObserver;
|
||||
import io.agentscope.core.agent.Agent;
|
||||
import io.agentscope.core.hook.ErrorEvent;
|
||||
import io.agentscope.core.hook.HookEvent;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 监听 AgentScope 原生错误事件,并发射运行失败旁路事件。
|
||||
*
|
||||
* <p>该观察器复用 {@link AgentRuntimeEventType#FAILED},但通过 payload 中的
|
||||
* {@code source=HOOK} 标识它来自 AgentScope 生命周期观察,不等同于 Easy-Agents
|
||||
* runtime 外层流已经完成失败收口。</p>
|
||||
*/
|
||||
public class AgentRuntimeErrorObserver implements AgentRuntimeObserver {
|
||||
|
||||
private final AgentRuntimeEventBridge eventBridge;
|
||||
|
||||
/**
|
||||
* 创建运行错误观察器。
|
||||
*
|
||||
* @param eventBridge 旁路事件桥
|
||||
*/
|
||||
public AgentRuntimeErrorObserver(AgentRuntimeEventBridge eventBridge) {
|
||||
this.eventBridge = eventBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* 观察 AgentScope 错误事件。
|
||||
*
|
||||
* @param event AgentScope Hook 事件
|
||||
* @return 完成信号
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> observe(HookEvent event) {
|
||||
if (!(event instanceof ErrorEvent errorEvent)) {
|
||||
return Mono.empty();
|
||||
}
|
||||
Throwable error = errorEvent.getError();
|
||||
AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.FAILED);
|
||||
runtimeEvent.getPayload().put("source", "HOOK");
|
||||
runtimeEvent.getPayload().put("phase", "ERROR");
|
||||
runtimeEvent.getPayload().put("stage", "AGENTSCOPE_HOOK");
|
||||
runtimeEvent.getPayload().put("errorType", error == null ? null : error.getClass().getName());
|
||||
runtimeEvent.getPayload().put("message", error == null ? "AgentScope hook error." : error.getMessage());
|
||||
appendAgent(runtimeEvent, errorEvent.getAgent());
|
||||
eventBridge.emit(runtimeEvent);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
private void appendAgent(AgentRuntimeEvent event, Agent agent) {
|
||||
if (agent == null) {
|
||||
return;
|
||||
}
|
||||
event.getPayload().put("agentName", agent.getName());
|
||||
event.getPayload().put("agentScopeAgentId", agent.getAgentId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.easyagents.agent.runtime.event.observer;
|
||||
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeObserver;
|
||||
import io.agentscope.core.hook.HookEvent;
|
||||
import io.agentscope.core.hook.PostReasoningEvent;
|
||||
import io.agentscope.core.hook.PreReasoningEvent;
|
||||
import io.agentscope.core.message.Msg;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 监听 AgentScope 推理生命周期,并发射思考状态旁路事件。
|
||||
*
|
||||
* <p>{@link AgentRuntimeEventType#REASONING_DELTA} 表示主线路中的推理内容片段;
|
||||
* 本观察器发射的 started/completed 事件只用于前端状态展示,不携带模型上下文,
|
||||
* 也不会修改 AgentScope 的推理输入或输出。</p>
|
||||
*/
|
||||
public class ReasoningLifecycleObserver implements AgentRuntimeObserver {
|
||||
|
||||
private final AgentRuntimeEventBridge eventBridge;
|
||||
|
||||
/**
|
||||
* 创建推理生命周期观察器。
|
||||
*
|
||||
* @param eventBridge 旁路事件桥
|
||||
*/
|
||||
public ReasoningLifecycleObserver(AgentRuntimeEventBridge eventBridge) {
|
||||
this.eventBridge = eventBridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* 观察推理开始和完成事件。
|
||||
*
|
||||
* @param event AgentScope Hook 事件
|
||||
* @return 完成信号
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> observe(HookEvent event) {
|
||||
if (event instanceof PreReasoningEvent preReasoningEvent) {
|
||||
emitStarted(preReasoningEvent);
|
||||
return Mono.empty();
|
||||
}
|
||||
if (event instanceof PostReasoningEvent postReasoningEvent) {
|
||||
emitCompleted(postReasoningEvent);
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
private void emitStarted(PreReasoningEvent event) {
|
||||
AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.REASONING_STARTED);
|
||||
runtimeEvent.getPayload().put("modelName", event.getModelName());
|
||||
runtimeEvent.getPayload().put("messageCount", event.getInputMessages() == null ? 0 : event.getInputMessages().size());
|
||||
runtimeEvent.getPayload().put("source", "HOOK");
|
||||
runtimeEvent.getPayload().put("phase", "PRE_REASONING");
|
||||
eventBridge.emit(runtimeEvent);
|
||||
}
|
||||
|
||||
private void emitCompleted(PostReasoningEvent event) {
|
||||
AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.REASONING_COMPLETED);
|
||||
runtimeEvent.getPayload().put("modelName", event.getModelName());
|
||||
runtimeEvent.getPayload().put("stopRequested", event.isStopRequested());
|
||||
runtimeEvent.getPayload().put("gotoReasoningRequested", event.isGotoReasoningRequested());
|
||||
runtimeEvent.getPayload().put("text", reasoningText(event.getReasoningMessage()));
|
||||
runtimeEvent.getPayload().put("source", "HOOK");
|
||||
runtimeEvent.getPayload().put("phase", "POST_REASONING");
|
||||
eventBridge.emit(runtimeEvent);
|
||||
}
|
||||
|
||||
private String reasoningText(Msg message) {
|
||||
if (message == null || message.getTextContent() == null) {
|
||||
return "";
|
||||
}
|
||||
return message.getTextContent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package com.easyagents.agent.runtime.event.observer;
|
||||
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeObserver;
|
||||
import com.easyagents.agent.runtime.skill.AgentSkillBinding;
|
||||
import com.easyagents.agent.runtime.skill.AgentSkillLoadCall;
|
||||
import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext;
|
||||
import io.agentscope.core.hook.HookEvent;
|
||||
import io.agentscope.core.hook.PostActingEvent;
|
||||
import io.agentscope.core.hook.PreActingEvent;
|
||||
import io.agentscope.core.message.ContentBlock;
|
||||
import io.agentscope.core.message.TextBlock;
|
||||
import io.agentscope.core.message.ToolResultBlock;
|
||||
import io.agentscope.core.message.ToolUseBlock;
|
||||
import io.agentscope.core.skill.SkillBox;
|
||||
import io.agentscope.core.tool.Toolkit;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
/**
|
||||
* 监听 AgentScope 工具执行生命周期,并发射 Skill 旁路事件。
|
||||
*
|
||||
* <p>该观察器只做 Easy-Agents 的旁路监察,不修改 AgentScope {@link HookEvent}。
|
||||
* Skill 加载工具 {@link AgentSkillRuntimeContext#LOAD_SKILL_TOOL_NAME} 本身仍是
|
||||
* AgentScope 主线路中的工具调用;本观察器只把它翻译成调用方可展示的
|
||||
* {@link AgentRuntimeEventType#SKILL_CALL}、{@link AgentRuntimeEventType#SKILL_RESULT}
|
||||
* 和 {@link AgentRuntimeEventType#SKILL_FAILED}。已激活 Skill 内部的普通工具调用会
|
||||
* 被翻译成 {@link AgentRuntimeEventType#SKILL_STEP}。</p>
|
||||
*
|
||||
* <p>Skill 是否激活以 AgentScope {@link SkillBox} 和 {@link Toolkit#getActiveGroups()}
|
||||
* 为准,本地 {@link AgentSkillRuntimeContext} 只缓存旁路展示所需的归属状态。</p>
|
||||
*/
|
||||
public class SkillExecutionObserver implements AgentRuntimeObserver {
|
||||
|
||||
private final AgentRuntimeEventBridge eventBridge;
|
||||
private final AgentSkillRuntimeContext skillContext;
|
||||
private final SkillBox skillBox;
|
||||
|
||||
/**
|
||||
* 创建 Skill 执行观察器。
|
||||
*
|
||||
* @param eventBridge 旁路事件桥
|
||||
* @param skillContext Skill 运行时上下文
|
||||
*/
|
||||
public SkillExecutionObserver(AgentRuntimeEventBridge eventBridge,
|
||||
AgentSkillRuntimeContext skillContext) {
|
||||
this(eventBridge, skillContext, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Skill 执行观察器。
|
||||
*
|
||||
* @param eventBridge 旁路事件桥
|
||||
* @param skillContext Skill 运行时上下文
|
||||
* @param skillBox AgentScope SkillBox,作为 Skill 激活状态的权威来源
|
||||
*/
|
||||
public SkillExecutionObserver(AgentRuntimeEventBridge eventBridge,
|
||||
AgentSkillRuntimeContext skillContext,
|
||||
SkillBox skillBox) {
|
||||
this.eventBridge = eventBridge;
|
||||
this.skillContext = skillContext;
|
||||
this.skillBox = skillBox;
|
||||
}
|
||||
|
||||
/**
|
||||
* 观察 Skill 加载和已激活 Skill 内部工具执行。
|
||||
*
|
||||
* @param event AgentScope Hook 事件
|
||||
* @return 完成信号
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> observe(HookEvent event) {
|
||||
if (skillContext == null) {
|
||||
return Mono.empty();
|
||||
}
|
||||
if (event instanceof PreActingEvent preActingEvent) {
|
||||
observePreActing(preActingEvent);
|
||||
return Mono.empty();
|
||||
}
|
||||
if (event instanceof PostActingEvent postActingEvent) {
|
||||
observePostActing(postActingEvent);
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
private void observePreActing(PreActingEvent event) {
|
||||
ToolUseBlock toolUse = event.getToolUse();
|
||||
if (toolUse == null) {
|
||||
return;
|
||||
}
|
||||
syncSkillState(toolUse.getName(), event.getToolkit());
|
||||
if (skillContext.isSkillLoadTool(toolUse.getName())) {
|
||||
emitSkillCall(event, toolUse);
|
||||
return;
|
||||
}
|
||||
AgentSkillBinding activeBinding = skillContext.getActiveToolBinding(toolUse.getName());
|
||||
if (activeBinding != null) {
|
||||
emitSkillStepCall(toolUse, activeBinding);
|
||||
}
|
||||
}
|
||||
|
||||
private void observePostActing(PostActingEvent event) {
|
||||
ToolResultBlock result = event.getToolResult();
|
||||
ToolUseBlock toolUse = event.getToolUse();
|
||||
String toolName = result == null ? toolName(toolUse) : result.getName();
|
||||
if (skillContext.isSkillLoadTool(toolName)) {
|
||||
emitSkillResult(result, toolUse, event.getToolkit());
|
||||
return;
|
||||
}
|
||||
syncSkillState(toolName, event.getToolkit());
|
||||
AgentSkillBinding activeBinding = skillContext.getActiveToolBinding(toolName);
|
||||
if (activeBinding != null) {
|
||||
emitSkillStepResult(result, toolUse, activeBinding);
|
||||
}
|
||||
}
|
||||
|
||||
private void emitSkillCall(PreActingEvent event, ToolUseBlock toolUse) {
|
||||
AgentSkillLoadCall call = skillContext.rememberLoadCall(toolUse.getId(), toolUse.getInput());
|
||||
if (!skillContext.markLoadCallEmitted(toolUse.getId())) {
|
||||
return;
|
||||
}
|
||||
AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.SKILL_CALL);
|
||||
runtimeEvent.setToolCallId(toolUse.getId());
|
||||
runtimeEvent.getPayload().put("toolCallId", toolUse.getId());
|
||||
runtimeEvent.getPayload().put("toolName", toolUse.getName());
|
||||
runtimeEvent.getPayload().put("input", toolUse.getInput());
|
||||
runtimeEvent.getPayload().put("status", "RUNNING");
|
||||
runtimeEvent.getPayload().put("source", "HOOK");
|
||||
runtimeEvent.getPayload().put("phase", "PRE_ACTING");
|
||||
appendSkillLoadPayload(runtimeEvent, call);
|
||||
runtimeEvent.getMetadata().putAll(nullToEmpty(toolUse.getMetadata()));
|
||||
eventBridge.emit(runtimeEvent);
|
||||
}
|
||||
|
||||
private void emitSkillResult(ToolResultBlock result, ToolUseBlock toolUse, Toolkit toolkit) {
|
||||
String toolCallId = result == null ? toolCallId(toolUse) : result.getId();
|
||||
AgentSkillLoadCall call = skillContext.removeLoadCall(toolCallId);
|
||||
boolean active = syncSkillState(call, toolkit);
|
||||
AgentRuntimeEvent runtimeEvent = eventBridge.event(skillResultType(result, active));
|
||||
runtimeEvent.setToolCallId(toolCallId);
|
||||
runtimeEvent.getPayload().put("toolCallId", toolCallId);
|
||||
runtimeEvent.getPayload().put("toolName", result == null ? toolName(toolUse) : result.getName());
|
||||
runtimeEvent.getPayload().put("text", resultText(result));
|
||||
runtimeEvent.getPayload().put("status", runtimeEvent.getEventType() == AgentRuntimeEventType.SKILL_RESULT
|
||||
? "SUCCESS"
|
||||
: "FAILED");
|
||||
runtimeEvent.getPayload().put("success", runtimeEvent.getEventType() == AgentRuntimeEventType.SKILL_RESULT);
|
||||
runtimeEvent.getPayload().put("suspended", result != null && result.isSuspended());
|
||||
runtimeEvent.getPayload().put("active", active);
|
||||
runtimeEvent.getPayload().put("source", "HOOK");
|
||||
runtimeEvent.getPayload().put("phase", "POST_ACTING");
|
||||
if (result != null) {
|
||||
runtimeEvent.getMetadata().putAll(nullToEmpty(result.getMetadata()));
|
||||
}
|
||||
appendSkillLoadPayload(runtimeEvent, call);
|
||||
eventBridge.emit(runtimeEvent);
|
||||
}
|
||||
|
||||
private void emitSkillStepCall(ToolUseBlock toolUse, AgentSkillBinding binding) {
|
||||
AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.SKILL_STEP);
|
||||
runtimeEvent.setToolCallId(toolUse.getId());
|
||||
runtimeEvent.getPayload().put("toolCallId", toolUse.getId());
|
||||
runtimeEvent.getPayload().put("name", toolUse.getName());
|
||||
runtimeEvent.getPayload().put("toolName", toolUse.getName());
|
||||
runtimeEvent.getPayload().put("input", toolUse.getInput());
|
||||
runtimeEvent.getPayload().put("content", toolUse.getContent());
|
||||
runtimeEvent.getPayload().put("stepType", "TOOL_CALL");
|
||||
runtimeEvent.getPayload().put("stepName", toolUse.getName());
|
||||
runtimeEvent.getPayload().put("status", "RUNNING");
|
||||
runtimeEvent.getPayload().put("source", "HOOK");
|
||||
runtimeEvent.getPayload().put("phase", "PRE_ACTING");
|
||||
appendSkillPayload(runtimeEvent.getPayload(), binding);
|
||||
appendSkillPayload(runtimeEvent.getMetadata(), binding);
|
||||
runtimeEvent.getMetadata().putAll(nullToEmpty(toolUse.getMetadata()));
|
||||
eventBridge.emit(runtimeEvent);
|
||||
}
|
||||
|
||||
private void emitSkillStepResult(ToolResultBlock result,
|
||||
ToolUseBlock toolUse,
|
||||
AgentSkillBinding binding) {
|
||||
AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.SKILL_STEP);
|
||||
String toolCallId = result == null ? toolCallId(toolUse) : result.getId();
|
||||
String toolName = result == null ? toolName(toolUse) : result.getName();
|
||||
runtimeEvent.setToolCallId(toolCallId);
|
||||
runtimeEvent.getPayload().put("toolCallId", toolCallId);
|
||||
runtimeEvent.getPayload().put("name", toolName);
|
||||
runtimeEvent.getPayload().put("toolName", toolName);
|
||||
runtimeEvent.getPayload().put("text", resultText(result));
|
||||
runtimeEvent.getPayload().put("suspended", result != null && result.isSuspended());
|
||||
runtimeEvent.getPayload().put("stepType", "TOOL_RESULT");
|
||||
runtimeEvent.getPayload().put("stepName", toolName);
|
||||
runtimeEvent.getPayload().put("status", success(result) ? "SUCCESS" : "FAILED");
|
||||
runtimeEvent.getPayload().put("success", success(result));
|
||||
runtimeEvent.getPayload().put("source", "HOOK");
|
||||
runtimeEvent.getPayload().put("phase", "POST_ACTING");
|
||||
appendSkillPayload(runtimeEvent.getPayload(), binding);
|
||||
appendSkillPayload(runtimeEvent.getMetadata(), binding);
|
||||
if (result != null) {
|
||||
runtimeEvent.getMetadata().putAll(nullToEmpty(result.getMetadata()));
|
||||
}
|
||||
eventBridge.emit(runtimeEvent);
|
||||
}
|
||||
|
||||
private AgentRuntimeEventType skillResultType(ToolResultBlock result, boolean active) {
|
||||
return success(result) && active ? AgentRuntimeEventType.SKILL_RESULT : AgentRuntimeEventType.SKILL_FAILED;
|
||||
}
|
||||
|
||||
private boolean success(ToolResultBlock result) {
|
||||
if (result == null) {
|
||||
return false;
|
||||
}
|
||||
Object success = result.getMetadata() == null ? null : result.getMetadata().get("success");
|
||||
return !(success instanceof Boolean) || Boolean.TRUE.equals(success);
|
||||
}
|
||||
|
||||
private String resultText(ToolResultBlock result) {
|
||||
if (result == null || result.getOutput() == null || result.getOutput().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
for (ContentBlock output : result.getOutput()) {
|
||||
if (output instanceof TextBlock textBlock && textBlock.getText() != null) {
|
||||
joiner.add(textBlock.getText());
|
||||
} else if (output != null) {
|
||||
joiner.add(output.toString());
|
||||
}
|
||||
}
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
private boolean syncSkillState(AgentSkillLoadCall call, Toolkit toolkit) {
|
||||
if (call == null) {
|
||||
return false;
|
||||
}
|
||||
boolean active = isAgentScopeSkillActive(call.getSkillId(), toolkit);
|
||||
skillContext.syncSkillActive(call.getSkillId(), active);
|
||||
return active;
|
||||
}
|
||||
|
||||
private void syncSkillState(String toolName, Toolkit toolkit) {
|
||||
AgentSkillBinding binding = skillContext.getToolBinding(toolName);
|
||||
if (binding == null) {
|
||||
return;
|
||||
}
|
||||
skillContext.syncSkillActive(binding.getSkillId(), isAgentScopeSkillActive(binding.getSkillId(), toolkit));
|
||||
}
|
||||
|
||||
private boolean isAgentScopeSkillActive(String skillId, Toolkit toolkit) {
|
||||
if (skillId == null || skillId.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
if (skillBox != null && skillBox.isSkillActive(skillId)) {
|
||||
return true;
|
||||
}
|
||||
return toolkit != null && toolkit.getActiveGroups().contains(skillToolGroupName(skillId));
|
||||
}
|
||||
|
||||
private String skillToolGroupName(String skillId) {
|
||||
return skillId + "_skill_tools";
|
||||
}
|
||||
|
||||
private void appendSkillLoadPayload(AgentRuntimeEvent event, AgentSkillLoadCall call) {
|
||||
if (call == null) {
|
||||
return;
|
||||
}
|
||||
event.getPayload().put("skillId", call.getSkillId());
|
||||
event.getPayload().put("skillName", call.getSkillName());
|
||||
event.getPayload().put("skillDisplayName", call.getSkillDisplayName());
|
||||
event.getPayload().put("skillBoxId", call.getSkillBoxId());
|
||||
event.getPayload().put("path", call.getPath());
|
||||
event.getMetadata().put("skillId", call.getSkillId());
|
||||
event.getMetadata().put("skillName", call.getSkillName());
|
||||
event.getMetadata().put("skillDisplayName", call.getSkillDisplayName());
|
||||
event.getMetadata().put("skillBoxId", call.getSkillBoxId());
|
||||
}
|
||||
|
||||
private void appendSkillPayload(Map<String, Object> target, AgentSkillBinding binding) {
|
||||
if (binding == null || target == null) {
|
||||
return;
|
||||
}
|
||||
target.put("skillId", binding.getSkillId());
|
||||
target.put("skillName", binding.getSkillName());
|
||||
target.put("skillDisplayName", binding.getSkillDisplayName());
|
||||
target.put("skillBoxId", binding.getSkillBoxId());
|
||||
}
|
||||
|
||||
private String toolCallId(ToolUseBlock toolUse) {
|
||||
return toolUse == null ? null : toolUse.getId();
|
||||
}
|
||||
|
||||
private String toolName(ToolUseBlock toolUse) {
|
||||
return toolUse == null ? null : toolUse.getName();
|
||||
}
|
||||
|
||||
private Map<String, Object> nullToEmpty(Map<String, Object> map) {
|
||||
return map == null ? new LinkedHashMap<>() : map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.easyagents.agent.runtime.event.observer;
|
||||
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeObserver;
|
||||
import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolCategory;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||
import io.agentscope.core.hook.HookEvent;
|
||||
import io.agentscope.core.hook.PostActingEvent;
|
||||
import io.agentscope.core.hook.PreActingEvent;
|
||||
import io.agentscope.core.message.TextBlock;
|
||||
import io.agentscope.core.message.ToolResultBlock;
|
||||
import io.agentscope.core.message.ToolUseBlock;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 监听 AgentScope 原生工具执行生命周期,并发射工具状态旁路事件。
|
||||
*
|
||||
* <p>该观察器复用 {@link AgentRuntimeEventType#TOOL_CALL} 和
|
||||
* {@link AgentRuntimeEventType#TOOL_RESULT},用于 EasyFlow 展示工具开始与完成状态。
|
||||
* 它不修改 AgentScope HookEvent,也不写入模型上下文。</p>
|
||||
*/
|
||||
public class ToolExecutionObserver implements AgentRuntimeObserver {
|
||||
|
||||
private final AgentRuntimeEventBridge eventBridge;
|
||||
private final AgentSkillRuntimeContext skillContext;
|
||||
private final Map<String, AgentToolSpec> toolSpecs;
|
||||
|
||||
/**
|
||||
* 创建工具执行观察器。
|
||||
*
|
||||
* @param eventBridge 旁路事件桥
|
||||
*/
|
||||
public ToolExecutionObserver(AgentRuntimeEventBridge eventBridge) {
|
||||
this(eventBridge, null, List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工具执行观察器。
|
||||
*
|
||||
* @param eventBridge 旁路事件桥
|
||||
* @param skillContext Skill 上下文,用于跳过由 SkillExecutionObserver 处理的工具
|
||||
*/
|
||||
public ToolExecutionObserver(AgentRuntimeEventBridge eventBridge,
|
||||
AgentSkillRuntimeContext skillContext) {
|
||||
this(eventBridge, skillContext, List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工具执行观察器。
|
||||
*
|
||||
* @param eventBridge 旁路事件桥
|
||||
* @param skillContext Skill 上下文,用于跳过由 SkillExecutionObserver 处理的工具
|
||||
* @param toolSpecs 工具声明列表,用于补齐展示名称和治理元数据
|
||||
*/
|
||||
public ToolExecutionObserver(AgentRuntimeEventBridge eventBridge,
|
||||
AgentSkillRuntimeContext skillContext,
|
||||
List<AgentToolSpec> toolSpecs) {
|
||||
this.eventBridge = eventBridge;
|
||||
this.skillContext = skillContext;
|
||||
this.toolSpecs = (toolSpecs == null ? List.<AgentToolSpec>of() : toolSpecs).stream()
|
||||
.filter(spec -> spec != null && spec.getName() != null && !spec.getName().isBlank())
|
||||
.collect(Collectors.toMap(AgentToolSpec::getName, Function.identity(), (left, right) -> left,
|
||||
LinkedHashMap::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* 观察工具执行前后事件。
|
||||
*
|
||||
* @param event AgentScope Hook 事件
|
||||
* @return 完成信号
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> observe(HookEvent event) {
|
||||
if (event instanceof PreActingEvent preActingEvent) {
|
||||
emitToolCall(preActingEvent);
|
||||
return Mono.empty();
|
||||
}
|
||||
if (event instanceof PostActingEvent postActingEvent) {
|
||||
emitToolResult(postActingEvent);
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
private void emitToolCall(PreActingEvent event) {
|
||||
ToolUseBlock toolUse = event.getToolUse();
|
||||
if (toolUse == null) {
|
||||
return;
|
||||
}
|
||||
if (isSkillTool(toolUse.getName())) {
|
||||
return;
|
||||
}
|
||||
AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.TOOL_CALL);
|
||||
runtimeEvent.setToolCallId(toolUse.getId());
|
||||
runtimeEvent.getPayload().put("toolCallId", toolUse.getId());
|
||||
runtimeEvent.getPayload().put("name", toolUse.getName());
|
||||
runtimeEvent.getPayload().put("toolName", toolUse.getName());
|
||||
runtimeEvent.getPayload().put("status", "RUNNING");
|
||||
enrichToolPayload(runtimeEvent, toolUse.getName());
|
||||
eventBridge.emit(runtimeEvent);
|
||||
}
|
||||
|
||||
private void emitToolResult(PostActingEvent event) {
|
||||
ToolResultBlock result = event.getToolResult();
|
||||
ToolUseBlock toolUse = event.getToolUse();
|
||||
if (result == null && toolUse == null) {
|
||||
return;
|
||||
}
|
||||
String toolCallId = result == null ? toolUse.getId() : result.getId();
|
||||
String toolName = result == null ? toolUse.getName() : result.getName();
|
||||
if (isSkillTool(toolName)) {
|
||||
return;
|
||||
}
|
||||
AgentRuntimeEvent runtimeEvent = eventBridge.event(AgentRuntimeEventType.TOOL_RESULT);
|
||||
runtimeEvent.setToolCallId(toolCallId);
|
||||
runtimeEvent.getPayload().put("toolCallId", toolCallId);
|
||||
runtimeEvent.getPayload().put("name", toolName);
|
||||
runtimeEvent.getPayload().put("toolName", toolName);
|
||||
runtimeEvent.getPayload().put("status", success(result) ? "SUCCESS" : "FAILED");
|
||||
runtimeEvent.getPayload().put("success", success(result));
|
||||
enrichToolPayload(runtimeEvent, toolName);
|
||||
eventBridge.emit(runtimeEvent);
|
||||
}
|
||||
|
||||
private void enrichToolPayload(AgentRuntimeEvent runtimeEvent, String toolName) {
|
||||
AgentToolSpec toolSpec = toolSpecs.get(toolName);
|
||||
if (toolSpec == null) {
|
||||
return;
|
||||
}
|
||||
runtimeEvent.getPayload().put("toolCategory", toolSpec.getCategory().name());
|
||||
if (toolSpec.getMetadata() == null || toolSpec.getMetadata().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, Object> metadata = toolSpec.getMetadata();
|
||||
putIfPresent(runtimeEvent.getPayload(), metadata, "toolDisplayName");
|
||||
putIfPresent(runtimeEvent.getPayload(), metadata, "skillId");
|
||||
if (toolSpec.getCategory() == AgentToolCategory.KNOWLEDGE) {
|
||||
putIfPresent(runtimeEvent.getPayload(), metadata, "knowledgeId");
|
||||
putIfPresent(runtimeEvent.getPayload(), metadata, "knowledgeName");
|
||||
putIfPresent(runtimeEvent.getPayload(), metadata, "knowledgeRuntimeName");
|
||||
}
|
||||
}
|
||||
|
||||
private void putIfPresent(Map<String, Object> payload, Map<String, Object> metadata, String key) {
|
||||
if (metadata.containsKey(key)) {
|
||||
payload.put(key, metadata.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean success(ToolResultBlock result) {
|
||||
if (result == null) {
|
||||
return false;
|
||||
}
|
||||
Object success = result.getMetadata() == null ? null : result.getMetadata().get("success");
|
||||
if (success instanceof Boolean) {
|
||||
return Boolean.TRUE.equals(success);
|
||||
}
|
||||
// AgentScope 1.x 将工具异常转换为不带 success metadata 的 "Error: ..." 文本结果。
|
||||
return result.getOutput().stream()
|
||||
.filter(TextBlock.class::isInstance)
|
||||
.map(TextBlock.class::cast)
|
||||
.map(TextBlock::getText)
|
||||
.noneMatch(text -> text != null && text.startsWith("Error: "));
|
||||
}
|
||||
|
||||
private boolean isSkillTool(String toolName) {
|
||||
if (skillContext == null) {
|
||||
return false;
|
||||
}
|
||||
return skillContext.isSkillLoadTool(toolName) || skillContext.getActiveToolBinding(toolName) != null;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,39 @@
|
||||
package com.easyagents.agent.runtime.hitl;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentResumeRequest;
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 工具审批协调器。
|
||||
*/
|
||||
public class AgentToolApprovalCoordinator {
|
||||
|
||||
/** MCP 工具类型。 */
|
||||
private static final String MCP_TOOL_TYPE = "MCP";
|
||||
|
||||
/** 是否启用内存审批协调。 */
|
||||
private final boolean enabled;
|
||||
private final Map<String, PendingApproval> approvals = new ConcurrentHashMap<>();
|
||||
/** 恢复令牌到待审批项的索引。 */
|
||||
private final Map<String, PendingApproval> approvals = new LinkedHashMap<>();
|
||||
/** 审批批次ID到批次状态的索引。 */
|
||||
private final Map<String, ApprovalBatch> approvalBatches = new LinkedHashMap<>();
|
||||
/** 工具调用ID到恢复令牌的唯一索引。 */
|
||||
private final Map<String, String> tokensByToolCallId = new LinkedHashMap<>();
|
||||
/** 工具调用ID到一次性执行授权的索引。 */
|
||||
private final Map<String, ExecutionAuthorization> executionAuthorizations = new LinkedHashMap<>();
|
||||
/** 当前 Turn 已批准的可复用工具作用域。 */
|
||||
private final Set<String> reusableApprovalScopes = new LinkedHashSet<>();
|
||||
|
||||
/**
|
||||
* 创建已启用的协调器。
|
||||
@@ -64,6 +83,54 @@ public class AgentToolApprovalCoordinator {
|
||||
Map<String, Object> toolInput,
|
||||
Map<String, Object> metadata,
|
||||
Instant expiresAt) {
|
||||
return register(sessionId, agentId, toolCallId, toolName, approvalPrompt, toolInput,
|
||||
metadata, expiresAt, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册一个属于指定审批批次的待审批请求。
|
||||
*
|
||||
* <p>同一批次内的全部工具调用均批准后,协调器才会签发执行授权。任意一项拒绝或
|
||||
* 过期都会关闭整个批次,避免未批准调用跟随已批准调用一起恢复执行。</p>
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @param agentId 智能体ID
|
||||
* @param toolCallId 工具调用ID
|
||||
* @param toolName 工具名称
|
||||
* @param approvalPrompt 审批文案
|
||||
* @param toolInput 工具入参
|
||||
* @param metadata 元数据
|
||||
* @param expiresAt 过期时间
|
||||
* @param approvalBatchId 审批批次ID;为空时创建单调用批次
|
||||
* @return 待审批状态
|
||||
*/
|
||||
public synchronized AgentPendingState register(String sessionId,
|
||||
String agentId,
|
||||
String toolCallId,
|
||||
String toolName,
|
||||
String approvalPrompt,
|
||||
Map<String, Object> toolInput,
|
||||
Map<String, Object> metadata,
|
||||
Instant expiresAt,
|
||||
String approvalBatchId) {
|
||||
if (enabled && (toolCallId == null || toolCallId.isBlank())) {
|
||||
throw new AgentRuntimeException("Approval-required tool call must include toolCallId.");
|
||||
}
|
||||
if (enabled && toolCallId != null && !toolCallId.isBlank()) {
|
||||
String existingToken = tokensByToolCallId.get(toolCallId);
|
||||
PendingApproval existing = existingToken == null ? null : approvals.get(existingToken);
|
||||
if (existing != null && !isExpired(existing.state)) {
|
||||
if (!Objects.equals(existing.state.getToolName(), toolName)
|
||||
|| !Objects.equals(existing.state.getToolInput(), toolInput)) {
|
||||
throw new AgentRuntimeException(
|
||||
"Duplicate toolCallId is bound to a different tool call: " + toolCallId);
|
||||
}
|
||||
return existing.state;
|
||||
}
|
||||
if (existing != null) {
|
||||
closeBatch(existing.batchId, "审批请求已过期。");
|
||||
}
|
||||
}
|
||||
AgentPendingState state = new AgentPendingState();
|
||||
state.setSessionId(sessionId);
|
||||
state.setAgentId(agentId);
|
||||
@@ -71,13 +138,26 @@ public class AgentToolApprovalCoordinator {
|
||||
state.setToolName(toolName);
|
||||
state.setApprovalPrompt(approvalPrompt);
|
||||
state.setToolInput(toolInput);
|
||||
state.setMetadata(metadata);
|
||||
state.setExpiresAt(expiresAt);
|
||||
String token = state.getResumeToken().getValue();
|
||||
String effectiveBatchId = approvalBatchId == null || approvalBatchId.isBlank()
|
||||
? token
|
||||
: approvalBatchId;
|
||||
Map<String, Object> effectiveMetadata = metadata == null
|
||||
? new LinkedHashMap<>()
|
||||
: new LinkedHashMap<>(metadata);
|
||||
effectiveMetadata.put("approvalBatchId", effectiveBatchId);
|
||||
state.setMetadata(effectiveMetadata);
|
||||
if (enabled) {
|
||||
String token = state.getResumeToken().getValue();
|
||||
PendingApproval pendingApproval = new PendingApproval(state, new CompletableFuture<>());
|
||||
PendingApproval pendingApproval = new PendingApproval(
|
||||
state, effectiveBatchId, new CompletableFuture<>());
|
||||
approvals.put(token, pendingApproval);
|
||||
pendingApproval.future.whenComplete((response, error) -> approvals.remove(token));
|
||||
ApprovalBatch batch = approvalBatches.computeIfAbsent(effectiveBatchId, ApprovalBatch::new);
|
||||
batch.tokens.add(token);
|
||||
batch.members.put(token, pendingApproval);
|
||||
if (toolCallId != null && !toolCallId.isBlank()) {
|
||||
tokensByToolCallId.put(toolCallId, token);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@@ -86,14 +166,14 @@ public class AgentToolApprovalCoordinator {
|
||||
* 等待指定审批令牌的响应。
|
||||
*
|
||||
* @param resumeToken 恢复令牌
|
||||
* @return 审批响应
|
||||
* @return 恢复请求
|
||||
*/
|
||||
public Mono<AgentToolApprovalResponse> await(AgentResumeToken resumeToken) {
|
||||
public synchronized Mono<AgentResumeRequest> await(AgentResumeToken resumeToken) {
|
||||
if (!enabled) {
|
||||
AgentToolApprovalResponse response = new AgentToolApprovalResponse();
|
||||
response.setResumeToken(resumeToken);
|
||||
response.setApproved(true);
|
||||
return Mono.just(response);
|
||||
AgentResumeRequest request = new AgentResumeRequest();
|
||||
request.setResumeToken(resumeToken);
|
||||
request.setApproved(true);
|
||||
return Mono.just(request);
|
||||
}
|
||||
if (resumeToken == null || resumeToken.getValue() == null || resumeToken.getValue().isBlank()) {
|
||||
return Mono.error(new AgentToolApprovalRejectedException("缺少审批令牌。"));
|
||||
@@ -102,23 +182,260 @@ public class AgentToolApprovalCoordinator {
|
||||
if (pendingApproval == null) {
|
||||
return Mono.error(new AgentToolApprovalRejectedException("审批请求已失效。"));
|
||||
}
|
||||
if (isExpired(pendingApproval.state)) {
|
||||
closeBatch(pendingApproval.batchId, "审批请求已过期。");
|
||||
return Mono.error(new AgentToolApprovalRejectedException("审批请求已过期。"));
|
||||
}
|
||||
return Mono.fromFuture(pendingApproval.future);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交审批响应。
|
||||
* 处理恢复请求并生成批次级审批决议。
|
||||
*
|
||||
* @param response 审批响应
|
||||
* @param request 恢复请求
|
||||
* @return 审批决议
|
||||
*/
|
||||
public void submit(AgentToolApprovalResponse response) {
|
||||
if (!enabled || response == null || response.getResumeToken() == null
|
||||
|| response.getResumeToken().getValue() == null) {
|
||||
public synchronized AgentToolApprovalResolution resolve(AgentResumeRequest request) {
|
||||
validateResumeRequest(request);
|
||||
if (!enabled) {
|
||||
AgentPendingState state = new AgentPendingState();
|
||||
state.setResumeToken(request.getResumeToken());
|
||||
return new AgentToolApprovalResolution(
|
||||
AgentToolApprovalResolution.Status.READY, state, List.of(), null);
|
||||
}
|
||||
String token = request.getResumeToken().getValue();
|
||||
PendingApproval pendingApproval = approvals.get(token);
|
||||
if (pendingApproval == null || pendingApproval.decision != ApprovalDecision.PENDING) {
|
||||
throw new AgentRuntimeException("Agent resume token is invalid, expired, or already consumed.");
|
||||
}
|
||||
if (isExpired(pendingApproval.state)) {
|
||||
closeBatch(pendingApproval.batchId, "审批请求已过期。");
|
||||
return new AgentToolApprovalResolution(
|
||||
AgentToolApprovalResolution.Status.EXPIRED,
|
||||
pendingApproval.state,
|
||||
List.of(),
|
||||
"审批请求已过期。");
|
||||
}
|
||||
|
||||
pendingApproval.decision = request.isApproved()
|
||||
? ApprovalDecision.APPROVED
|
||||
: ApprovalDecision.REJECTED;
|
||||
approvals.remove(token);
|
||||
removeToolCallIndex(pendingApproval.state, token);
|
||||
pendingApproval.future.complete(request);
|
||||
|
||||
if (!request.isApproved()) {
|
||||
String reason = request.getRejectReason() == null || request.getRejectReason().isBlank()
|
||||
? "工具执行已被拒绝。"
|
||||
: request.getRejectReason();
|
||||
closeBatch(pendingApproval.batchId, reason);
|
||||
return new AgentToolApprovalResolution(
|
||||
AgentToolApprovalResolution.Status.REJECTED,
|
||||
pendingApproval.state,
|
||||
List.of(),
|
||||
reason);
|
||||
}
|
||||
|
||||
ApprovalBatch batch = approvalBatches.get(pendingApproval.batchId);
|
||||
if (batch == null) {
|
||||
authorize(pendingApproval);
|
||||
return new AgentToolApprovalResolution(
|
||||
AgentToolApprovalResolution.Status.READY,
|
||||
pendingApproval.state,
|
||||
List.of(),
|
||||
null);
|
||||
}
|
||||
List<AgentPendingState> remainingStates = pendingStatesInBatch(batch);
|
||||
AgentPendingState expiredState = remainingStates.stream()
|
||||
.filter(this::isExpired)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (expiredState != null) {
|
||||
closeBatch(batch.batchId, "审批请求已过期。");
|
||||
return new AgentToolApprovalResolution(
|
||||
AgentToolApprovalResolution.Status.EXPIRED,
|
||||
pendingApproval.state,
|
||||
List.of(),
|
||||
"审批请求已过期。");
|
||||
}
|
||||
if (!remainingStates.isEmpty()) {
|
||||
return new AgentToolApprovalResolution(
|
||||
AgentToolApprovalResolution.Status.WAITING,
|
||||
pendingApproval.state,
|
||||
remainingStates,
|
||||
null);
|
||||
}
|
||||
|
||||
for (String batchToken : batch.tokens) {
|
||||
PendingApproval member = batch.members.get(batchToken);
|
||||
if (member != null && member.decision == ApprovalDecision.APPROVED) {
|
||||
authorize(member);
|
||||
}
|
||||
}
|
||||
approvalBatches.remove(batch.batchId);
|
||||
return new AgentToolApprovalResolution(
|
||||
AgentToolApprovalResolution.Status.READY,
|
||||
pendingApproval.state,
|
||||
List.of(),
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 消费恢复请求对应的待审批状态。
|
||||
*
|
||||
* <p>该兼容入口供工具适配器内部的 await/consume 流程使用。批次中仍有未决调用时
|
||||
* 会拒绝提前消费;单调用批准后会移除无需经过 PreActing 的执行凭证。</p>
|
||||
*
|
||||
* @param request 恢复请求
|
||||
* @return 待审批状态
|
||||
*/
|
||||
public synchronized AgentPendingState consume(AgentResumeRequest request) {
|
||||
AgentToolApprovalResolution resolution = resolve(request);
|
||||
if (resolution.getStatus() == AgentToolApprovalResolution.Status.WAITING) {
|
||||
throw new AgentRuntimeException("Approval batch still has pending tool calls.");
|
||||
}
|
||||
AgentPendingState state = resolution.getResolvedState();
|
||||
if (resolution.getStatus() == AgentToolApprovalResolution.Status.READY
|
||||
&& state != null
|
||||
&& state.getToolCallId() != null) {
|
||||
// 兼容工具适配器内部 await/consume 流程,该流程会直接调用工具,不经过 PreActing 二次校验。
|
||||
executionAuthorizations.remove(state.getToolCallId());
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据服务端持久化审批结果签发受信任的执行授权。
|
||||
*
|
||||
* <p>恢复元数据应提供单个 {@code toolCallId/toolName/toolInput},或通过
|
||||
* {@code approvedToolCalls} 提供上述字段组成的列表。MCP 调用可额外携带受信任的
|
||||
* {@code toolType/mcpId},用于签发当前 Turn 的复用作用域。该入口仅供已经完成
|
||||
* 持久化令牌校验和一次性消费的服务端集成层使用。</p>
|
||||
*
|
||||
* @param request 受信任恢复请求
|
||||
*/
|
||||
public synchronized void authorizeTrustedExecution(AgentResumeRequest request) {
|
||||
validateResumeRequest(request);
|
||||
if (!enabled || !request.isApproved()) {
|
||||
return;
|
||||
}
|
||||
PendingApproval pendingApproval = approvals.get(response.getResumeToken().getValue());
|
||||
if (pendingApproval != null) {
|
||||
pendingApproval.future.complete(response);
|
||||
Map<String, Object> metadata = request.getMetadata() == null
|
||||
? Map.of()
|
||||
: request.getMetadata();
|
||||
Object approvedToolCalls = metadata.get("approvedToolCalls");
|
||||
Map<String, ExecutionAuthorization> trustedAuthorizations = new LinkedHashMap<>();
|
||||
Set<String> trustedReusableScopes = new LinkedHashSet<>();
|
||||
int authorizationCount = 0;
|
||||
if (approvedToolCalls instanceof List<?> calls) {
|
||||
for (Object call : calls) {
|
||||
if (call instanceof Map<?, ?> callMap) {
|
||||
authorizeTrustedCall(callMap, trustedAuthorizations, trustedReusableScopes);
|
||||
authorizationCount++;
|
||||
}
|
||||
}
|
||||
} else if (metadata.containsKey("toolCallId")) {
|
||||
authorizeTrustedCall(metadata, trustedAuthorizations, trustedReusableScopes);
|
||||
authorizationCount++;
|
||||
}
|
||||
if (authorizationCount == 0) {
|
||||
throw new AgentRuntimeException(
|
||||
"Trusted resume metadata must include approved toolCallId, toolName, and toolInput.");
|
||||
}
|
||||
executionAuthorizations.putAll(trustedAuthorizations);
|
||||
reusableApprovalScopes.addAll(trustedReusableScopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 消费指定工具调用的一次性执行授权。
|
||||
*
|
||||
* @param toolCallId 工具调用ID
|
||||
* @param toolName 工具名称
|
||||
* @param toolInput 工具入参
|
||||
*/
|
||||
public synchronized void consumeExecutionAuthorization(String toolCallId,
|
||||
String toolName,
|
||||
Map<String, Object> toolInput) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
if (toolCallId == null || toolCallId.isBlank()) {
|
||||
throw new AgentToolApprovalRejectedException("待执行工具缺少 toolCallId,无法校验审批结果。");
|
||||
}
|
||||
ExecutionAuthorization authorization = executionAuthorizations.remove(toolCallId);
|
||||
if (authorization == null) {
|
||||
throw new AgentToolApprovalRejectedException("工具调用未获得批准或批准已被消费。");
|
||||
}
|
||||
if (!Objects.equals(authorization.toolName, toolName)
|
||||
|| !Objects.equals(authorization.toolInput, toolInput)) {
|
||||
throw new AgentToolApprovalRejectedException("工具调用与已批准内容不一致。");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断工具元数据对应的复用作用域是否已在当前 Turn 获得批准。
|
||||
*
|
||||
* @param metadata 服务端工具元数据
|
||||
* @return 当前 Turn 已批准时为 true
|
||||
*/
|
||||
public synchronized boolean isReusableApprovalGranted(Map<String, Object> metadata) {
|
||||
String approvalScope = reusableApprovalScope(metadata);
|
||||
return approvalScope != null && reusableApprovalScopes.contains(approvalScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析可在当前 Turn 复用的审批作用域。
|
||||
*
|
||||
* <p>MCP 使用稳定 {@code mcpId} 生成作用域;受控 Shell 脚本仅接受动态审批策略写入的
|
||||
* 内容摘要作用域。缺少受信任标识时返回 null,使调用方继续执行逐调用审批。</p>
|
||||
*
|
||||
* @param metadata 服务端工具元数据
|
||||
* @return 可复用审批作用域;不可复用时返回 null
|
||||
*/
|
||||
public String reusableApprovalScope(Map<String, Object> metadata) {
|
||||
if (metadata == null || metadata.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String explicitScope = stringValue(metadata.get("approvalScope"));
|
||||
if (Boolean.TRUE.equals(metadata.get("operateTool"))
|
||||
&& "SHELL".equalsIgnoreCase(stringValue(metadata.get("operateToolType")))
|
||||
&& explicitScope != null
|
||||
&& explicitScope.startsWith("SHELL_SCRIPT:")) {
|
||||
return explicitScope;
|
||||
}
|
||||
String toolType = stringValue(metadata.get("toolType"));
|
||||
String mcpId = stringValue(metadata.get("mcpId"));
|
||||
if (!MCP_TOOL_TYPE.equalsIgnoreCase(toolType) || mcpId == null) {
|
||||
return null;
|
||||
}
|
||||
return MCP_TOOL_TYPE + ":" + mcpId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理尚未消费的一次性执行授权。
|
||||
*/
|
||||
public synchronized void clearExecutionAuthorizations() {
|
||||
executionAuthorizations.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理当前 Turn 的可复用工具审批作用域。
|
||||
*/
|
||||
public synchronized void clearReusableApprovalScopes() {
|
||||
reusableApprovalScopes.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定会话当前仍待处理的审批状态。
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @return 待审批状态快照
|
||||
*/
|
||||
public synchronized List<AgentPendingState> pendingStates(String sessionId) {
|
||||
return approvals.values().stream()
|
||||
.filter(pending -> pending.decision == ApprovalDecision.PENDING)
|
||||
.map(pending -> pending.state)
|
||||
.filter(state -> sessionId == null || Objects.equals(sessionId, state.getSessionId()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,18 +443,17 @@ public class AgentToolApprovalCoordinator {
|
||||
*
|
||||
* @param reason 取消原因
|
||||
*/
|
||||
public void cancelAll(String reason) {
|
||||
public synchronized void cancelAll(String reason) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
for (PendingApproval pendingApproval : approvals.values()) {
|
||||
AgentToolApprovalResponse response = new AgentToolApprovalResponse();
|
||||
response.setResumeToken(pendingApproval.state.getResumeToken());
|
||||
response.setApproved(false);
|
||||
response.setRejectReason(reason);
|
||||
pendingApproval.future.complete(response);
|
||||
for (String batchId : new ArrayList<>(approvalBatches.keySet())) {
|
||||
closeBatch(batchId, reason);
|
||||
}
|
||||
approvals.clear();
|
||||
tokensByToolCallId.clear();
|
||||
executionAuthorizations.clear();
|
||||
reusableApprovalScopes.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,13 +465,250 @@ public class AgentToolApprovalCoordinator {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验恢复请求中的令牌字段。
|
||||
*
|
||||
* @param request 恢复请求
|
||||
*/
|
||||
private void validateResumeRequest(AgentResumeRequest request) {
|
||||
if (request == null || request.getResumeToken() == null
|
||||
|| request.getResumeToken().getValue() == null
|
||||
|| request.getResumeToken().getValue().isBlank()) {
|
||||
throw new AgentRuntimeException("Agent resume token is required.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断待审批状态是否已经过期。
|
||||
*
|
||||
* @param state 待审批状态
|
||||
* @return 已过期时为 true
|
||||
*/
|
||||
private boolean isExpired(AgentPendingState state) {
|
||||
return state != null
|
||||
&& state.getExpiresAt() != null
|
||||
&& !state.getExpiresAt().isAfter(Instant.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取批次内尚未决策的审批状态。
|
||||
*
|
||||
* @param batch 审批批次
|
||||
* @return 未决审批状态
|
||||
*/
|
||||
private List<AgentPendingState> pendingStatesInBatch(ApprovalBatch batch) {
|
||||
List<AgentPendingState> states = new ArrayList<>();
|
||||
for (String token : batch.tokens) {
|
||||
PendingApproval member = batch.members.get(token);
|
||||
if (member != null && member.decision == ApprovalDecision.PENDING) {
|
||||
states.add(member.state);
|
||||
}
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为已批准状态签发一次性执行授权。
|
||||
*
|
||||
* @param pendingApproval 已批准状态
|
||||
*/
|
||||
private void authorize(PendingApproval pendingApproval) {
|
||||
AgentPendingState state = pendingApproval.state;
|
||||
String approvalScope = reusableApprovalScope(state.getMetadata());
|
||||
if (approvalScope != null) {
|
||||
reusableApprovalScopes.add(approvalScope);
|
||||
return;
|
||||
}
|
||||
if (state.getToolCallId() == null || state.getToolCallId().isBlank()) {
|
||||
throw new AgentRuntimeException("Approved tool call is missing toolCallId.");
|
||||
}
|
||||
executionAuthorizations.put(state.getToolCallId(), new ExecutionAuthorization(
|
||||
state.getToolName(),
|
||||
state.getToolInput()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 为服务端持久化审批结果签发一次性执行授权。
|
||||
*
|
||||
* @param callMap 已批准调用元数据
|
||||
* @param trustedAuthorizations 本次恢复待签发的一次性授权集合
|
||||
* @param trustedReusableScopes 本次恢复待签发的可复用作用域集合
|
||||
*/
|
||||
private void authorizeTrustedCall(Map<?, ?> callMap,
|
||||
Map<String, ExecutionAuthorization> trustedAuthorizations,
|
||||
Set<String> trustedReusableScopes) {
|
||||
String toolCallId = stringValue(callMap.get("toolCallId"));
|
||||
String toolName = stringValue(callMap.get("toolName"));
|
||||
if (toolCallId == null || toolName == null) {
|
||||
throw new AgentRuntimeException(
|
||||
"Trusted resume metadata must include non-empty toolCallId and toolName.");
|
||||
}
|
||||
Map<String, Object> toolInput = stringKeyMap(callMap.get("toolInput"));
|
||||
Map<String, Object> scopeMetadata = new LinkedHashMap<>();
|
||||
scopeMetadata.put("toolType", callMap.get("toolType"));
|
||||
scopeMetadata.put("mcpId", callMap.get("mcpId"));
|
||||
scopeMetadata.put("operateTool", callMap.get("operateTool"));
|
||||
scopeMetadata.put("operateToolType", callMap.get("operateToolType"));
|
||||
scopeMetadata.put("approvalScope", callMap.get("approvalScope"));
|
||||
String reusableScope = reusableApprovalScope(scopeMetadata);
|
||||
if (reusableScope != null) {
|
||||
trustedReusableScopes.add(reusableScope);
|
||||
return;
|
||||
}
|
||||
ExecutionAuthorization authorization = new ExecutionAuthorization(toolName, toolInput);
|
||||
ExecutionAuthorization previous = trustedAuthorizations.put(toolCallId, authorization);
|
||||
if (previous != null
|
||||
&& (!Objects.equals(previous.toolName, toolName)
|
||||
|| !Objects.equals(previous.toolInput, toolInput))) {
|
||||
throw new AgentRuntimeException(
|
||||
"Trusted resume contains conflicting tool calls for toolCallId: " + toolCallId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将值转换为非空字符串。
|
||||
*
|
||||
* @param value 原始值
|
||||
* @return 非空字符串;无法转换时返回 null
|
||||
*/
|
||||
private String stringValue(Object value) {
|
||||
if (value == null || String.valueOf(value).isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return String.valueOf(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将任意键 Map 转换为字符串键 Map。
|
||||
*
|
||||
* @param value 原始值
|
||||
* @return 字符串键 Map
|
||||
*/
|
||||
private Map<String, Object> stringKeyMap(Object value) {
|
||||
if (value == null) {
|
||||
return Map.of();
|
||||
}
|
||||
if (!(value instanceof Map<?, ?> source)) {
|
||||
throw new AgentRuntimeException("Trusted resume toolInput must be a map.");
|
||||
}
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<?, ?> entry : source.entrySet()) {
|
||||
if (entry.getKey() != null) {
|
||||
result.put(String.valueOf(entry.getKey()), entry.getValue());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除工具调用ID到审批令牌的索引。
|
||||
*
|
||||
* @param state 待审批状态
|
||||
* @param token 审批令牌
|
||||
*/
|
||||
private void removeToolCallIndex(AgentPendingState state, String token) {
|
||||
if (state.getToolCallId() != null && !state.getToolCallId().isBlank()) {
|
||||
tokensByToolCallId.remove(state.getToolCallId(), token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭审批批次并拒绝尚未决策的审批项。
|
||||
*
|
||||
* @param batchId 审批批次ID
|
||||
* @param reason 关闭原因
|
||||
*/
|
||||
private void closeBatch(String batchId, String reason) {
|
||||
ApprovalBatch batch = approvalBatches.remove(batchId);
|
||||
if (batch == null) {
|
||||
return;
|
||||
}
|
||||
for (String token : batch.tokens) {
|
||||
PendingApproval member = batch.members.get(token);
|
||||
if (member == null) {
|
||||
continue;
|
||||
}
|
||||
approvals.remove(token);
|
||||
removeToolCallIndex(member.state, token);
|
||||
if (member.decision == ApprovalDecision.PENDING) {
|
||||
member.decision = ApprovalDecision.REJECTED;
|
||||
AgentResumeRequest rejection = new AgentResumeRequest();
|
||||
rejection.setResumeToken(member.state.getResumeToken());
|
||||
rejection.setApproved(false);
|
||||
rejection.setRejectReason(reason);
|
||||
member.future.complete(rejection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个审批项的内部决策状态。
|
||||
*/
|
||||
private enum ApprovalDecision {
|
||||
PENDING,
|
||||
APPROVED,
|
||||
REJECTED
|
||||
}
|
||||
|
||||
/**
|
||||
* 待审批项及其异步等待句柄。
|
||||
*/
|
||||
private static class PendingApproval {
|
||||
private final AgentPendingState state;
|
||||
private final CompletableFuture<AgentToolApprovalResponse> future;
|
||||
private final String batchId;
|
||||
private final CompletableFuture<AgentResumeRequest> future;
|
||||
private ApprovalDecision decision = ApprovalDecision.PENDING;
|
||||
|
||||
private PendingApproval(AgentPendingState state, CompletableFuture<AgentToolApprovalResponse> future) {
|
||||
/**
|
||||
* 创建待审批项。
|
||||
*
|
||||
* @param state 待审批状态
|
||||
* @param batchId 审批批次ID
|
||||
* @param future 审批响应等待句柄
|
||||
*/
|
||||
private PendingApproval(AgentPendingState state,
|
||||
String batchId,
|
||||
CompletableFuture<AgentResumeRequest> future) {
|
||||
this.state = Objects.requireNonNull(state, "state");
|
||||
this.batchId = Objects.requireNonNull(batchId, "batchId");
|
||||
this.future = Objects.requireNonNull(future, "future");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同一轮推理产生的审批批次。
|
||||
*/
|
||||
private static class ApprovalBatch {
|
||||
private final String batchId;
|
||||
private final Set<String> tokens = new LinkedHashSet<>();
|
||||
private final Map<String, PendingApproval> members = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 创建审批批次。
|
||||
*
|
||||
* @param batchId 审批批次ID
|
||||
*/
|
||||
private ApprovalBatch(String batchId) {
|
||||
this.batchId = Objects.requireNonNull(batchId, "batchId");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 已批准工具调用的一次性执行凭证。
|
||||
*/
|
||||
private static class ExecutionAuthorization {
|
||||
private final String toolName;
|
||||
private final Map<String, Object> toolInput;
|
||||
|
||||
/**
|
||||
* 创建一次性执行授权。
|
||||
*
|
||||
* @param toolName 工具名称
|
||||
* @param toolInput 工具入参
|
||||
*/
|
||||
private ExecutionAuthorization(String toolName, Map<String, Object> toolInput) {
|
||||
this.toolName = toolName;
|
||||
this.toolInput = toolInput == null ? Map.of() : new LinkedHashMap<>(toolInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.easyagents.agent.runtime.hitl;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 单次工具调用的动态审批判定。
|
||||
*
|
||||
* @param valid 调用是否通过审批前静态校验
|
||||
* @param approvalRequired 是否需要人工审批
|
||||
* @param forced 是否禁止复用既有审批
|
||||
* @param reusableScope 可复用审批作用域;为空表示逐调用审批
|
||||
* @param metadata 写入审批事件的受信任元数据
|
||||
*/
|
||||
public record AgentToolApprovalEvaluation(
|
||||
boolean valid,
|
||||
boolean approvalRequired,
|
||||
boolean forced,
|
||||
String reusableScope,
|
||||
Map<String, Object> metadata) {
|
||||
|
||||
/**
|
||||
* 创建审批前静态校验失败的判定。
|
||||
*
|
||||
* @return 无需弹出审批的无效判定
|
||||
*/
|
||||
public static AgentToolApprovalEvaluation invalid() {
|
||||
return new AgentToolApprovalEvaluation(false, false, false, null, Map.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建通过静态校验的判定。
|
||||
*
|
||||
* @param approvalRequired 是否需要审批
|
||||
* @param forced 是否强制逐调用审批
|
||||
* @param reusableScope 可复用作用域
|
||||
* @return 动态审批判定
|
||||
*/
|
||||
public static AgentToolApprovalEvaluation valid(boolean approvalRequired,
|
||||
boolean forced,
|
||||
String reusableScope) {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
if (reusableScope != null && !reusableScope.isBlank()) {
|
||||
metadata.put("approvalScope", reusableScope);
|
||||
}
|
||||
return new AgentToolApprovalEvaluation(
|
||||
true, approvalRequired, forced, reusableScope, Map.copyOf(metadata));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.easyagents.agent.runtime.hitl;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 根据单次工具入参执行审批前校验并计算动态审批策略。
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface AgentToolApprovalPolicy {
|
||||
|
||||
/**
|
||||
* 评估一次工具调用。
|
||||
*
|
||||
* @param toolInput 工具调用入参
|
||||
* @return 动态审批判定
|
||||
*/
|
||||
AgentToolApprovalEvaluation evaluate(Map<String, Object> toolInput);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.easyagents.agent.runtime.hitl;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 工具审批决议。
|
||||
*/
|
||||
public final class AgentToolApprovalResolution {
|
||||
|
||||
/**
|
||||
* 审批决议状态。
|
||||
*/
|
||||
public enum Status {
|
||||
/**
|
||||
* 当前审批批次仍有待处理调用。
|
||||
*/
|
||||
WAITING,
|
||||
|
||||
/**
|
||||
* 当前审批批次已全部批准,可以恢复执行。
|
||||
*/
|
||||
READY,
|
||||
|
||||
/**
|
||||
* 当前审批批次已被拒绝。
|
||||
*/
|
||||
REJECTED,
|
||||
|
||||
/**
|
||||
* 当前审批批次已过期。
|
||||
*/
|
||||
EXPIRED
|
||||
}
|
||||
|
||||
private final Status status;
|
||||
private final AgentPendingState resolvedState;
|
||||
private final List<AgentPendingState> remainingStates;
|
||||
private final String reason;
|
||||
|
||||
/**
|
||||
* 创建工具审批决议。
|
||||
*
|
||||
* @param status 决议状态
|
||||
* @param resolvedState 本次处理的待审批状态
|
||||
* @param remainingStates 同一批次剩余的待审批状态
|
||||
* @param reason 拒绝或过期原因
|
||||
*/
|
||||
public AgentToolApprovalResolution(Status status,
|
||||
AgentPendingState resolvedState,
|
||||
List<AgentPendingState> remainingStates,
|
||||
String reason) {
|
||||
this.status = status;
|
||||
this.resolvedState = resolvedState;
|
||||
this.remainingStates = remainingStates == null ? List.of() : List.copyOf(remainingStates);
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取决议状态。
|
||||
*
|
||||
* @return 决议状态
|
||||
*/
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本次处理的待审批状态。
|
||||
*
|
||||
* @return 待审批状态
|
||||
*/
|
||||
public AgentPendingState getResolvedState() {
|
||||
return resolvedState;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取同一批次剩余的待审批状态。
|
||||
*
|
||||
* @return 剩余待审批状态
|
||||
*/
|
||||
public List<AgentPendingState> getRemainingStates() {
|
||||
return remainingStates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取拒绝或过期原因。
|
||||
*
|
||||
* @return 原因
|
||||
*/
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.easyagents.agent.runtime.hitl;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 工具执行审批响应。
|
||||
*/
|
||||
public class AgentToolApprovalResponse {
|
||||
|
||||
private AgentResumeToken resumeToken;
|
||||
private boolean approved;
|
||||
private String rejectReason;
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 获取恢复令牌。
|
||||
*
|
||||
* @return 恢复令牌
|
||||
*/
|
||||
public AgentResumeToken getResumeToken() {
|
||||
return resumeToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置恢复令牌。
|
||||
*
|
||||
* @param resumeToken 恢复令牌
|
||||
*/
|
||||
public void setResumeToken(AgentResumeToken resumeToken) {
|
||||
this.resumeToken = resumeToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回是否批准执行工具。
|
||||
*
|
||||
* @return 批准时为 true
|
||||
*/
|
||||
public boolean isApproved() {
|
||||
return approved;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否批准执行工具。
|
||||
*
|
||||
* @param approved 批准标记
|
||||
*/
|
||||
public void setApproved(boolean approved) {
|
||||
this.approved = approved;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取拒绝原因。
|
||||
*
|
||||
* @return 拒绝原因
|
||||
*/
|
||||
public String getRejectReason() {
|
||||
return rejectReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置拒绝原因。
|
||||
*
|
||||
* @param rejectReason 拒绝原因
|
||||
*/
|
||||
public void setRejectReason(String rejectReason) {
|
||||
this.rejectReason = rejectReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审批元数据。
|
||||
*
|
||||
* @return 审批元数据
|
||||
*/
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置审批元数据。
|
||||
*
|
||||
* @param metadata 审批元数据
|
||||
*/
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.easyagents.agent.runtime.knowledge;
|
||||
|
||||
/**
|
||||
* 知识库检索策略。
|
||||
*/
|
||||
public enum AgentKnowledgePolicy {
|
||||
AGENTIC,
|
||||
GENERIC,
|
||||
DISABLED
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.easyagents.agent.runtime.knowledge;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 知识库声明与检索器的运行时绑定。
|
||||
*/
|
||||
public final class AgentKnowledgeRegistration {
|
||||
|
||||
private final AgentKnowledgeSpec knowledgeSpec;
|
||||
private final AgentKnowledgeRetriever retriever;
|
||||
|
||||
/**
|
||||
* 创建知识库运行时绑定。
|
||||
*
|
||||
* @param knowledgeSpec 知识库声明
|
||||
* @param retriever 知识库检索器
|
||||
* @throws NullPointerException 声明或检索器为空时抛出
|
||||
*/
|
||||
public AgentKnowledgeRegistration(AgentKnowledgeSpec knowledgeSpec,
|
||||
AgentKnowledgeRetriever retriever) {
|
||||
this.knowledgeSpec = Objects.requireNonNull(knowledgeSpec, "knowledgeSpec");
|
||||
this.retriever = Objects.requireNonNull(retriever, "retriever");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库声明。
|
||||
*
|
||||
* @return 知识库声明
|
||||
*/
|
||||
public AgentKnowledgeSpec getKnowledgeSpec() {
|
||||
return knowledgeSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库检索器。
|
||||
*
|
||||
* @return 知识库检索器
|
||||
*/
|
||||
public AgentKnowledgeRetriever getRetriever() {
|
||||
return retriever;
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,9 @@ import java.util.Map;
|
||||
public class AgentKnowledgeSpec {
|
||||
|
||||
private String knowledgeId;
|
||||
private String runtimeName;
|
||||
private String name;
|
||||
private String description;
|
||||
private AgentKnowledgePolicy retrievalMode = AgentKnowledgePolicy.AGENTIC;
|
||||
private int limit = 5;
|
||||
private double scoreThreshold = 0D;
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
@@ -34,6 +34,24 @@ public class AgentKnowledgeSpec {
|
||||
this.knowledgeId = knowledgeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调用方提供的英文运行名。
|
||||
*
|
||||
* @return 英文运行名
|
||||
*/
|
||||
public String getRuntimeName() {
|
||||
return runtimeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置调用方提供的英文运行名。
|
||||
*
|
||||
* @param runtimeName 英文运行名
|
||||
*/
|
||||
public void setRuntimeName(String runtimeName) {
|
||||
this.runtimeName = runtimeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库名称。
|
||||
*
|
||||
@@ -70,24 +88,6 @@ public class AgentKnowledgeSpec {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检索模式。
|
||||
*
|
||||
* @return 检索模式
|
||||
*/
|
||||
public AgentKnowledgePolicy getRetrievalMode() {
|
||||
return retrievalMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置检索模式。
|
||||
*
|
||||
* @param retrievalMode 检索模式
|
||||
*/
|
||||
public void setRetrievalMode(AgentKnowledgePolicy retrievalMode) {
|
||||
this.retrievalMode = retrievalMode == null ? AgentKnowledgePolicy.AGENTIC : retrievalMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取限制数量。
|
||||
*
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.easyagents.agent.runtime.knowledge;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Agent 知识库工具名称规范。
|
||||
*/
|
||||
public final class AgentKnowledgeToolNames {
|
||||
|
||||
/**
|
||||
* 知识库工具名称前缀。
|
||||
*/
|
||||
public static final String PREFIX = "retrieve_knowledge_";
|
||||
|
||||
/**
|
||||
* OpenAI-compatible Function Call 的通用名称长度上限。
|
||||
*/
|
||||
public static final int MAX_TOOL_NAME_LENGTH = 64;
|
||||
|
||||
private static final int HASH_LENGTH = 8;
|
||||
private static final Pattern SAFE_RUNTIME_NAME = Pattern.compile("^[A-Za-z0-9_-]+$");
|
||||
|
||||
/**
|
||||
* 禁止实例化工具类。
|
||||
*/
|
||||
private AgentKnowledgeToolNames() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据调用方运行名生成稳定的知识库工具名。
|
||||
*
|
||||
* <p>超长名称保留可读前缀并追加稳定短哈希,避免不同模型服务对 Function Call
|
||||
* 名称长度限制不一致。</p>
|
||||
*
|
||||
* @param runtimeName 调用方提供的英文运行名
|
||||
* @return 合法且稳定的工具名
|
||||
* @throws AgentRuntimeException 运行名为空或包含非法字符时抛出
|
||||
*/
|
||||
public static String build(String runtimeName) {
|
||||
String normalized = runtimeName == null ? "" : runtimeName.trim();
|
||||
if (normalized.isEmpty()) {
|
||||
throw new AgentRuntimeException("Knowledge runtime name is required.");
|
||||
}
|
||||
if (!SAFE_RUNTIME_NAME.matcher(normalized).matches()) {
|
||||
throw new AgentRuntimeException(
|
||||
"Knowledge runtime name must contain only letters, numbers, underscores, or hyphens: "
|
||||
+ normalized);
|
||||
}
|
||||
String toolName = PREFIX + normalized;
|
||||
if (toolName.length() <= MAX_TOOL_NAME_LENGTH) {
|
||||
return toolName;
|
||||
}
|
||||
String hash = shortHash(normalized);
|
||||
int readableLength = MAX_TOOL_NAME_LENGTH - PREFIX.length() - HASH_LENGTH - 1;
|
||||
return PREFIX + normalized.substring(0, readableLength) + "_" + hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算用于超长工具名消歧的稳定短哈希。
|
||||
*
|
||||
* @param value 原始运行名
|
||||
* @return 八位十六进制哈希
|
||||
*/
|
||||
private static String shortHash(String value) {
|
||||
try {
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256")
|
||||
.digest(value.getBytes(StandardCharsets.UTF_8));
|
||||
return HexFormat.of().formatHex(digest).substring(0, HASH_LENGTH);
|
||||
} catch (NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException("SHA-256 is unavailable.", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.easyagents.agent.runtime.knowledge.citation;
|
||||
|
||||
import com.easyagents.agent.runtime.message.AgentKnowledgeReference;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 知识库引用匹配器。
|
||||
*
|
||||
* <p>该接口只负责从最终答案和本轮检索候选中选择可展示的引用。实现不应修改答案文本,
|
||||
* 也不应把引用写入 AgentScope memory/session。</p>
|
||||
*/
|
||||
public interface AgentKnowledgeCitationMatcher {
|
||||
|
||||
/**
|
||||
* 匹配最终答案中可由候选知识片段支撑的引用。
|
||||
*
|
||||
* @param answerText 最终答案文本
|
||||
* @param candidates 本轮检索候选引用
|
||||
* @return 匹配到的知识库引用
|
||||
*/
|
||||
List<AgentKnowledgeReference> match(String answerText, Collection<AgentKnowledgeReference> candidates);
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package com.easyagents.agent.runtime.knowledge.citation;
|
||||
|
||||
import com.easyagents.agent.runtime.message.AgentKnowledgeReference;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 基于文本证据的启发式知识库引用匹配器。
|
||||
*
|
||||
* <p>该实现用于模型没有显式输出引用 ID 的场景。它只能说明答案文本与某些检索片段
|
||||
* 存在较强文本支撑关系,不能证明 LLM 真实使用了该片段。因此匹配策略保持保守:
|
||||
* 宁可少返回引用,也不为了“看起来有引用”而强行猜测。</p>
|
||||
*/
|
||||
public class HeuristicKnowledgeCitationMatcher implements AgentKnowledgeCitationMatcher {
|
||||
|
||||
private static final int MIN_NORMALIZED_ANSWER_LENGTH = 4;
|
||||
private static final int MIN_CONTENT_LENGTH = 6;
|
||||
private static final int MAX_REFERENCES = 5;
|
||||
private static final double MIN_SUPPORT_SCORE = 0.42D;
|
||||
|
||||
/**
|
||||
* 根据最终答案和候选知识片段匹配引用。
|
||||
*
|
||||
* @param answerText 最终答案文本
|
||||
* @param candidates 本轮检索候选引用
|
||||
* @return 高置信引用列表
|
||||
*/
|
||||
@Override
|
||||
public List<AgentKnowledgeReference> match(String answerText, Collection<AgentKnowledgeReference> candidates) {
|
||||
if (candidates == null || candidates.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
String normalizedAnswer = normalize(answerText);
|
||||
if (normalizedAnswer.length() < MIN_NORMALIZED_ANSWER_LENGTH) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> normalizedSegments = normalizeSegments(answerText);
|
||||
List<ScoredKnowledgeReference> scoredReferences = new ArrayList<>();
|
||||
for (AgentKnowledgeReference candidate : candidates) {
|
||||
String normalizedContent = normalize(candidate == null ? null : candidate.getChunkContent());
|
||||
double supportScore = supportScore(normalizedAnswer, normalizedContent);
|
||||
// 长篇汇总回答中,每条证据通常只支撑一个段落或条目。继续只用整篇答案作分母,
|
||||
// 会随着主题增多把有效引用的重合度稀释到阈值以下。
|
||||
for (String normalizedSegment : normalizedSegments) {
|
||||
supportScore = Math.max(supportScore, supportScore(normalizedSegment, normalizedContent));
|
||||
}
|
||||
if (supportScore >= MIN_SUPPORT_SCORE) {
|
||||
scoredReferences.add(new ScoredKnowledgeReference(candidate, supportScore));
|
||||
}
|
||||
}
|
||||
scoredReferences.sort(Comparator.comparingDouble(ScoredKnowledgeReference::score).reversed());
|
||||
return scoredReferences.stream()
|
||||
.limit(MAX_REFERENCES)
|
||||
.map(ScoredKnowledgeReference::reference)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将答案切分为可独立核验的段落或句子并完成归一化。
|
||||
*
|
||||
* @param answerText 最终答案文本
|
||||
* @return 非空的归一化答案片段
|
||||
*/
|
||||
private List<String> normalizeSegments(String answerText) {
|
||||
if (answerText == null || answerText.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return Arrays.stream(answerText.split("[\\r\\n。!?!?;;]+"))
|
||||
.map(this::normalize)
|
||||
.filter(segment -> segment.length() >= MIN_NORMALIZED_ANSWER_LENGTH)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算答案与候选片段之间的文本支撑分。
|
||||
*
|
||||
* @param normalizedAnswer 归一化后的答案
|
||||
* @param normalizedContent 归一化后的候选片段
|
||||
* @return 支撑分
|
||||
*/
|
||||
private double supportScore(String normalizedAnswer, String normalizedContent) {
|
||||
if (normalizedAnswer.isBlank()
|
||||
|| normalizedContent.isBlank()
|
||||
|| normalizedContent.length() < MIN_CONTENT_LENGTH) {
|
||||
return 0D;
|
||||
}
|
||||
if (normalizedContent.contains(normalizedAnswer)) {
|
||||
return 1D;
|
||||
}
|
||||
int longestCommon = longestCommonSubstringLength(normalizedAnswer, normalizedContent);
|
||||
double gramOverlap = gramOverlapRatio(charGrams(normalizedAnswer, 2), charGrams(normalizedContent, 2));
|
||||
double numericCoverage = numericCoverage(normalizedAnswer, normalizedContent);
|
||||
|
||||
// 长连续文本片段比零散关键词更能说明答案来自该 chunk。
|
||||
if (longestCommon >= 8 && gramOverlap >= 0.12D) {
|
||||
double numericBoost = numericCoverage >= 0.8D ? 0.12D : numericCoverage * 0.08D;
|
||||
return Math.max(0.55D, gramOverlap + Math.min(0.35D, longestCommon / 60D) + numericBoost);
|
||||
}
|
||||
if (gramOverlap >= 0.28D && longestCommon >= 5) {
|
||||
return gramOverlap + Math.min(0.25D, longestCommon / 80D);
|
||||
}
|
||||
return gramOverlap * 0.7D + Math.min(0.2D, longestCommon / 80D) + numericCoverage * 0.08D;
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化用于引用匹配的文本。
|
||||
*
|
||||
* @param text 原始文本
|
||||
* @return 低噪声文本
|
||||
*/
|
||||
private String normalize(String text) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
return text.toLowerCase()
|
||||
.replace('至', '到')
|
||||
.replaceAll("[^\\p{IsHan}a-z0-9]", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成字符 ngram。
|
||||
*
|
||||
* @param text 文本
|
||||
* @param size ngram 长度
|
||||
* @return ngram 集合
|
||||
*/
|
||||
private Set<String> charGrams(String text, int size) {
|
||||
Set<String> grams = new HashSet<>();
|
||||
if (text == null || text.length() < size) {
|
||||
return grams;
|
||||
}
|
||||
for (int i = 0; i <= text.length() - size; i++) {
|
||||
grams.add(text.substring(i, i + size));
|
||||
}
|
||||
return grams;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算答案 ngram 被候选片段覆盖的比例。
|
||||
*
|
||||
* @param answerGrams 答案 ngram
|
||||
* @param contentGrams 候选片段 ngram
|
||||
* @return 覆盖比例
|
||||
*/
|
||||
private double gramOverlapRatio(Set<String> answerGrams, Set<String> contentGrams) {
|
||||
if (answerGrams == null || answerGrams.isEmpty() || contentGrams == null || contentGrams.isEmpty()) {
|
||||
return 0D;
|
||||
}
|
||||
int matched = 0;
|
||||
for (String gram : answerGrams) {
|
||||
if (contentGrams.contains(gram)) {
|
||||
matched++;
|
||||
}
|
||||
}
|
||||
return (double) matched / (double) answerGrams.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算答案中的数字片段被候选片段覆盖的比例。
|
||||
*
|
||||
* <p>数字只能作为弱加权因素,不能单独造成高置信引用,避免日期、章节号等噪声误判。</p>
|
||||
*
|
||||
* @param normalizedAnswer 归一化后的答案
|
||||
* @param normalizedContent 归一化后的候选片段
|
||||
* @return 数字覆盖比例
|
||||
*/
|
||||
private double numericCoverage(String normalizedAnswer, String normalizedContent) {
|
||||
List<String> numbers = extractNumericTokens(normalizedAnswer);
|
||||
if (numbers.isEmpty()) {
|
||||
return 0D;
|
||||
}
|
||||
int matched = 0;
|
||||
for (String number : numbers) {
|
||||
if (normalizedContent.contains(number)) {
|
||||
matched++;
|
||||
}
|
||||
}
|
||||
return (double) matched / (double) numbers.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取数字片段。
|
||||
*
|
||||
* @param text 文本
|
||||
* @return 数字片段
|
||||
*/
|
||||
private List<String> extractNumericTokens(String text) {
|
||||
List<String> numbers = new ArrayList<>();
|
||||
if (text == null || text.isBlank()) {
|
||||
return numbers;
|
||||
}
|
||||
java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("\\d+").matcher(text);
|
||||
while (matcher.find()) {
|
||||
numbers.add(matcher.group());
|
||||
}
|
||||
return numbers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算最长公共子串长度。
|
||||
*
|
||||
* @param first 第一段文本
|
||||
* @param second 第二段文本
|
||||
* @return 最长公共子串长度
|
||||
*/
|
||||
private int longestCommonSubstringLength(String first, String second) {
|
||||
if (first == null || second == null || first.isEmpty() || second.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int[] previous = new int[second.length() + 1];
|
||||
int max = 0;
|
||||
for (int i = 1; i <= first.length(); i++) {
|
||||
int[] current = new int[second.length() + 1];
|
||||
for (int j = 1; j <= second.length(); j++) {
|
||||
if (first.charAt(i - 1) == second.charAt(j - 1)) {
|
||||
current[j] = previous[j - 1] + 1;
|
||||
max = Math.max(max, current[j]);
|
||||
}
|
||||
}
|
||||
previous = current;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
private record ScoredKnowledgeReference(AgentKnowledgeReference reference, double score) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.easyagents.agent.runtime.mcp;
|
||||
|
||||
import io.agentscope.core.tool.mcp.McpClientWrapper;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 为 MCP client 增加运行时工具别名。
|
||||
*/
|
||||
class AliasedMcpClientWrapper extends McpClientWrapper {
|
||||
|
||||
static final String RAW_TOOL_NAME_META_KEY = "easyagentsRawMcpToolName";
|
||||
|
||||
private final McpClientWrapper delegate;
|
||||
private final Map<String, String> rawToAlias;
|
||||
private final Map<String, String> aliasToRaw;
|
||||
private final String toolNamePrefix;
|
||||
|
||||
/**
|
||||
* 创建 MCP client 别名包装器。
|
||||
*
|
||||
* @param delegate 原始 MCP client
|
||||
* @param rawToAlias 原始工具名到运行时工具名的映射
|
||||
*/
|
||||
AliasedMcpClientWrapper(McpClientWrapper delegate, Map<String, String> rawToAlias) {
|
||||
this(delegate, rawToAlias, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 MCP client 别名包装器。
|
||||
*
|
||||
* @param delegate 原始 MCP client
|
||||
* @param rawToAlias 原始工具名到运行时工具名的映射
|
||||
* @param toolNamePrefix 动态工具名前缀
|
||||
*/
|
||||
AliasedMcpClientWrapper(McpClientWrapper delegate, Map<String, String> rawToAlias, String toolNamePrefix) {
|
||||
super(delegate == null ? "mcp" : delegate.getName());
|
||||
this.delegate = delegate;
|
||||
this.rawToAlias = rawToAlias == null ? Map.of() : new LinkedHashMap<>(rawToAlias);
|
||||
this.aliasToRaw = new LinkedHashMap<>();
|
||||
this.toolNamePrefix = toolNamePrefix == null || toolNamePrefix.isBlank() ? null : toolNamePrefix.trim();
|
||||
this.rawToAlias.forEach((rawName, aliasName) -> {
|
||||
if (rawName != null && aliasName != null && !aliasName.isBlank()) {
|
||||
this.aliasToRaw.put(aliasName, rawName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化底层 MCP client。
|
||||
*
|
||||
* @return 初始化完成信号
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> initialize() {
|
||||
return delegate.initialize().doOnSuccess(ignored -> initialized = delegate.isInitialized());
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回已替换为运行时别名的工具列表。
|
||||
*
|
||||
* @return 工具列表
|
||||
*/
|
||||
@Override
|
||||
public Mono<List<McpSchema.Tool>> listTools() {
|
||||
return delegate.listTools().map(this::aliasTools);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 MCP 工具,运行时别名会映射回原始工具名。
|
||||
*
|
||||
* @param toolName 运行时工具名
|
||||
* @param arguments 工具参数
|
||||
* @return 工具调用结果
|
||||
*/
|
||||
@Override
|
||||
public Mono<McpSchema.CallToolResult> callTool(String toolName, Map<String, Object> arguments) {
|
||||
return delegate.callTool(rawToolName(toolName), arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭底层 MCP client。
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
delegate.close();
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
private List<McpSchema.Tool> aliasTools(List<McpSchema.Tool> tools) {
|
||||
if (tools == null || tools.isEmpty()) {
|
||||
cachedTools.clear();
|
||||
return List.of();
|
||||
}
|
||||
List<McpSchema.Tool> aliased = new ArrayList<>();
|
||||
cachedTools.clear();
|
||||
Map<String, String> usedAliases = new LinkedHashMap<>();
|
||||
for (McpSchema.Tool tool : tools) {
|
||||
if (tool == null) {
|
||||
continue;
|
||||
}
|
||||
McpSchema.Tool aliasTool = aliasTool(tool, usedAliases);
|
||||
cachedTools.put(aliasTool.name(), aliasTool);
|
||||
aliased.add(aliasTool);
|
||||
}
|
||||
return aliased;
|
||||
}
|
||||
|
||||
private McpSchema.Tool aliasTool(McpSchema.Tool tool, Map<String, String> usedAliases) {
|
||||
String aliasName = uniqueAliasName(aliasName(tool.name()), tool.name(), usedAliases);
|
||||
if (aliasName == null || aliasName.isBlank() || aliasName.equals(tool.name())) {
|
||||
return tool;
|
||||
}
|
||||
aliasToRaw.put(aliasName, tool.name());
|
||||
Map<String, Object> meta = new LinkedHashMap<>();
|
||||
if (tool.meta() != null) {
|
||||
meta.putAll(tool.meta());
|
||||
}
|
||||
meta.put(RAW_TOOL_NAME_META_KEY, tool.name());
|
||||
return new McpSchema.Tool(aliasName, tool.title(), tool.description(), tool.inputSchema(),
|
||||
tool.outputSchema(), tool.annotations(), meta);
|
||||
}
|
||||
|
||||
private String uniqueAliasName(String aliasName, String rawName, Map<String, String> usedAliases) {
|
||||
if (aliasName == null || aliasName.isBlank()) {
|
||||
return aliasName;
|
||||
}
|
||||
String existingRawName = usedAliases.get(aliasName);
|
||||
if (existingRawName == null || existingRawName.equals(rawName)) {
|
||||
usedAliases.put(aliasName, rawName);
|
||||
return aliasName;
|
||||
}
|
||||
int suffix = 2;
|
||||
String candidate = aliasName + "_" + suffix;
|
||||
while (usedAliases.containsKey(candidate)) {
|
||||
suffix++;
|
||||
candidate = aliasName + "_" + suffix;
|
||||
}
|
||||
usedAliases.put(candidate, rawName);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private String aliasName(String rawName) {
|
||||
String explicitAlias = rawToAlias.get(rawName);
|
||||
if (explicitAlias != null && !explicitAlias.isBlank()) {
|
||||
return explicitAlias;
|
||||
}
|
||||
if (toolNamePrefix == null) {
|
||||
return rawName;
|
||||
}
|
||||
return toolNamePrefix + safeToolNameSegment(rawName);
|
||||
}
|
||||
|
||||
private String safeToolNameSegment(String value) {
|
||||
String normalized = String.valueOf(value == null ? "" : value).trim()
|
||||
.replaceAll("[^A-Za-z0-9_-]", "_")
|
||||
.replaceAll("_+", "_");
|
||||
if (normalized.isBlank()) {
|
||||
return "tool";
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String rawToolName(String toolName) {
|
||||
return aliasToRaw.getOrDefault(toolName, toolName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.easyagents.agent.runtime.mcp;
|
||||
|
||||
import io.agentscope.core.tool.mcp.McpClientWrapper;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 将一次验证通过的远端 MCP Tool 清单冻结为只读白名单视图。
|
||||
*/
|
||||
final class FrozenMcpClientWrapper extends McpClientWrapper {
|
||||
|
||||
private final McpClientWrapper delegate;
|
||||
private final List<McpSchema.Tool> frozenTools;
|
||||
private final Map<String, String> runtimeToRaw = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 创建冻结 MCP client 视图。
|
||||
*
|
||||
* @param delegate 原始 client
|
||||
* @param actualTools 已一次性读取并验证的远端 Tool
|
||||
* @param manifest 冻结清单
|
||||
* @param aliases 显式运行别名
|
||||
* @param prefix 运行名前缀
|
||||
*/
|
||||
FrozenMcpClientWrapper(McpClientWrapper delegate,
|
||||
List<McpSchema.Tool> actualTools,
|
||||
List<McpToolManifestEntry> manifest,
|
||||
Map<String, String> aliases,
|
||||
String prefix) {
|
||||
super(delegate == null ? "mcp" : delegate.getName());
|
||||
this.delegate = delegate;
|
||||
this.frozenTools = freeze(actualTools, manifest, aliases, prefix);
|
||||
this.frozenTools.forEach(tool -> cachedTools.put(tool.name(), tool));
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public Mono<Void> initialize() {
|
||||
return delegate.initialize().doOnSuccess(ignored -> initialized = delegate.isInitialized());
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public Mono<List<McpSchema.Tool>> listTools() {
|
||||
return Mono.just(frozenTools);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public Mono<McpSchema.CallToolResult> callTool(String toolName, Map<String, Object> arguments) {
|
||||
return delegate.callTool(runtimeToRaw.getOrDefault(toolName, toolName), arguments);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public void close() {
|
||||
delegate.close();
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按冻结 manifest 顺序裁剪并应用稳定运行别名。
|
||||
*
|
||||
* @param actualTools 远端当前 Tool
|
||||
* @param manifest 冻结清单
|
||||
* @param aliases 显式别名
|
||||
* @param prefix 动态前缀
|
||||
* @return 不可变 Tool 白名单
|
||||
*/
|
||||
private List<McpSchema.Tool> freeze(List<McpSchema.Tool> actualTools,
|
||||
List<McpToolManifestEntry> manifest,
|
||||
Map<String, String> aliases,
|
||||
String prefix) {
|
||||
Map<String, McpSchema.Tool> actualByName = new LinkedHashMap<>();
|
||||
if (actualTools != null) {
|
||||
actualTools.stream().filter(tool -> tool != null && tool.name() != null)
|
||||
.forEach(tool -> actualByName.put(tool.name(), tool));
|
||||
}
|
||||
Map<String, String> usedRuntimeNames = new LinkedHashMap<>();
|
||||
List<McpSchema.Tool> result = new ArrayList<>();
|
||||
for (McpToolManifestEntry entry : manifest) {
|
||||
McpSchema.Tool actual = actualByName.get(entry.getName());
|
||||
if (actual == null) {
|
||||
throw new IllegalStateException("Frozen MCP tool is missing after validation: " + entry.getName());
|
||||
}
|
||||
String runtimeName = uniqueRuntimeName(
|
||||
runtimeName(actual.name(), aliases, prefix), actual.name(), usedRuntimeNames);
|
||||
runtimeToRaw.put(runtimeName, actual.name());
|
||||
Map<String, Object> meta = new LinkedHashMap<>();
|
||||
if (actual.meta() != null) {
|
||||
meta.putAll(actual.meta());
|
||||
}
|
||||
if (!runtimeName.equals(actual.name())) {
|
||||
meta.put(AliasedMcpClientWrapper.RAW_TOOL_NAME_META_KEY, actual.name());
|
||||
}
|
||||
// 模型可见描述也必须来自发布时冻结清单,避免远端描述在运行中漂移。
|
||||
result.add(new McpSchema.Tool(runtimeName, actual.title(), entry.getDescription(),
|
||||
actual.inputSchema(), actual.outputSchema(), actual.annotations(), meta));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算单个 Tool 的运行名。
|
||||
*
|
||||
* @param rawName 原始名称
|
||||
* @param aliases 显式别名
|
||||
* @param prefix 动态前缀
|
||||
* @return 运行名
|
||||
*/
|
||||
private String runtimeName(String rawName, Map<String, String> aliases, String prefix) {
|
||||
String alias = aliases == null ? null : aliases.get(rawName);
|
||||
if (alias != null && !alias.isBlank()) {
|
||||
return alias;
|
||||
}
|
||||
if (prefix == null || prefix.isBlank()) {
|
||||
return rawName;
|
||||
}
|
||||
String segment = String.valueOf(rawName == null ? "" : rawName).trim()
|
||||
.replaceAll("[^A-Za-z0-9_-]", "_")
|
||||
.replaceAll("_+", "_");
|
||||
return prefix.trim() + (segment.isBlank() ? "tool" : segment);
|
||||
}
|
||||
|
||||
/**
|
||||
* 避免别名碰撞。
|
||||
*
|
||||
* @param candidate 候选运行名
|
||||
* @param rawName 原始名称
|
||||
* @param used 已使用运行名
|
||||
* @return 唯一运行名
|
||||
*/
|
||||
private String uniqueRuntimeName(String candidate,
|
||||
String rawName,
|
||||
Map<String, String> used) {
|
||||
String existing = used.get(candidate);
|
||||
if (existing == null || existing.equals(rawName)) {
|
||||
used.put(candidate, rawName);
|
||||
return candidate;
|
||||
}
|
||||
int suffix = 2;
|
||||
String value = candidate + "_" + suffix;
|
||||
while (used.containsKey(value)) {
|
||||
suffix++;
|
||||
value = candidate + "_" + suffix;
|
||||
}
|
||||
used.put(value, rawName);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.easyagents.agent.runtime.mcp;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import io.agentscope.core.tool.mcp.McpClientBuilder;
|
||||
import io.agentscope.core.tool.mcp.McpClientWrapper;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 创建 AgentScope MCP client。
|
||||
*/
|
||||
public class McpClientFactory {
|
||||
|
||||
/**
|
||||
* 根据 MCP 运行时声明创建 AgentScope MCP client。
|
||||
*
|
||||
* @param spec MCP 运行时声明
|
||||
* @return AgentScope MCP client
|
||||
*/
|
||||
public McpClientWrapper create(McpSpec spec) {
|
||||
McpSpecValidator.validateConnection(spec);
|
||||
McpClientBuilder builder = McpClientBuilder.create(spec.getName())
|
||||
.timeout(timeout(spec.getTimeout(), Duration.ofSeconds(120)))
|
||||
.initializationTimeout(timeout(spec.getInitializationTimeout(), Duration.ofSeconds(30)));
|
||||
switch (spec.getTransportType()) {
|
||||
case STDIO -> builder.stdioTransport(spec.getCommand(), spec.getArgs(), spec.getEnv());
|
||||
case SSE -> builder.sseTransport(spec.getUrl())
|
||||
.headers(spec.getHeaders())
|
||||
.queryParams(spec.getQueryParams());
|
||||
case HTTP -> builder.streamableHttpTransport(spec.getUrl())
|
||||
.headers(spec.getHeaders())
|
||||
.queryParams(spec.getQueryParams());
|
||||
default -> throw new AgentRuntimeException("Unsupported MCP transport type: " + spec.getTransportType());
|
||||
}
|
||||
return builder.buildAsync().block();
|
||||
}
|
||||
|
||||
private Duration timeout(Duration value, Duration defaultValue) {
|
||||
return value == null || value.isZero() || value.isNegative() ? defaultValue : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.easyagents.agent.runtime.mcp;
|
||||
|
||||
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||
import io.agentscope.core.tool.mcp.McpClientWrapper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MCP 注册结果。
|
||||
*/
|
||||
public class McpRegistration {
|
||||
|
||||
private final List<McpClientWrapper> clients;
|
||||
private final List<AgentToolSpec> toolSpecs;
|
||||
private final List<McpSkillRegistration> skillRegistrations;
|
||||
|
||||
/**
|
||||
* 创建 MCP 注册结果。
|
||||
*
|
||||
* @param clients 已创建 MCP client
|
||||
* @param toolSpecs 已注册工具声明
|
||||
*/
|
||||
public McpRegistration(List<McpClientWrapper> clients, List<AgentToolSpec> toolSpecs) {
|
||||
this(clients, toolSpecs, List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 MCP 注册结果。
|
||||
*
|
||||
* @param clients 已创建 MCP client
|
||||
* @param toolSpecs 已发现工具声明
|
||||
* @param skillRegistrations 等待注册到 Skill 的 MCP client
|
||||
*/
|
||||
public McpRegistration(List<McpClientWrapper> clients,
|
||||
List<AgentToolSpec> toolSpecs,
|
||||
List<McpSkillRegistration> skillRegistrations) {
|
||||
this.clients = clients == null ? List.of() : new ArrayList<>(clients);
|
||||
this.toolSpecs = toolSpecs == null ? List.of() : new ArrayList<>(toolSpecs);
|
||||
this.skillRegistrations = skillRegistrations == null
|
||||
? List.of()
|
||||
: new ArrayList<>(skillRegistrations);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建空注册结果。
|
||||
*
|
||||
* @return 空注册结果
|
||||
*/
|
||||
public static McpRegistration empty() {
|
||||
return new McpRegistration(List.of(), List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已创建 MCP client。
|
||||
*
|
||||
* @return 已创建 MCP client
|
||||
*/
|
||||
public List<McpClientWrapper> getClients() {
|
||||
return clients;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已注册工具声明。
|
||||
*
|
||||
* @return 已注册工具声明
|
||||
*/
|
||||
public List<AgentToolSpec> getToolSpecs() {
|
||||
return toolSpecs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取等待注册到 Skill 的 MCP client。
|
||||
*
|
||||
* @return Skill MCP 注册声明
|
||||
*/
|
||||
public List<McpSkillRegistration> getSkillRegistrations() {
|
||||
return skillRegistrations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.easyagents.agent.runtime.mcp;
|
||||
|
||||
import io.agentscope.core.tool.mcp.McpClientWrapper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 等待注册到指定 Skill 的 MCP client。
|
||||
*/
|
||||
public class McpSkillRegistration {
|
||||
|
||||
private final String skillId;
|
||||
private final McpClientWrapper client;
|
||||
private final List<String> enableTools;
|
||||
private final List<String> disableTools;
|
||||
private final Map<String, Map<String, Object>> presetParameters;
|
||||
|
||||
/**
|
||||
* 创建 Skill MCP 注册声明。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param client MCP client
|
||||
* @param enableTools 运行时工具白名单
|
||||
* @param disableTools 运行时工具黑名单
|
||||
* @param presetParameters 预设参数
|
||||
*/
|
||||
public McpSkillRegistration(String skillId,
|
||||
McpClientWrapper client,
|
||||
List<String> enableTools,
|
||||
List<String> disableTools,
|
||||
Map<String, Map<String, Object>> presetParameters) {
|
||||
this.skillId = skillId;
|
||||
this.client = client;
|
||||
this.enableTools = enableTools == null ? List.of() : new ArrayList<>(enableTools);
|
||||
this.disableTools = disableTools == null ? List.of() : new ArrayList<>(disableTools);
|
||||
this.presetParameters = presetParameters == null
|
||||
? Map.of()
|
||||
: new LinkedHashMap<>(presetParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Skill ID。
|
||||
*
|
||||
* @return Skill ID
|
||||
*/
|
||||
public String getSkillId() {
|
||||
return skillId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MCP client。
|
||||
*
|
||||
* @return MCP client
|
||||
*/
|
||||
public McpClientWrapper getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行时工具白名单。
|
||||
*
|
||||
* @return 工具白名单
|
||||
*/
|
||||
public List<String> getEnableTools() {
|
||||
return enableTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行时工具黑名单。
|
||||
*
|
||||
* @return 工具黑名单
|
||||
*/
|
||||
public List<String> getDisableTools() {
|
||||
return disableTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预设参数。
|
||||
*
|
||||
* @return 预设参数
|
||||
*/
|
||||
public Map<String, Map<String, Object>> getPresetParameters() {
|
||||
return presetParameters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
package com.easyagents.agent.runtime.mcp;
|
||||
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCP 运行时声明。
|
||||
*/
|
||||
public class McpSpec {
|
||||
|
||||
private String name;
|
||||
private String description;
|
||||
private McpTransportType transportType = McpTransportType.STDIO;
|
||||
private String command;
|
||||
private List<String> args = new ArrayList<>();
|
||||
private Map<String, String> env = new LinkedHashMap<>();
|
||||
private String url;
|
||||
private Map<String, String> headers = new LinkedHashMap<>();
|
||||
private Map<String, String> queryParams = new LinkedHashMap<>();
|
||||
private Duration timeout = Duration.ofSeconds(120);
|
||||
private Duration initializationTimeout = Duration.ofSeconds(30);
|
||||
private List<String> enableTools = new ArrayList<>();
|
||||
private List<String> disableTools = new ArrayList<>();
|
||||
private String groupName;
|
||||
private Map<String, Map<String, Object>> presetParameters = new LinkedHashMap<>();
|
||||
private Map<String, String> toolAliases = new LinkedHashMap<>();
|
||||
private String toolNamePrefix;
|
||||
private boolean approvalRequired;
|
||||
private AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
|
||||
private Map<String, AgentToolApprovalRequest> toolApprovalRequests = new LinkedHashMap<>();
|
||||
private String skillId;
|
||||
private List<McpToolManifestEntry> frozenToolManifest = new ArrayList<>();
|
||||
private String frozenToolManifestHash;
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 获取 MCP client 名称。
|
||||
*
|
||||
* @return MCP client 名称
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 MCP client 名称。
|
||||
*
|
||||
* @param name MCP client 名称
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MCP 描述。
|
||||
*
|
||||
* @return MCP 描述
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 MCP 描述。
|
||||
*
|
||||
* @param description MCP 描述
|
||||
*/
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接方式。
|
||||
*
|
||||
* @return 连接方式
|
||||
*/
|
||||
public McpTransportType getTransportType() {
|
||||
return transportType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接方式。
|
||||
*
|
||||
* @param transportType 连接方式
|
||||
*/
|
||||
public void setTransportType(McpTransportType transportType) {
|
||||
this.transportType = transportType == null ? McpTransportType.STDIO : transportType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过字符串设置连接方式。
|
||||
*
|
||||
* @param transportType 连接方式文本
|
||||
*/
|
||||
public void setTransportType(String transportType) {
|
||||
this.transportType = McpTransportType.from(transportType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 stdio 命令。
|
||||
*
|
||||
* @return stdio 命令
|
||||
*/
|
||||
public String getCommand() {
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 stdio 命令。
|
||||
*
|
||||
* @param command stdio 命令
|
||||
*/
|
||||
public void setCommand(String command) {
|
||||
this.command = command;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 stdio 参数。
|
||||
*
|
||||
* @return stdio 参数
|
||||
*/
|
||||
public List<String> getArgs() {
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 stdio 参数。
|
||||
*
|
||||
* @param args stdio 参数
|
||||
*/
|
||||
public void setArgs(List<String> args) {
|
||||
this.args = args == null ? new ArrayList<>() : new ArrayList<>(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 stdio 环境变量。
|
||||
*
|
||||
* @return stdio 环境变量
|
||||
*/
|
||||
public Map<String, String> getEnv() {
|
||||
return env;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 stdio 环境变量。
|
||||
*
|
||||
* @param env stdio 环境变量
|
||||
*/
|
||||
public void setEnv(Map<String, String> env) {
|
||||
this.env = env == null ? new LinkedHashMap<>() : new LinkedHashMap<>(env);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HTTP 地址。
|
||||
*
|
||||
* @return HTTP 地址
|
||||
*/
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 HTTP 地址。
|
||||
*
|
||||
* @param url HTTP 地址
|
||||
*/
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HTTP 请求头。
|
||||
*
|
||||
* @return HTTP 请求头
|
||||
*/
|
||||
public Map<String, String> getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 HTTP 请求头。
|
||||
*
|
||||
* @param headers HTTP 请求头
|
||||
*/
|
||||
public void setHeaders(Map<String, String> headers) {
|
||||
this.headers = headers == null ? new LinkedHashMap<>() : new LinkedHashMap<>(headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HTTP 查询参数。
|
||||
*
|
||||
* @return HTTP 查询参数
|
||||
*/
|
||||
public Map<String, String> getQueryParams() {
|
||||
return queryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 HTTP 查询参数。
|
||||
*
|
||||
* @param queryParams HTTP 查询参数
|
||||
*/
|
||||
public void setQueryParams(Map<String, String> queryParams) {
|
||||
this.queryParams = queryParams == null ? new LinkedHashMap<>() : new LinkedHashMap<>(queryParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求超时时间。
|
||||
*
|
||||
* @return 请求超时时间
|
||||
*/
|
||||
public Duration getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求超时时间。
|
||||
*
|
||||
* @param timeout 请求超时时间
|
||||
*/
|
||||
public void setTimeout(Duration timeout) {
|
||||
this.timeout = timeout == null ? Duration.ofSeconds(120) : timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取初始化超时时间。
|
||||
*
|
||||
* @return 初始化超时时间
|
||||
*/
|
||||
public Duration getInitializationTimeout() {
|
||||
return initializationTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置初始化超时时间。
|
||||
*
|
||||
* @param initializationTimeout 初始化超时时间
|
||||
*/
|
||||
public void setInitializationTimeout(Duration initializationTimeout) {
|
||||
this.initializationTimeout = initializationTimeout == null ? Duration.ofSeconds(30) : initializationTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用工具白名单。
|
||||
*
|
||||
* @return 启用工具白名单
|
||||
*/
|
||||
public List<String> getEnableTools() {
|
||||
return enableTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置启用工具白名单。
|
||||
*
|
||||
* @param enableTools 启用工具白名单
|
||||
*/
|
||||
public void setEnableTools(List<String> enableTools) {
|
||||
this.enableTools = enableTools == null ? new ArrayList<>() : new ArrayList<>(enableTools);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取禁用工具黑名单。
|
||||
*
|
||||
* @return 禁用工具黑名单
|
||||
*/
|
||||
public List<String> getDisableTools() {
|
||||
return disableTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置禁用工具黑名单。
|
||||
*
|
||||
* @param disableTools 禁用工具黑名单
|
||||
*/
|
||||
public void setDisableTools(List<String> disableTools) {
|
||||
this.disableTools = disableTools == null ? new ArrayList<>() : new ArrayList<>(disableTools);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具分组名。
|
||||
*
|
||||
* @return 工具分组名
|
||||
*/
|
||||
public String getGroupName() {
|
||||
return groupName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工具分组名。
|
||||
*
|
||||
* @param groupName 工具分组名
|
||||
*/
|
||||
public void setGroupName(String groupName) {
|
||||
this.groupName = groupName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预置参数。
|
||||
*
|
||||
* @return 预置参数
|
||||
*/
|
||||
public Map<String, Map<String, Object>> getPresetParameters() {
|
||||
return presetParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置预置参数。
|
||||
*
|
||||
* @param presetParameters 预置参数
|
||||
*/
|
||||
public void setPresetParameters(Map<String, Map<String, Object>> presetParameters) {
|
||||
this.presetParameters = presetParameters == null ? new LinkedHashMap<>() : new LinkedHashMap<>(presetParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MCP 原始工具名到运行时工具名的别名映射。
|
||||
*
|
||||
* @return 工具别名映射
|
||||
*/
|
||||
public Map<String, String> getToolAliases() {
|
||||
return toolAliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 MCP 原始工具名到运行时工具名的别名映射。
|
||||
*
|
||||
* @param toolAliases 工具别名映射
|
||||
*/
|
||||
public void setToolAliases(Map<String, String> toolAliases) {
|
||||
this.toolAliases = toolAliases == null ? new LinkedHashMap<>() : new LinkedHashMap<>(toolAliases);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动态工具名前缀。
|
||||
*
|
||||
* @return 动态工具名前缀
|
||||
*/
|
||||
public String getToolNamePrefix() {
|
||||
return toolNamePrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置动态工具名前缀。
|
||||
*
|
||||
* @param toolNamePrefix 动态工具名前缀
|
||||
*/
|
||||
public void setToolNamePrefix(String toolNamePrefix) {
|
||||
this.toolNamePrefix = toolNamePrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 MCP 工具是否默认需要人工审批。
|
||||
*
|
||||
* @return 需要审批时为 true
|
||||
*/
|
||||
public boolean isApprovalRequired() {
|
||||
return approvalRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 MCP 工具是否默认需要人工审批。
|
||||
*
|
||||
* @param approvalRequired 审批标记
|
||||
*/
|
||||
public void setApprovalRequired(boolean approvalRequired) {
|
||||
this.approvalRequired = approvalRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审批请求。
|
||||
*
|
||||
* @return 审批请求
|
||||
*/
|
||||
public AgentToolApprovalRequest getApprovalRequest() {
|
||||
return approvalRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置审批请求。
|
||||
*
|
||||
* @param approvalRequest 审批请求
|
||||
*/
|
||||
public void setApprovalRequest(AgentToolApprovalRequest approvalRequest) {
|
||||
this.approvalRequest = approvalRequest == null ? new AgentToolApprovalRequest() : approvalRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行时工具名到审批请求的映射。
|
||||
*
|
||||
* @return 工具审批请求映射
|
||||
*/
|
||||
public Map<String, AgentToolApprovalRequest> getToolApprovalRequests() {
|
||||
return toolApprovalRequests;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置运行时工具名到审批请求的映射。
|
||||
*
|
||||
* @param toolApprovalRequests 工具审批请求映射
|
||||
*/
|
||||
public void setToolApprovalRequests(Map<String, AgentToolApprovalRequest> toolApprovalRequests) {
|
||||
this.toolApprovalRequests = toolApprovalRequests == null
|
||||
? new LinkedHashMap<>()
|
||||
: new LinkedHashMap<>(toolApprovalRequests);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所属 Skill ID。
|
||||
*
|
||||
* @return Skill ID;未绑定 Skill 时为空
|
||||
*/
|
||||
public String getSkillId() {
|
||||
return skillId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置所属 Skill ID。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
*/
|
||||
public void setSkillId(String skillId) {
|
||||
this.skillId = skillId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取冻结 Tool 清单。
|
||||
*
|
||||
* @return 冻结 Tool 清单
|
||||
*/
|
||||
public List<McpToolManifestEntry> getFrozenToolManifest() {
|
||||
return frozenToolManifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置冻结 Tool 清单。
|
||||
*
|
||||
* @param frozenToolManifest 冻结 Tool 清单
|
||||
*/
|
||||
public void setFrozenToolManifest(List<McpToolManifestEntry> frozenToolManifest) {
|
||||
this.frozenToolManifest = frozenToolManifest == null
|
||||
? new ArrayList<>()
|
||||
: new ArrayList<>(frozenToolManifest);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取冻结 Tool 清单 hash。
|
||||
*
|
||||
* @return 清单 hash
|
||||
*/
|
||||
public String getFrozenToolManifestHash() {
|
||||
return frozenToolManifestHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置冻结 Tool 清单 hash。
|
||||
*
|
||||
* @param frozenToolManifestHash 清单 hash
|
||||
*/
|
||||
public void setFrozenToolManifestHash(String frozenToolManifestHash) {
|
||||
this.frozenToolManifestHash = frozenToolManifestHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
*
|
||||
* @return 元数据
|
||||
*/
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置元数据。
|
||||
*
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : new LinkedHashMap<>(metadata);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.easyagents.agent.runtime.mcp;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||
import com.easyagents.agent.runtime.tool.operate.AgentOperateToolAdapter;
|
||||
import com.easyagents.agent.runtime.tool.operate.AgentOperateToolSpec;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* MCP 声明校验器。
|
||||
*/
|
||||
public final class McpSpecValidator {
|
||||
|
||||
private McpSpecValidator() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 MCP 连接配置。
|
||||
*
|
||||
* @param spec MCP 运行时声明
|
||||
*/
|
||||
public static void validateConnection(McpSpec spec) {
|
||||
if (spec == null) {
|
||||
throw new AgentRuntimeException("MCP spec is required.");
|
||||
}
|
||||
if (spec.getName() == null || spec.getName().isBlank()) {
|
||||
throw new AgentRuntimeException("MCP name is required.");
|
||||
}
|
||||
if (spec.getTransportType() == null) {
|
||||
throw new AgentRuntimeException("MCP transport type is required: " + spec.getName());
|
||||
}
|
||||
validateToolAliases(spec);
|
||||
switch (spec.getTransportType()) {
|
||||
case STDIO -> {
|
||||
if (spec.getCommand() == null || spec.getCommand().isBlank()) {
|
||||
throw new AgentRuntimeException("MCP stdio command is required: " + spec.getName());
|
||||
}
|
||||
}
|
||||
case SSE, HTTP -> {
|
||||
if (spec.getUrl() == null || spec.getUrl().isBlank()) {
|
||||
throw new AgentRuntimeException("MCP url is required: " + spec.getName());
|
||||
}
|
||||
}
|
||||
default -> throw new AgentRuntimeException("Unsupported MCP transport type: " + spec.getTransportType());
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateToolAliases(McpSpec spec) {
|
||||
Map<String, String> aliases = spec.getToolAliases();
|
||||
if (aliases == null || aliases.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<String> runtimeNames = new HashSet<>();
|
||||
for (Map.Entry<String, String> entry : aliases.entrySet()) {
|
||||
String rawName = entry.getKey();
|
||||
String runtimeName = entry.getValue();
|
||||
if (rawName == null || rawName.isBlank()) {
|
||||
throw new AgentRuntimeException("MCP raw tool name is required: " + spec.getName());
|
||||
}
|
||||
if (runtimeName == null || runtimeName.isBlank()) {
|
||||
throw new AgentRuntimeException("MCP runtime tool name is required: " + spec.getName());
|
||||
}
|
||||
if (!runtimeNames.add(runtimeName)) {
|
||||
throw new AgentRuntimeException("MCP runtime tool alias conflicts: " + runtimeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 MCP 工具与既有工具名冲突。
|
||||
*
|
||||
* @param businessToolSpecs 普通工具声明
|
||||
* @param mcpToolSpecs MCP 工具声明
|
||||
* @param operateToolSpecs 操作工具声明
|
||||
*/
|
||||
public static void validateToolConflicts(List<AgentToolSpec> businessToolSpecs,
|
||||
List<AgentToolSpec> mcpToolSpecs,
|
||||
List<AgentOperateToolSpec> operateToolSpecs) {
|
||||
Set<String> names = new HashSet<>();
|
||||
addToolNames(names, businessToolSpecs, "Agent tool conflicts with existing tool: ");
|
||||
addToolNames(names, mcpToolSpecs, "MCP tool conflicts with existing tool: ");
|
||||
Set<String> operateToolNames = new AgentOperateToolAdapter().enabledToolNames(operateToolSpecs);
|
||||
for (String operateToolName : operateToolNames) {
|
||||
if (!names.add(operateToolName)) {
|
||||
throw new AgentRuntimeException("Agent operate tool conflicts with existing tool: " + operateToolName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addToolNames(Set<String> names, List<AgentToolSpec> toolSpecs, String messagePrefix) {
|
||||
if (toolSpecs == null || toolSpecs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (AgentToolSpec toolSpec : toolSpecs) {
|
||||
if (toolSpec == null || toolSpec.getName() == null || toolSpec.getName().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (!names.add(toolSpec.getName())) {
|
||||
throw new AgentRuntimeException(messagePrefix + toolSpec.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
package com.easyagents.agent.runtime.mcp;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* MCP Tool 冻结清单规范化与完整性校验器。
|
||||
*/
|
||||
public final class McpToolManifest {
|
||||
|
||||
/** MCP Tool 原始名称允许的最大 Unicode 字符数。 */
|
||||
public static final int MAX_TOOL_NAME_LENGTH = 128;
|
||||
|
||||
/** MCP Tool 描述允许的最大 Unicode 字符数。 */
|
||||
public static final int MAX_TOOL_DESCRIPTION_LENGTH = 4_096;
|
||||
|
||||
/** 单个输入或输出 Schema 允许的最大 UTF-8 字节数。 */
|
||||
public static final int MAX_SCHEMA_UTF8_BYTES = 256 * 1_024;
|
||||
|
||||
/** 完整规范化 Manifest 允许的最大 UTF-8 字节数。 */
|
||||
public static final int MAX_MANIFEST_UTF8_BYTES = 2 * 1_024 * 1_024;
|
||||
|
||||
private McpToolManifest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MCP Tool 转换为稳定清单项。
|
||||
*
|
||||
* @param tools MCP Tool 列表
|
||||
* @return 按名称稳定排序的清单
|
||||
*/
|
||||
public static List<McpToolManifestEntry> fromTools(List<McpSchema.Tool> tools) {
|
||||
if (tools == null || tools.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<McpToolManifestEntry> entries = new ArrayList<>();
|
||||
Set<String> names = new HashSet<>();
|
||||
int manifestBytes = 2;
|
||||
for (McpSchema.Tool tool : tools) {
|
||||
if (tool == null || tool.name() == null || tool.name().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (!names.add(tool.name())) {
|
||||
throw new AgentRuntimeException("Duplicate MCP tool name: " + tool.name());
|
||||
}
|
||||
McpToolManifestEntry entry = new McpToolManifestEntry();
|
||||
entry.setName(tool.name());
|
||||
entry.setDescription(normalizeText(tool.description()));
|
||||
entry.setInputSchema(normalizeSchema("MCP tool input schema", tool.inputSchema()));
|
||||
entry.setOutputSchema(normalizeSchema("MCP tool output schema", tool.outputSchema()));
|
||||
assertEntryBounds(entry);
|
||||
manifestBytes += JSON.toJSONString(toCanonicalValue(entry))
|
||||
.getBytes(StandardCharsets.UTF_8).length;
|
||||
if (!entries.isEmpty()) {
|
||||
manifestBytes++;
|
||||
}
|
||||
if (manifestBytes > MAX_MANIFEST_UTF8_BYTES) {
|
||||
throw new AgentRuntimeException("MCP tool manifest exceeds "
|
||||
+ MAX_MANIFEST_UTF8_BYTES + " UTF-8 bytes.");
|
||||
}
|
||||
entries.add(entry);
|
||||
}
|
||||
entries.sort(Comparator.comparing(McpToolManifestEntry::getName));
|
||||
assertManifestSize(entries);
|
||||
return List.copyOf(entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算冻结清单的 SHA-256。
|
||||
*
|
||||
* @param entries 冻结清单
|
||||
* @return 十六进制 SHA-256
|
||||
*/
|
||||
public static String hash(List<McpToolManifestEntry> entries) {
|
||||
String json = canonicalJson(entries);
|
||||
try {
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256")
|
||||
.digest(json.getBytes(StandardCharsets.UTF_8));
|
||||
return java.util.HexFormat.of().formatHex(digest);
|
||||
} catch (NoSuchAlgorithmException error) {
|
||||
throw new AgentRuntimeException("SHA-256 is unavailable for MCP manifest validation.", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验远端 Tool 与冻结白名单一致,同时忽略远端新增 Tool。
|
||||
*
|
||||
* @param spec MCP 运行时声明
|
||||
* @param actualTools 远端当前 Tool
|
||||
* @throws AgentRuntimeException 冻结清单缺失、被篡改、Tool 缺失或 Schema 漂移时抛出
|
||||
*/
|
||||
public static void assertFrozenManifest(McpSpec spec, List<McpSchema.Tool> actualTools) {
|
||||
if (spec == null || spec.getSkillId() == null || spec.getSkillId().isBlank()) {
|
||||
return;
|
||||
}
|
||||
List<McpToolManifestEntry> expected = spec.getFrozenToolManifest();
|
||||
String expectedHash = spec.getFrozenToolManifestHash();
|
||||
if (expected == null || expected.isEmpty() || expectedHash == null || expectedHash.isBlank()) {
|
||||
throw new AgentRuntimeException("Skill-bound MCP requires a frozen tool manifest: " + spec.getName());
|
||||
}
|
||||
if (!expectedHash.equals(hash(expected))) {
|
||||
throw new AgentRuntimeException("Skill-bound MCP frozen tool manifest is invalid: " + spec.getName());
|
||||
}
|
||||
Set<String> frozenNames = new HashSet<>();
|
||||
for (McpToolManifestEntry entry : expected) {
|
||||
if (entry != null && entry.getName() != null && !entry.getName().isBlank()) {
|
||||
frozenNames.add(entry.getName());
|
||||
}
|
||||
}
|
||||
Map<String, McpToolManifestEntry> actualByName = new LinkedHashMap<>();
|
||||
if (actualTools != null) {
|
||||
for (McpSchema.Tool tool : actualTools) {
|
||||
if (tool == null || tool.name() == null || !frozenNames.contains(tool.name())) {
|
||||
continue;
|
||||
}
|
||||
if (actualByName.containsKey(tool.name())) {
|
||||
throw new AgentRuntimeException("Duplicate MCP tool name: " + tool.name());
|
||||
}
|
||||
List<McpToolManifestEntry> normalized = fromTools(List.of(tool));
|
||||
if (!normalized.isEmpty()) {
|
||||
actualByName.put(tool.name(), normalized.get(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (McpToolManifestEntry expectedEntry : expected) {
|
||||
if (expectedEntry == null || expectedEntry.getName() == null || expectedEntry.getName().isBlank()) {
|
||||
throw new AgentRuntimeException("Skill-bound MCP frozen tool name is required: " + spec.getName());
|
||||
}
|
||||
McpToolManifestEntry actualEntry = actualByName.get(expectedEntry.getName());
|
||||
if (actualEntry == null) {
|
||||
throw new AgentRuntimeException("Skill-bound MCP tool is missing: " + expectedEntry.getName());
|
||||
}
|
||||
if (!sameRuntimeSchema(expectedEntry, actualEntry)) {
|
||||
throw new AgentRuntimeException("Skill-bound MCP tool schema has changed: "
|
||||
+ expectedEntry.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较 Runtime 必须锁定的 Tool 名称及输入、输出 Schema。
|
||||
*
|
||||
* <p>描述用于保存与发布阶段的完整 manifest 变更识别,但远端仅调整描述时不会改变
|
||||
* 已发布 Tool 的可调用边界,因此运行时不应中断既有 Agent。</p>
|
||||
*
|
||||
* @param expected 冻结清单项
|
||||
* @param actual 远端当前清单项
|
||||
* @return 名称及 Schema 相同时返回 {@code true}
|
||||
*/
|
||||
private static boolean sameRuntimeSchema(McpToolManifestEntry expected,
|
||||
McpToolManifestEntry actual) {
|
||||
return java.util.Objects.equals(expected.getName(), actual.getName())
|
||||
&& java.util.Objects.equals(normalizeJson(expected.getInputSchema()),
|
||||
normalizeJson(actual.getInputSchema()))
|
||||
&& java.util.Objects.equals(normalizeJson(expected.getOutputSchema()),
|
||||
normalizeJson(actual.getOutputSchema()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将冻结清单转换为稳定 JSON,并对反序列化后的清单执行同等边界校验。
|
||||
*
|
||||
* @param entries 冻结清单
|
||||
* @return 稳定 JSON
|
||||
* @throws AgentRuntimeException 清单包含重复名称或超出预算时抛出
|
||||
*/
|
||||
private static String canonicalJson(List<McpToolManifestEntry> entries) {
|
||||
List<Map<String, Object>> canonical = new ArrayList<>();
|
||||
Set<String> names = new HashSet<>();
|
||||
if (entries != null) {
|
||||
entries.stream()
|
||||
.filter(entry -> entry != null && entry.getName() != null && !entry.getName().isBlank())
|
||||
.sorted(Comparator.comparing(McpToolManifestEntry::getName))
|
||||
.forEach(entry -> {
|
||||
if (!names.add(entry.getName())) {
|
||||
throw new AgentRuntimeException("Duplicate MCP tool name: " + entry.getName());
|
||||
}
|
||||
McpToolManifestEntry normalized = new McpToolManifestEntry();
|
||||
normalized.setName(entry.getName());
|
||||
normalized.setDescription(normalizeText(entry.getDescription()));
|
||||
normalized.setInputSchema(normalizeSchema(
|
||||
"MCP tool input schema", entry.getInputSchema()));
|
||||
normalized.setOutputSchema(normalizeSchema(
|
||||
"MCP tool output schema", entry.getOutputSchema()));
|
||||
assertEntryBounds(normalized);
|
||||
canonical.add(toCanonicalValue(normalized));
|
||||
});
|
||||
}
|
||||
String json = JSON.toJSONString(canonical);
|
||||
assertUtf8Size("MCP tool manifest", json, MAX_MANIFEST_UTF8_BYTES);
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验单个清单项的名称、描述及 Schema 预算。
|
||||
*
|
||||
* @param entry 已规范化的清单项
|
||||
* @throws AgentRuntimeException 任一字段超出预算时抛出
|
||||
*/
|
||||
private static void assertEntryBounds(McpToolManifestEntry entry) {
|
||||
assertTextLength("MCP tool name", entry.getName(), MAX_TOOL_NAME_LENGTH);
|
||||
assertTextLength("MCP tool description", entry.getDescription(), MAX_TOOL_DESCRIPTION_LENGTH);
|
||||
assertSchemaSize("MCP tool input schema", entry.getInputSchema());
|
||||
assertSchemaSize("MCP tool output schema", entry.getOutputSchema());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验规范化清单的聚合字节预算。
|
||||
*
|
||||
* @param entries 已规范化且排序的清单
|
||||
* @throws AgentRuntimeException 清单超出聚合预算时抛出
|
||||
*/
|
||||
private static void assertManifestSize(List<McpToolManifestEntry> entries) {
|
||||
List<Map<String, Object>> canonical = entries.stream()
|
||||
.map(McpToolManifest::toCanonicalValue)
|
||||
.toList();
|
||||
assertUtf8Size("MCP tool manifest", JSON.toJSONString(canonical), MAX_MANIFEST_UTF8_BYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造用于哈希和预算计算的稳定清单值。
|
||||
*
|
||||
* @param entry 已规范化的清单项
|
||||
* @return 保持字段顺序的清单值
|
||||
*/
|
||||
private static Map<String, Object> toCanonicalValue(McpToolManifestEntry entry) {
|
||||
Map<String, Object> value = new LinkedHashMap<>();
|
||||
value.put("name", entry.getName());
|
||||
value.put("description", normalizeText(entry.getDescription()));
|
||||
value.put("inputSchema", entry.getInputSchema());
|
||||
value.put("outputSchema", entry.getOutputSchema());
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Unicode 字符长度,避免 UTF-16 代理对被重复计数。
|
||||
*
|
||||
* @param field 字段名称
|
||||
* @param value 字段值
|
||||
* @param maxLength 最大 Unicode 字符数
|
||||
* @throws AgentRuntimeException 字段超长时抛出
|
||||
*/
|
||||
private static void assertTextLength(String field, String value, int maxLength) {
|
||||
if (value != null && value.codePointCount(0, value.length()) > maxLength) {
|
||||
throw new AgentRuntimeException(field + " exceeds " + maxLength + " characters.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验单个 Schema 的 UTF-8 字节预算。
|
||||
*
|
||||
* @param field Schema 字段名称
|
||||
* @param schema 已规范化 Schema
|
||||
* @throws AgentRuntimeException Schema 超出预算时抛出
|
||||
*/
|
||||
private static void assertSchemaSize(String field, Object schema) {
|
||||
if (schema != null) {
|
||||
assertUtf8Size(field, JSON.toJSONString(schema), MAX_SCHEMA_UTF8_BYTES);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 JSON 或文本的 UTF-8 字节长度。
|
||||
*
|
||||
* @param field 字段名称
|
||||
* @param value 待校验文本
|
||||
* @param maxBytes 最大 UTF-8 字节数
|
||||
* @throws AgentRuntimeException 文本超出预算时抛出
|
||||
*/
|
||||
private static void assertUtf8Size(String field, String value, int maxBytes) {
|
||||
int bytes = value.getBytes(StandardCharsets.UTF_8).length;
|
||||
if (bytes > maxBytes) {
|
||||
throw new AgentRuntimeException(field + " exceeds " + maxBytes + " UTF-8 bytes.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在解析和排序前限制原始 Schema,避免超大输入进入规范化流程。
|
||||
*
|
||||
* @param field Schema 字段名称
|
||||
* @param value 原始 Schema
|
||||
* @return 规范化 Schema
|
||||
* @throws AgentRuntimeException 原始 Schema 超出预算时抛出
|
||||
*/
|
||||
private static Object normalizeSchema(String field, Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String json = JSON.toJSONString(value);
|
||||
assertUtf8Size(field, json, MAX_SCHEMA_UTF8_BYTES);
|
||||
return sortJson(JSON.parse(json));
|
||||
}
|
||||
|
||||
private static Object normalizeJson(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return sortJson(JSON.parse(JSON.toJSONString(value)));
|
||||
}
|
||||
|
||||
private static Object sortJson(Object value) {
|
||||
if (value instanceof Map<?, ?> source) {
|
||||
Map<String, Object> sorted = new TreeMap<>();
|
||||
source.forEach((key, child) -> sorted.put(String.valueOf(key), sortJson(child)));
|
||||
return sorted;
|
||||
}
|
||||
if (value instanceof List<?> source) {
|
||||
List<Object> sorted = new ArrayList<>(source.size());
|
||||
for (Object child : source) {
|
||||
sorted.add(sortJson(child));
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String normalizeText(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.easyagents.agent.runtime.mcp;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* MCP Tool 冻结清单项。
|
||||
*/
|
||||
public class McpToolManifestEntry {
|
||||
|
||||
private String name;
|
||||
private String description;
|
||||
private Object inputSchema;
|
||||
private Object outputSchema;
|
||||
|
||||
/**
|
||||
* 获取 Tool 名称。
|
||||
*
|
||||
* @return Tool 名称
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Tool 名称。
|
||||
*
|
||||
* @param name Tool 名称
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Tool 描述。
|
||||
*
|
||||
* @return Tool 描述
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Tool 描述。
|
||||
*
|
||||
* @param description Tool 描述
|
||||
*/
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取输入 Schema。
|
||||
*
|
||||
* @return 输入 Schema
|
||||
*/
|
||||
public Object getInputSchema() {
|
||||
return inputSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置输入 Schema。
|
||||
*
|
||||
* @param inputSchema 输入 Schema
|
||||
*/
|
||||
public void setInputSchema(Object inputSchema) {
|
||||
this.inputSchema = inputSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取输出 Schema。
|
||||
*
|
||||
* @return 输出 Schema
|
||||
*/
|
||||
public Object getOutputSchema() {
|
||||
return outputSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置输出 Schema。
|
||||
*
|
||||
* @param outputSchema 输出 Schema
|
||||
*/
|
||||
public void setOutputSchema(Object outputSchema) {
|
||||
this.outputSchema = outputSchema;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof McpToolManifestEntry that)) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(name, that.name)
|
||||
&& Objects.equals(description, that.description)
|
||||
&& Objects.equals(inputSchema, that.inputSchema)
|
||||
&& Objects.equals(outputSchema, that.outputSchema);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, description, inputSchema, outputSchema);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package com.easyagents.agent.runtime.mcp;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolCategory;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolVisibility;
|
||||
import io.agentscope.core.tool.Toolkit;
|
||||
import io.agentscope.core.tool.mcp.McpClientWrapper;
|
||||
import io.agentscope.core.tool.mcp.McpTool;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 将 MCP 运行时声明注册到 AgentScope Toolkit。
|
||||
*/
|
||||
public class McpToolkitAdapter {
|
||||
|
||||
private final McpClientFactory clientFactory;
|
||||
|
||||
/**
|
||||
* 使用默认 MCP client factory 创建适配器。
|
||||
*/
|
||||
public McpToolkitAdapter() {
|
||||
this(new McpClientFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定 MCP client factory 创建适配器。
|
||||
*
|
||||
* @param clientFactory MCP client factory
|
||||
*/
|
||||
public McpToolkitAdapter(McpClientFactory clientFactory) {
|
||||
this.clientFactory = clientFactory == null ? new McpClientFactory() : clientFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MCP 声明注册到 Toolkit。
|
||||
*
|
||||
* @param specs MCP 声明
|
||||
* @param toolkit AgentScope Toolkit
|
||||
* @return MCP 注册结果
|
||||
*/
|
||||
public McpRegistration register(List<McpSpec> specs, Toolkit toolkit) {
|
||||
if (specs == null || specs.isEmpty()) {
|
||||
return McpRegistration.empty();
|
||||
}
|
||||
if (toolkit == null) {
|
||||
throw new AgentRuntimeException("AgentScope toolkit is required for MCP registration.");
|
||||
}
|
||||
List<McpClientWrapper> clients = new ArrayList<>();
|
||||
List<AgentToolSpec> toolSpecs = new ArrayList<>();
|
||||
List<McpSkillRegistration> skillRegistrations = new ArrayList<>();
|
||||
try {
|
||||
for (McpSpec spec : specs) {
|
||||
if (spec == null) {
|
||||
continue;
|
||||
}
|
||||
McpSpecValidator.validateConnection(spec);
|
||||
McpClientWrapper client = clientFactory.create(spec);
|
||||
clients.add(client);
|
||||
if (isSkillBound(spec)) {
|
||||
List<McpSchema.Tool> actualTools = initializeAndListTools(client);
|
||||
McpToolManifest.assertFrozenManifest(spec, actualTools);
|
||||
client = new FrozenMcpClientWrapper(client, actualTools,
|
||||
spec.getFrozenToolManifest(), spec.getToolAliases(), spec.getToolNamePrefix());
|
||||
} else {
|
||||
client = applyAliases(spec, client);
|
||||
}
|
||||
clients.set(clients.size() - 1, client);
|
||||
if (isSkillBound(spec)) {
|
||||
// Skill MCP 必须以冻结 manifest 派生白名单,调用方不能通过空列表放宽到远端全部 Tool。
|
||||
spec.setEnableTools(frozenRuntimeToolNames(spec, client));
|
||||
skillRegistrations.add(new McpSkillRegistration(
|
||||
spec.getSkillId(), client, spec.getEnableTools(), spec.getDisableTools(),
|
||||
spec.getPresetParameters()));
|
||||
} else {
|
||||
registerClient(spec, client, toolkit);
|
||||
}
|
||||
toolSpecs.addAll(toToolSpecs(spec, registeredTools(spec, client)));
|
||||
}
|
||||
} catch (RuntimeException error) {
|
||||
closeQuietly(clients);
|
||||
throw error;
|
||||
}
|
||||
return new McpRegistration(clients, toolSpecs, skillRegistrations);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据冻结原始 Tool 名称和别名后的远端清单生成强制运行白名单。
|
||||
*
|
||||
* @param spec Skill MCP 声明
|
||||
* @param client 已应用运行别名的 client
|
||||
* @return 冻结 Tool 对应的运行名
|
||||
*/
|
||||
private List<String> frozenRuntimeToolNames(McpSpec spec, McpClientWrapper client) {
|
||||
Set<String> frozenRawNames = new LinkedHashSet<>();
|
||||
for (McpToolManifestEntry entry : spec.getFrozenToolManifest()) {
|
||||
if (entry != null && entry.getName() != null && !entry.getName().isBlank()) {
|
||||
frozenRawNames.add(entry.getName());
|
||||
}
|
||||
}
|
||||
List<String> names = new ArrayList<>();
|
||||
for (McpSchema.Tool tool : listTools(client)) {
|
||||
if (tool != null && frozenRawNames.contains(rawToolName(spec, tool))) {
|
||||
names.add(tool.name());
|
||||
}
|
||||
}
|
||||
if (names.size() != frozenRawNames.size()) {
|
||||
throw new AgentRuntimeException("Skill-bound MCP frozen tool aliases are incomplete: "
|
||||
+ spec.getName());
|
||||
}
|
||||
return List.copyOf(names);
|
||||
}
|
||||
|
||||
private McpClientWrapper applyAliases(McpSpec spec, McpClientWrapper client) {
|
||||
boolean hasExplicitAliases = spec.getToolAliases() != null && !spec.getToolAliases().isEmpty();
|
||||
boolean hasDynamicPrefix = spec.getToolNamePrefix() != null && !spec.getToolNamePrefix().isBlank();
|
||||
if (!hasExplicitAliases && !hasDynamicPrefix) {
|
||||
return client;
|
||||
}
|
||||
return new AliasedMcpClientWrapper(client, spec.getToolAliases(), spec.getToolNamePrefix());
|
||||
}
|
||||
|
||||
private void registerClient(McpSpec spec, McpClientWrapper client, Toolkit toolkit) {
|
||||
String groupName = blankToNull(spec.getGroupName());
|
||||
if (groupName != null && toolkit.getToolGroup(groupName) == null) {
|
||||
toolkit.createToolGroup(groupName, spec.getDescription(), true);
|
||||
}
|
||||
toolkit.registration()
|
||||
.mcpClient(client)
|
||||
.enableTools(emptyToNull(spec.getEnableTools()))
|
||||
.disableTools(emptyToNull(spec.getDisableTools()))
|
||||
.group(groupName)
|
||||
.presetParameters(emptyToNull(spec.getPresetParameters()))
|
||||
.apply();
|
||||
}
|
||||
|
||||
private List<McpSchema.Tool> registeredTools(McpSpec spec, McpClientWrapper client) {
|
||||
List<McpSchema.Tool> tools = listTools(client);
|
||||
if (tools == null || tools.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<McpSchema.Tool> filtered = new ArrayList<>();
|
||||
for (McpSchema.Tool tool : tools) {
|
||||
if (tool != null && shouldRegister(tool.name(), spec.getEnableTools(), spec.getDisableTools())) {
|
||||
filtered.add(tool);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private List<McpSchema.Tool> listTools(McpClientWrapper client) {
|
||||
List<McpSchema.Tool> tools = client.listTools().block();
|
||||
return tools == null ? List.of() : tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 client 后读取一次远端 Tool 清单。
|
||||
*
|
||||
* @param client MCP client
|
||||
* @return Tool 清单
|
||||
*/
|
||||
private List<McpSchema.Tool> initializeAndListTools(McpClientWrapper client) {
|
||||
// AgentScope validates the initialized flag when listTools() is invoked. Build the
|
||||
// second publisher only after initialization has completed, otherwise eager publisher
|
||||
// assembly can fail even though the server initializes successfully moments later.
|
||||
client.initialize().block();
|
||||
List<McpSchema.Tool> tools = client.listTools().block();
|
||||
return tools == null ? List.of() : tools;
|
||||
}
|
||||
|
||||
private boolean isSkillBound(McpSpec spec) {
|
||||
return spec.getSkillId() != null && !spec.getSkillId().isBlank();
|
||||
}
|
||||
|
||||
private boolean shouldRegister(String toolName, List<String> enableTools, List<String> disableTools) {
|
||||
if (enableTools != null && !enableTools.isEmpty()) {
|
||||
return enableTools.contains(toolName);
|
||||
}
|
||||
return disableTools == null || disableTools.isEmpty() || !disableTools.contains(toolName);
|
||||
}
|
||||
|
||||
private List<AgentToolSpec> toToolSpecs(McpSpec spec, List<McpSchema.Tool> tools) {
|
||||
if (tools == null || tools.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<AgentToolSpec> toolSpecs = new ArrayList<>();
|
||||
for (McpSchema.Tool tool : tools) {
|
||||
AgentToolSpec toolSpec = new AgentToolSpec();
|
||||
Set<String> excludedPresetNames = Set.of();
|
||||
Map<String, Object> toolPresetParameters = spec.getPresetParameters() == null
|
||||
? null
|
||||
: spec.getPresetParameters().get(tool.name());
|
||||
if (toolPresetParameters != null) {
|
||||
excludedPresetNames = toolPresetParameters.keySet();
|
||||
}
|
||||
toolSpec.setName(tool.name());
|
||||
toolSpec.setDescription(tool.description());
|
||||
toolSpec.setCategory(AgentToolCategory.MCP);
|
||||
toolSpec.setVisibility(AgentToolVisibility.VISIBLE);
|
||||
toolSpec.setParametersSchema(McpTool.convertMcpSchemaToParameters(tool.inputSchema(), excludedPresetNames));
|
||||
toolSpec.setOutputSchema(tool.outputSchema());
|
||||
AgentToolApprovalRequest toolApprovalRequest = toolApprovalRequest(spec, tool.name());
|
||||
toolSpec.setApprovalRequired(spec.isApprovalRequired() || toolApprovalRequest != null);
|
||||
toolSpec.setApprovalRequest(toolApprovalRequest == null ? spec.getApprovalRequest() : toolApprovalRequest);
|
||||
toolSpec.setMetadata(metadata(spec, tool));
|
||||
toolSpecs.add(toolSpec);
|
||||
}
|
||||
return toolSpecs;
|
||||
}
|
||||
|
||||
private AgentToolApprovalRequest toolApprovalRequest(McpSpec spec, String toolName) {
|
||||
if (spec.getToolApprovalRequests() == null || spec.getToolApprovalRequests().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return spec.getToolApprovalRequests().get(toolName);
|
||||
}
|
||||
|
||||
private Map<String, Object> metadata(McpSpec spec, McpSchema.Tool tool) {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
if (spec.getMetadata() != null) {
|
||||
spec.getMetadata().forEach((key, value) -> {
|
||||
if (!isSensitiveMetadataKey(key)) {
|
||||
metadata.put(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
metadata.put("source", "MCP");
|
||||
metadata.put("mcpName", spec.getName());
|
||||
metadata.put("mcpToolName", tool.name());
|
||||
metadata.put("rawMcpToolName", rawToolName(spec, tool));
|
||||
metadata.put("toolDisplayName", toolDisplayName(spec, tool));
|
||||
metadata.put("transportType", spec.getTransportType().configValue());
|
||||
if (isSkillBound(spec)) {
|
||||
metadata.put("skillId", spec.getSkillId());
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private String rawToolName(McpSpec spec, McpSchema.Tool tool) {
|
||||
if (tool != null && tool.meta() != null) {
|
||||
Object rawName = tool.meta().get(AliasedMcpClientWrapper.RAW_TOOL_NAME_META_KEY);
|
||||
if (rawName != null && !String.valueOf(rawName).isBlank()) {
|
||||
return String.valueOf(rawName);
|
||||
}
|
||||
}
|
||||
String toolName = tool == null ? null : tool.name();
|
||||
if (spec.getToolAliases() != null && !spec.getToolAliases().isEmpty()) {
|
||||
for (Map.Entry<String, String> entry : spec.getToolAliases().entrySet()) {
|
||||
if (Objects.equals(entry.getValue(), toolName)) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
return toolName;
|
||||
}
|
||||
|
||||
private String toolDisplayName(McpSpec spec, McpSchema.Tool tool) {
|
||||
String rawToolName = rawToolName(spec, tool);
|
||||
String mcpName = spec == null ? null : spec.getDescription();
|
||||
if (mcpName == null || mcpName.isBlank()) {
|
||||
mcpName = spec == null ? null : spec.getName();
|
||||
}
|
||||
if (mcpName == null || mcpName.isBlank()) {
|
||||
return rawToolName;
|
||||
}
|
||||
if (rawToolName == null || rawToolName.isBlank()) {
|
||||
return mcpName;
|
||||
}
|
||||
return mcpName + " - " + rawToolName;
|
||||
}
|
||||
|
||||
private boolean isSensitiveMetadataKey(String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String normalized = key.toLowerCase(Locale.ROOT).replace("-", "").replace("_", "");
|
||||
return normalized.contains("key")
|
||||
|| normalized.contains("token")
|
||||
|| normalized.contains("secret")
|
||||
|| normalized.contains("password")
|
||||
|| normalized.contains("authorization")
|
||||
|| normalized.contains("credential");
|
||||
}
|
||||
|
||||
private void closeQuietly(List<McpClientWrapper> clients) {
|
||||
for (McpClientWrapper client : clients) {
|
||||
if (client == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
client.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private <T> List<T> emptyToNull(List<T> values) {
|
||||
return values == null || values.isEmpty() ? null : values;
|
||||
}
|
||||
|
||||
private <K, V> Map<K, V> emptyToNull(Map<K, V> values) {
|
||||
return values == null || values.isEmpty() ? null : values;
|
||||
}
|
||||
|
||||
private String blankToNull(String value) {
|
||||
return value == null || value.isBlank() ? null : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.easyagents.agent.runtime.mcp;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* MCP 连接方式。
|
||||
*/
|
||||
public enum McpTransportType {
|
||||
|
||||
/**
|
||||
* 标准输入输出进程通信。
|
||||
*/
|
||||
STDIO,
|
||||
|
||||
/**
|
||||
* HTTP Server-Sent Events 通信。
|
||||
*/
|
||||
SSE,
|
||||
|
||||
/**
|
||||
* Streamable HTTP 通信。
|
||||
*/
|
||||
HTTP;
|
||||
|
||||
/**
|
||||
* 解析 MCP 连接方式。
|
||||
*
|
||||
* @param value 连接方式文本
|
||||
* @return MCP 连接方式
|
||||
*/
|
||||
public static McpTransportType from(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return STDIO;
|
||||
}
|
||||
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||
return switch (normalized) {
|
||||
case "stdio" -> STDIO;
|
||||
case "sse", "http-sse" -> SSE;
|
||||
case "http", "streamable-http", "http-stream" -> HTTP;
|
||||
default -> throw new AgentRuntimeException("Unsupported MCP transport type: " + value);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 Easy MCP 配置兼容值。
|
||||
*
|
||||
* @return transport 配置值
|
||||
*/
|
||||
public String configValue() {
|
||||
return switch (this) {
|
||||
case STDIO -> "stdio";
|
||||
case SSE -> "http-sse";
|
||||
case HTTP -> "http-stream";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.easyagents.agent.runtime.media;
|
||||
|
||||
/**
|
||||
* 在模型调用边界解析业务侧稳定媒体引用。
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface AgentMediaResolver {
|
||||
|
||||
/**
|
||||
* 解析媒体引用。
|
||||
*
|
||||
* @param reference 业务侧稳定媒体引用
|
||||
* @return 媒体资源
|
||||
* @throws RuntimeException 引用无效、越权或资源读取失败时抛出
|
||||
*/
|
||||
AgentMediaResource resolve(String reference);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.easyagents.agent.runtime.media;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 模型调用前解析得到的媒体资源。
|
||||
*
|
||||
* @param mimeType MIME 类型
|
||||
* @param bytes 媒体字节
|
||||
*/
|
||||
public record AgentMediaResource(String mimeType, byte[] bytes) {
|
||||
|
||||
/**
|
||||
* 创建不可变媒体资源。
|
||||
*/
|
||||
public AgentMediaResource {
|
||||
bytes = bytes == null ? new byte[0] : Arrays.copyOf(bytes, bytes.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回媒体字节副本。
|
||||
*
|
||||
* @return 媒体字节副本
|
||||
*/
|
||||
@Override
|
||||
public byte[] bytes() {
|
||||
return Arrays.copyOf(bytes, bytes.length);
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,12 @@ package com.easyagents.agent.runtime.memory;
|
||||
public class AgentMemoryCompressionParameter {
|
||||
|
||||
private boolean enabled = true;
|
||||
private int msgThreshold = 20;
|
||||
private Integer msgThreshold;
|
||||
private int lastKeep = 8;
|
||||
private double tokenRatio = 0.7D;
|
||||
private long maxToken = 12000L;
|
||||
private Double tokenRatio;
|
||||
private Long maxToken;
|
||||
private long largePayloadThreshold = 2048L;
|
||||
private int minCompressionTokenThreshold = 1000;
|
||||
private Integer minCompressionTokenThreshold;
|
||||
private double currentRoundCompressionRatio = 0.5D;
|
||||
private int minConsecutiveToolMessages = 4;
|
||||
|
||||
@@ -38,7 +38,7 @@ public class AgentMemoryCompressionParameter {
|
||||
*
|
||||
* @return 消息阈值
|
||||
*/
|
||||
public int getMsgThreshold() {
|
||||
public Integer getMsgThreshold() {
|
||||
return msgThreshold;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public class AgentMemoryCompressionParameter {
|
||||
* @param msgThreshold 消息阈值
|
||||
*/
|
||||
public void setMsgThreshold(int msgThreshold) {
|
||||
this.msgThreshold = msgThreshold <= 0 ? 20 : msgThreshold;
|
||||
this.msgThreshold = msgThreshold <= 0 ? null : msgThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +74,7 @@ public class AgentMemoryCompressionParameter {
|
||||
*
|
||||
* @return Token 比例
|
||||
*/
|
||||
public double getTokenRatio() {
|
||||
public Double getTokenRatio() {
|
||||
return tokenRatio;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public class AgentMemoryCompressionParameter {
|
||||
* @param tokenRatio Token 比例
|
||||
*/
|
||||
public void setTokenRatio(double tokenRatio) {
|
||||
this.tokenRatio = tokenRatio <= 0D ? 0.7D : tokenRatio;
|
||||
this.tokenRatio = tokenRatio <= 0D ? null : tokenRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +92,7 @@ public class AgentMemoryCompressionParameter {
|
||||
*
|
||||
* @return 最大 Token 数
|
||||
*/
|
||||
public long getMaxToken() {
|
||||
public Long getMaxToken() {
|
||||
return maxToken;
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ public class AgentMemoryCompressionParameter {
|
||||
* @param maxToken 最大 Token 数
|
||||
*/
|
||||
public void setMaxToken(long maxToken) {
|
||||
this.maxToken = maxToken <= 0L ? 12000L : maxToken;
|
||||
this.maxToken = maxToken <= 0L ? null : maxToken;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,7 +128,7 @@ public class AgentMemoryCompressionParameter {
|
||||
*
|
||||
* @return 最小压缩 Token 阈值
|
||||
*/
|
||||
public int getMinCompressionTokenThreshold() {
|
||||
public Integer getMinCompressionTokenThreshold() {
|
||||
return minCompressionTokenThreshold;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ public class AgentMemoryCompressionParameter {
|
||||
* @param minCompressionTokenThreshold 最小压缩 Token 阈值
|
||||
*/
|
||||
public void setMinCompressionTokenThreshold(int minCompressionTokenThreshold) {
|
||||
this.minCompressionTokenThreshold = Math.max(0, minCompressionTokenThreshold);
|
||||
this.minCompressionTokenThreshold = minCompressionTokenThreshold <= 0 ? null : minCompressionTokenThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,7 @@ public class AgentMemorySnapshot {
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加one message。
|
||||
* 添加消息。
|
||||
*
|
||||
* @param message 消息
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,7 @@ public class AgentKnowledgeReference {
|
||||
private String documentId;
|
||||
private String documentName;
|
||||
private String chunkId;
|
||||
private String chunkContent;
|
||||
private String sourceUri;
|
||||
private Double score;
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
@@ -107,6 +108,24 @@ public class AgentKnowledgeReference {
|
||||
this.chunkId = chunkId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取命中分片原文。
|
||||
*
|
||||
* @return 命中分片原文
|
||||
*/
|
||||
public String getChunkContent() {
|
||||
return chunkContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置命中分片原文。
|
||||
*
|
||||
* @param chunkContent 命中分片原文
|
||||
*/
|
||||
public void setChunkContent(String chunkContent) {
|
||||
this.chunkContent = chunkContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取来源 URI。
|
||||
*
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.util.Map;
|
||||
public class AgentMediaBlock extends AgentContentBlock {
|
||||
|
||||
private String mimeType;
|
||||
private String reference;
|
||||
private String url;
|
||||
private String data;
|
||||
private Integer minPixels;
|
||||
@@ -64,6 +65,24 @@ public class AgentMediaBlock extends AgentContentBlock {
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取由业务侧解析的稳定媒体引用。
|
||||
*
|
||||
* @return 媒体引用
|
||||
*/
|
||||
public String getReference() {
|
||||
return reference;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置由业务侧解析的稳定媒体引用。
|
||||
*
|
||||
* @param reference 媒体引用
|
||||
*/
|
||||
public void setReference(String reference) {
|
||||
this.reference = reference;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 URL。
|
||||
*
|
||||
@@ -209,6 +228,7 @@ public class AgentMediaBlock extends AgentContentBlock {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>(getMetadata());
|
||||
metadata.put("mediaKind", mediaKind);
|
||||
metadata.put("mimeType", mimeType);
|
||||
metadata.put("reference", reference);
|
||||
metadata.put("url", url);
|
||||
metadata.put("data", data);
|
||||
metadata.put("minPixels", minPixels);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.easyagents.agent.runtime.model;
|
||||
|
||||
/**
|
||||
* Agent 模型调用使用的 HTTP 版本策略。
|
||||
*/
|
||||
public enum AgentHttpVersionPolicy {
|
||||
|
||||
/** 按基础 URL 协议自动选择:HTTP 使用 1.1,HTTPS 优先使用 2。 */
|
||||
AUTO,
|
||||
|
||||
/** 强制使用 HTTP/1.1。 */
|
||||
HTTP_1_1,
|
||||
|
||||
/** 优先使用 HTTP/2,并允许 JDK 客户端按协议能力回退。 */
|
||||
HTTP_2_PREFERRED
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.easyagents.agent.runtime.model;
|
||||
|
||||
/**
|
||||
* OpenAI-compatible 请求中消息 content 的格式策略。
|
||||
*/
|
||||
public enum AgentMessageContentFormat {
|
||||
|
||||
/** 使用 AgentScope 默认格式,纯文本为字符串,多模态内容为数组。 */
|
||||
STANDARD,
|
||||
|
||||
/** 将全部角色的 content 规范为内容块数组。 */
|
||||
TEXT_PARTS
|
||||
}
|
||||
@@ -13,6 +13,10 @@ public class AgentModelSpec {
|
||||
private String baseUrl;
|
||||
private String endpointPath;
|
||||
private String apiKey;
|
||||
private boolean supportImage;
|
||||
private boolean supportImageBase64Only;
|
||||
private AgentHttpVersionPolicy httpVersionPolicy = AgentHttpVersionPolicy.AUTO;
|
||||
private AgentMessageContentFormat messageContentFormat = AgentMessageContentFormat.STANDARD;
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
@@ -105,6 +109,80 @@ public class AgentModelSpec {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断模型是否支持图片输入。
|
||||
*
|
||||
* @return 支持图片时返回 true
|
||||
*/
|
||||
public boolean isSupportImage() {
|
||||
return supportImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置模型是否支持图片输入。
|
||||
*
|
||||
* @param supportImage 是否支持图片
|
||||
*/
|
||||
public void setSupportImage(boolean supportImage) {
|
||||
this.supportImage = supportImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断模型是否只接受 Base64 图片。
|
||||
*
|
||||
* @return 仅接受 Base64 时返回 true
|
||||
*/
|
||||
public boolean isSupportImageBase64Only() {
|
||||
return supportImageBase64Only;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置模型是否只接受 Base64 图片。
|
||||
*
|
||||
* @param supportImageBase64Only 是否只接受 Base64 图片
|
||||
*/
|
||||
public void setSupportImageBase64Only(boolean supportImageBase64Only) {
|
||||
this.supportImageBase64Only = supportImageBase64Only;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent 模型调用的 HTTP 版本策略。
|
||||
*
|
||||
* @return HTTP 版本策略
|
||||
*/
|
||||
public AgentHttpVersionPolicy getHttpVersionPolicy() {
|
||||
return httpVersionPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 Agent 模型调用的 HTTP 版本策略。
|
||||
*
|
||||
* @param httpVersionPolicy HTTP 版本策略
|
||||
*/
|
||||
public void setHttpVersionPolicy(AgentHttpVersionPolicy httpVersionPolicy) {
|
||||
this.httpVersionPolicy = httpVersionPolicy == null ? AgentHttpVersionPolicy.AUTO : httpVersionPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OpenAI-compatible 请求中的消息 content 格式。
|
||||
*
|
||||
* @return 消息 content 格式
|
||||
*/
|
||||
public AgentMessageContentFormat getMessageContentFormat() {
|
||||
return messageContentFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 OpenAI-compatible 请求中的消息 content 格式。
|
||||
*
|
||||
* @param messageContentFormat 消息 content 格式
|
||||
*/
|
||||
public void setMessageContentFormat(AgentMessageContentFormat messageContentFormat) {
|
||||
this.messageContentFormat = messageContentFormat == null
|
||||
? AgentMessageContentFormat.STANDARD
|
||||
: messageContentFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
*
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
package com.easyagents.agent.runtime.persistence;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 运行时状态项。
|
||||
*
|
||||
* <p>MVP 中 value 是 JVM 对象态,不是稳定的传输格式,
|
||||
* 非内存 {@link AgentSessionStore} 实现必须显式完成序列化,
|
||||
* 再写入外部存储。</p>
|
||||
*/
|
||||
public class AgentRuntimeState {
|
||||
|
||||
private String name;
|
||||
private Object value;
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 创建a 状态项。
|
||||
*
|
||||
* @param name 状态名称
|
||||
* @param value 状态值
|
||||
* @return 状态项
|
||||
*/
|
||||
public static AgentRuntimeState of(String name, Object value) {
|
||||
AgentRuntimeState state = new AgentRuntimeState();
|
||||
state.setName(name);
|
||||
state.setValue(value);
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态名称。
|
||||
*
|
||||
* @return 状态名称
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置状态名称。
|
||||
*
|
||||
* @param name 状态名称
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态值。
|
||||
*
|
||||
* @return 状态值
|
||||
*/
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置状态值。
|
||||
*
|
||||
* @param value 状态值
|
||||
*/
|
||||
public void setValue(Object value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
*
|
||||
* @return 元数据
|
||||
*/
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置元数据。
|
||||
*
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package com.easyagents.agent.runtime.persistence;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 用于 AgentScope 类运行时会话状态的存储 SPI。
|
||||
*
|
||||
*/
|
||||
public interface AgentSessionStore {
|
||||
|
||||
/**
|
||||
* 保存one state value。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @param name 状态名称
|
||||
* @param state 状态值
|
||||
*/
|
||||
void save(String sessionKey, String name, AgentRuntimeState state);
|
||||
|
||||
/**
|
||||
* 保存a state list。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @param name 状态名称
|
||||
* @param states 状态列表
|
||||
*/
|
||||
void saveList(String sessionKey, String name, List<AgentRuntimeState> states);
|
||||
|
||||
/**
|
||||
* 获取one state value。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @param name 状态名称
|
||||
* @return 可选状态
|
||||
*/
|
||||
Optional<AgentRuntimeState> get(String sessionKey, String name);
|
||||
|
||||
/**
|
||||
* 获取a state list。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @param name 状态名称
|
||||
* @return 状态列表
|
||||
*/
|
||||
List<AgentRuntimeState> getList(String sessionKey, String name);
|
||||
|
||||
/**
|
||||
* 返回是否session key exists。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @return 存在时为 true
|
||||
*/
|
||||
boolean exists(String sessionKey);
|
||||
|
||||
/**
|
||||
* 删除a session key。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
*/
|
||||
void delete(String sessionKey);
|
||||
|
||||
/**
|
||||
* 列出会话键列表。
|
||||
*
|
||||
* @return 会话键列表
|
||||
*/
|
||||
Set<String> listSessionKeys();
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.easyagents.agent.runtime.persistence;
|
||||
package com.easyagents.agent.runtime.persistence.conversation;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRunRequest;
|
||||
import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
|
||||
/**
|
||||
@@ -14,5 +14,5 @@ public interface AgentConversationRecorder {
|
||||
* @param request 运行请求
|
||||
* @param event 运行时事件
|
||||
*/
|
||||
void record(AgentRunRequest request, AgentRuntimeEvent event);
|
||||
void record(AgentRuntimeExecutionContext request, AgentRuntimeEvent event);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.easyagents.agent.runtime.persistence.conversation.noop;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.persistence.conversation.AgentConversationRecorder;
|
||||
|
||||
/**
|
||||
* 空操作会话记录器。
|
||||
*/
|
||||
public enum NoopAgentConversationRecorder implements AgentConversationRecorder {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public void record(AgentRuntimeExecutionContext request, AgentRuntimeEvent event) {
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package com.easyagents.agent.runtime.persistence.json;
|
||||
|
||||
import com.easyagents.agent.runtime.persistence.AgentRuntimeState;
|
||||
import com.easyagents.agent.runtime.persistence.AgentSessionStoreException;
|
||||
import io.agentscope.core.state.State;
|
||||
import io.agentscope.core.util.JsonUtils;
|
||||
|
||||
/**
|
||||
* AgentScope 状态的 JSON 编解码器。
|
||||
*/
|
||||
public class AgentSessionStateCodec {
|
||||
|
||||
/**
|
||||
* 将状态对象编码为可持久化记录。
|
||||
*
|
||||
* @param state 状态对象
|
||||
* @return 可持久化记录
|
||||
*/
|
||||
public SerializedAgentRuntimeState encode(AgentRuntimeState state) {
|
||||
if (state == null || state.getValue() == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = state.getValue();
|
||||
if (!(value instanceof State)) {
|
||||
throw new AgentSessionStoreException("Only AgentScope State values can be serialized: "
|
||||
+ value.getClass().getName());
|
||||
}
|
||||
SerializedAgentRuntimeState serialized = new SerializedAgentRuntimeState();
|
||||
serialized.setName(state.getName());
|
||||
serialized.setStateClassName(value.getClass().getName());
|
||||
serialized.setStateJson(JsonUtils.getJsonCodec().toJson(value));
|
||||
serialized.setMetadata(state.getMetadata());
|
||||
return serialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将持久化记录解码为运行时状态。
|
||||
*
|
||||
* @param serialized 可持久化记录
|
||||
* @return 运行时状态
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public AgentRuntimeState decode(SerializedAgentRuntimeState serialized) {
|
||||
if (serialized == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Class<?> clazz = Class.forName(serialized.getStateClassName());
|
||||
if (!State.class.isAssignableFrom(clazz)) {
|
||||
throw new AgentSessionStoreException("Serialized state class is not AgentScope State: "
|
||||
+ serialized.getStateClassName());
|
||||
}
|
||||
State value = JsonUtils.getJsonCodec().fromJson(serialized.getStateJson(), (Class<? extends State>) clazz);
|
||||
AgentRuntimeState state = AgentRuntimeState.of(serialized.getName(), value);
|
||||
state.setMetadata(serialized.getMetadata());
|
||||
return state;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new AgentSessionStoreException("Serialized state class not found: "
|
||||
+ serialized.getStateClassName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.easyagents.agent.runtime.persistence.json;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* JSON 会话状态的底层键值存储后端。
|
||||
*/
|
||||
public interface AgentSessionStoreBackend {
|
||||
|
||||
/**
|
||||
* 保存单个状态记录。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @param name 状态名称
|
||||
* @param state 状态记录
|
||||
*/
|
||||
void save(String sessionKey, String name, SerializedAgentRuntimeState state);
|
||||
|
||||
/**
|
||||
* 保存状态记录列表。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @param name 状态名称
|
||||
* @param states 状态记录列表
|
||||
*/
|
||||
void saveList(String sessionKey, String name, List<SerializedAgentRuntimeState> states);
|
||||
|
||||
/**
|
||||
* 获取单个状态记录。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @param name 状态名称
|
||||
* @return 状态记录
|
||||
*/
|
||||
Optional<SerializedAgentRuntimeState> get(String sessionKey, String name);
|
||||
|
||||
/**
|
||||
* 获取状态记录列表。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @param name 状态名称
|
||||
* @return 状态记录列表
|
||||
*/
|
||||
List<SerializedAgentRuntimeState> getList(String sessionKey, String name);
|
||||
|
||||
/**
|
||||
* 判断会话是否存在。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @return 存在时为 true
|
||||
*/
|
||||
boolean exists(String sessionKey);
|
||||
|
||||
/**
|
||||
* 删除会话。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
*/
|
||||
void delete(String sessionKey);
|
||||
|
||||
/**
|
||||
* 列出全部会话键。
|
||||
*
|
||||
* @return 会话键集合
|
||||
*/
|
||||
Set<String> listSessionKeys();
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
package com.easyagents.agent.runtime.persistence.json;
|
||||
|
||||
import com.easyagents.agent.runtime.persistence.AgentSessionStoreException;
|
||||
import io.agentscope.core.util.JsonUtils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 基于本地文件的 JSON 会话状态后端。
|
||||
*/
|
||||
public class FileAgentSessionStoreBackend implements AgentSessionStoreBackend {
|
||||
|
||||
private static final Pattern SAFE_NAME = Pattern.compile("[A-Za-z0-9._-]+");
|
||||
private final Path rootDirectory;
|
||||
|
||||
/**
|
||||
* 创建文件后端。
|
||||
*
|
||||
* @param rootDirectory 根目录
|
||||
*/
|
||||
public FileAgentSessionStoreBackend(Path rootDirectory) {
|
||||
if (rootDirectory == null) {
|
||||
throw new AgentSessionStoreException("Agent session root directory is required.");
|
||||
}
|
||||
this.rootDirectory = rootDirectory;
|
||||
try {
|
||||
Files.createDirectories(rootDirectory);
|
||||
} catch (IOException e) {
|
||||
throw new AgentSessionStoreException("Failed to create agent session root directory: " + rootDirectory, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(String sessionKey, String name, SerializedAgentRuntimeState state) {
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
Path path = statePath(sessionKey, name);
|
||||
createParent(path);
|
||||
try {
|
||||
Files.writeString(path, JsonUtils.getJsonCodec().toPrettyJson(state), StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
|
||||
} catch (IOException e) {
|
||||
throw new AgentSessionStoreException("Failed to save agent session state: " + sessionKey + "/" + name, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveList(String sessionKey, String name, List<SerializedAgentRuntimeState> states) {
|
||||
Path path = listPath(sessionKey, name);
|
||||
createParent(path);
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
|
||||
if (states == null) {
|
||||
return;
|
||||
}
|
||||
for (SerializedAgentRuntimeState state : states) {
|
||||
writer.write(JsonUtils.getJsonCodec().toJson(state));
|
||||
writer.newLine();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AgentSessionStoreException("Failed to save agent session state list: " + sessionKey + "/" + name, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<SerializedAgentRuntimeState> get(String sessionKey, String name) {
|
||||
Path path = statePath(sessionKey, name);
|
||||
if (!Files.exists(path)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
String json = Files.readString(path, StandardCharsets.UTF_8);
|
||||
return Optional.of(JsonUtils.getJsonCodec().fromJson(json, SerializedAgentRuntimeState.class));
|
||||
} catch (IOException e) {
|
||||
throw new AgentSessionStoreException("Failed to load agent session state: " + sessionKey + "/" + name, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SerializedAgentRuntimeState> getList(String sessionKey, String name) {
|
||||
Path path = listPath(sessionKey, name);
|
||||
if (!Files.exists(path)) {
|
||||
return List.of();
|
||||
}
|
||||
List<SerializedAgentRuntimeState> states = new ArrayList<>();
|
||||
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (!line.isBlank()) {
|
||||
states.add(JsonUtils.getJsonCodec().fromJson(line, SerializedAgentRuntimeState.class));
|
||||
}
|
||||
}
|
||||
return states;
|
||||
} catch (IOException e) {
|
||||
throw new AgentSessionStoreException("Failed to load agent session state list: " + sessionKey + "/" + name, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String sessionKey) {
|
||||
return Files.isDirectory(sessionDirectory(sessionKey));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String sessionKey) {
|
||||
Path directory = sessionDirectory(sessionKey);
|
||||
if (!Files.exists(directory)) {
|
||||
return;
|
||||
}
|
||||
try (Stream<Path> paths = Files.walk(directory)) {
|
||||
List<Path> ordered = paths.sorted(Comparator.reverseOrder()).collect(Collectors.toList());
|
||||
for (Path path : ordered) {
|
||||
Files.deleteIfExists(path);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AgentSessionStoreException("Failed to delete agent session: " + sessionKey, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> listSessionKeys() {
|
||||
if (!Files.exists(rootDirectory)) {
|
||||
return Set.of();
|
||||
}
|
||||
try (Stream<Path> paths = Files.list(rootDirectory)) {
|
||||
return paths.filter(Files::isDirectory)
|
||||
.map(path -> path.getFileName().toString())
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
} catch (IOException e) {
|
||||
throw new AgentSessionStoreException("Failed to list agent sessions.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Path statePath(String sessionKey, String name) {
|
||||
return sessionDirectory(sessionKey).resolve(safeName(name) + ".json");
|
||||
}
|
||||
|
||||
private Path listPath(String sessionKey, String name) {
|
||||
return sessionDirectory(sessionKey).resolve(safeName(name) + ".jsonl");
|
||||
}
|
||||
|
||||
private Path sessionDirectory(String sessionKey) {
|
||||
return rootDirectory.resolve(safeName(sessionKey));
|
||||
}
|
||||
|
||||
private String safeName(String value) {
|
||||
if (value == null || value.isBlank() || !SAFE_NAME.matcher(value).matches()
|
||||
|| value.contains("..")) {
|
||||
throw new AgentSessionStoreException("Unsafe agent session storage key: " + value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private void createParent(Path path) {
|
||||
try {
|
||||
Files.createDirectories(path.getParent());
|
||||
} catch (IOException e) {
|
||||
throw new AgentSessionStoreException("Failed to create agent session directory: " + path.getParent(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package com.easyagents.agent.runtime.persistence.json;
|
||||
|
||||
import com.easyagents.agent.runtime.persistence.AgentRuntimeState;
|
||||
import com.easyagents.agent.runtime.persistence.AgentSessionStore;
|
||||
import com.easyagents.agent.runtime.persistence.AgentSessionStoreException;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 基于 JSON 记录的默认非内存会话存储。
|
||||
*/
|
||||
public class JsonAgentSessionStore implements AgentSessionStore {
|
||||
|
||||
private final AgentSessionStoreBackend backend;
|
||||
private final AgentSessionStateCodec codec;
|
||||
|
||||
/**
|
||||
* 创建文件型 JSON 会话存储。
|
||||
*
|
||||
* @param rootDirectory 根目录
|
||||
*/
|
||||
public JsonAgentSessionStore(Path rootDirectory) {
|
||||
this(new FileAgentSessionStoreBackend(rootDirectory), new AgentSessionStateCodec());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建可替换后端的 JSON 会话存储。
|
||||
*
|
||||
* @param backend 底层存储后端
|
||||
*/
|
||||
public JsonAgentSessionStore(AgentSessionStoreBackend backend) {
|
||||
this(backend, new AgentSessionStateCodec());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建可替换后端和编解码器的 JSON 会话存储。
|
||||
*
|
||||
* @param backend 底层存储后端
|
||||
* @param codec 状态编解码器
|
||||
*/
|
||||
public JsonAgentSessionStore(AgentSessionStoreBackend backend, AgentSessionStateCodec codec) {
|
||||
if (backend == null) {
|
||||
throw new AgentSessionStoreException("Agent session store backend is required.");
|
||||
}
|
||||
this.backend = backend;
|
||||
this.codec = codec == null ? new AgentSessionStateCodec() : codec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(String sessionKey, String name, AgentRuntimeState state) {
|
||||
SerializedAgentRuntimeState serialized = codec.encode(state);
|
||||
if (serialized != null) {
|
||||
backend.save(sessionKey, name, serialized);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveList(String sessionKey, String name, List<AgentRuntimeState> states) {
|
||||
List<SerializedAgentRuntimeState> serialized = new ArrayList<>();
|
||||
if (states != null) {
|
||||
for (AgentRuntimeState state : states) {
|
||||
SerializedAgentRuntimeState item = codec.encode(state);
|
||||
if (item != null) {
|
||||
serialized.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
backend.saveList(sessionKey, name, serialized);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AgentRuntimeState> get(String sessionKey, String name) {
|
||||
return backend.get(sessionKey, name).map(codec::decode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentRuntimeState> getList(String sessionKey, String name) {
|
||||
List<AgentRuntimeState> states = new ArrayList<>();
|
||||
for (SerializedAgentRuntimeState item : backend.getList(sessionKey, name)) {
|
||||
AgentRuntimeState state = codec.decode(item);
|
||||
if (state != null) {
|
||||
states.add(state);
|
||||
}
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String sessionKey) {
|
||||
return backend.exists(sessionKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String sessionKey) {
|
||||
backend.delete(sessionKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> listSessionKeys() {
|
||||
return backend.listSessionKeys();
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package com.easyagents.agent.runtime.persistence.json;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 可跨进程持久化的 AgentScope 状态记录。
|
||||
*/
|
||||
public class SerializedAgentRuntimeState {
|
||||
|
||||
private String name;
|
||||
private String stateClassName;
|
||||
private String stateJson;
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
private Instant createdAt = Instant.now();
|
||||
|
||||
/**
|
||||
* 获取状态名称。
|
||||
*
|
||||
* @return 状态名称
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置状态名称。
|
||||
*
|
||||
* @param name 状态名称
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态类名。
|
||||
*
|
||||
* @return 状态类名
|
||||
*/
|
||||
public String getStateClassName() {
|
||||
return stateClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置状态类名。
|
||||
*
|
||||
* @param stateClassName 状态类名
|
||||
*/
|
||||
public void setStateClassName(String stateClassName) {
|
||||
this.stateClassName = stateClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态 JSON。
|
||||
*
|
||||
* @return 状态 JSON
|
||||
*/
|
||||
public String getStateJson() {
|
||||
return stateJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置状态 JSON。
|
||||
*
|
||||
* @param stateJson 状态 JSON
|
||||
*/
|
||||
public void setStateJson(String stateJson) {
|
||||
this.stateJson = stateJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
*
|
||||
* @return 元数据
|
||||
*/
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置元数据。
|
||||
*
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取创建时间。
|
||||
*
|
||||
* @return 创建时间
|
||||
*/
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置创建时间。
|
||||
*
|
||||
* @param createdAt 创建时间
|
||||
*/
|
||||
public void setCreatedAt(Instant createdAt) {
|
||||
this.createdAt = createdAt == null ? Instant.now() : createdAt;
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package com.easyagents.agent.runtime.persistence.memory;
|
||||
|
||||
import com.easyagents.agent.runtime.persistence.AgentRuntimeState;
|
||||
import com.easyagents.agent.runtime.persistence.AgentSessionStore;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 用于测试和单节点 MVP 的内存会话存储。
|
||||
*/
|
||||
public class InMemoryAgentSessionStore implements AgentSessionStore {
|
||||
|
||||
private final Map<String, Map<String, List<AgentRuntimeState>>> states = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void save(String sessionKey, String name, AgentRuntimeState state) {
|
||||
if (sessionKey == null || name == null || state == null) {
|
||||
return;
|
||||
}
|
||||
saveList(sessionKey, name, List.of(state));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveList(String sessionKey, String name, List<AgentRuntimeState> states) {
|
||||
if (sessionKey == null || name == null) {
|
||||
return;
|
||||
}
|
||||
this.states.computeIfAbsent(sessionKey, key -> new ConcurrentHashMap<>())
|
||||
.put(name, states == null ? new ArrayList<>() : new ArrayList<>(states));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AgentRuntimeState> get(String sessionKey, String name) {
|
||||
List<AgentRuntimeState> list = getList(sessionKey, name);
|
||||
return list.isEmpty() ? Optional.empty() : Optional.ofNullable(list.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentRuntimeState> getList(String sessionKey, String name) {
|
||||
Map<String, List<AgentRuntimeState>> byName = states.getOrDefault(sessionKey, new LinkedHashMap<>());
|
||||
return new ArrayList<>(byName.getOrDefault(name, new ArrayList<>()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String sessionKey) {
|
||||
return states.containsKey(sessionKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String sessionKey) {
|
||||
states.remove(sessionKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> listSessionKeys() {
|
||||
return new LinkedHashSet<>(states.keySet());
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.easyagents.agent.runtime.persistence.noop;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRunRequest;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.persistence.AgentConversationRecorder;
|
||||
|
||||
/**
|
||||
* 空操作会话记录器。
|
||||
*/
|
||||
public enum NoopAgentConversationRecorder implements AgentConversationRecorder {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public void record(AgentRunRequest request, AgentRuntimeEvent event) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.easyagents.agent.runtime.persistence.session;
|
||||
|
||||
import io.agentscope.core.state.State;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 智能体会话状态存储接口。
|
||||
* <p>
|
||||
* 该接口承接 AgentScope {@code Session} 的持久化读写能力,用于按会话键保存和恢复
|
||||
* Agent 运行过程中产生的状态,例如 memory、toolkit、plan notebook 或有状态工具数据。
|
||||
* 具体实现可以基于内存、Redis、MySQL 等存储介质。
|
||||
*/
|
||||
public interface AgentSessionStore {
|
||||
|
||||
/**
|
||||
* 保存单个状态项。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @param name 状态名称
|
||||
* @param state 状态值
|
||||
*/
|
||||
void save(String sessionKey, String name, State state);
|
||||
|
||||
/**
|
||||
* 保存状态列表。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @param name 状态名称
|
||||
* @param states 状态列表
|
||||
*/
|
||||
void saveList(String sessionKey, String name, List<? extends State> states);
|
||||
|
||||
/**
|
||||
* 获取单个状态项。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @param name 状态名称
|
||||
* @param type 状态类型
|
||||
* @param <T> 状态类型
|
||||
* @return 可选状态
|
||||
*/
|
||||
<T extends State> Optional<T> get(String sessionKey, String name, Class<T> type);
|
||||
|
||||
/**
|
||||
* 获取状态列表。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @param name 状态名称
|
||||
* @param itemType 状态元素类型
|
||||
* @param <T> 状态元素类型
|
||||
* @return 状态列表
|
||||
*/
|
||||
<T extends State> List<T> getList(String sessionKey, String name, Class<T> itemType);
|
||||
|
||||
/**
|
||||
* 判断会话键是否存在。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
* @return 存在时为 true
|
||||
*/
|
||||
boolean exists(String sessionKey);
|
||||
|
||||
/**
|
||||
* 删除指定会话键下的全部状态。
|
||||
*
|
||||
* @param sessionKey 会话键
|
||||
*/
|
||||
void delete(String sessionKey);
|
||||
|
||||
/**
|
||||
* 列出当前存储中的会话键。
|
||||
*
|
||||
* @return 会话键列表
|
||||
*/
|
||||
Set<String> listSessionKeys();
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.easyagents.agent.runtime.persistence;
|
||||
package com.easyagents.agent.runtime.persistence.session;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.easyagents.agent.runtime.persistence.session.memory;
|
||||
|
||||
import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
|
||||
import io.agentscope.core.state.State;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 用于测试和单节点 MVP 的内存会话存储。
|
||||
*/
|
||||
public class InMemoryAgentSessionStore implements AgentSessionStore {
|
||||
|
||||
private final Map<String, Map<String, List<State>>> states = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void save(String sessionKey, String name, State state) {
|
||||
if (sessionKey == null || name == null || state == null) {
|
||||
return;
|
||||
}
|
||||
saveList(sessionKey, name, List.of(state));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveList(String sessionKey, String name, List<? extends State> states) {
|
||||
if (sessionKey == null || name == null) {
|
||||
return;
|
||||
}
|
||||
this.states.computeIfAbsent(sessionKey, key -> new ConcurrentHashMap<>())
|
||||
.put(name, states == null ? new ArrayList<>() : new ArrayList<>(states));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends State> Optional<T> get(String sessionKey, String name, Class<T> type) {
|
||||
List<T> list = getList(sessionKey, name, type);
|
||||
return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends State> List<T> getList(String sessionKey, String name, Class<T> itemType) {
|
||||
Map<String, List<State>> byName = states.getOrDefault(sessionKey, new LinkedHashMap<>());
|
||||
List<State> values = byName.getOrDefault(name, new ArrayList<>());
|
||||
if (itemType == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<T> typedValues = new ArrayList<>();
|
||||
for (State value : values) {
|
||||
if (itemType.isInstance(value)) {
|
||||
typedValues.add(itemType.cast(value));
|
||||
}
|
||||
}
|
||||
return typedValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists(String sessionKey) {
|
||||
return states.containsKey(sessionKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String sessionKey) {
|
||||
states.remove(sessionKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> listSessionKeys() {
|
||||
return new LinkedHashSet<>(states.keySet());
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.easyagents.agent.runtime.persistence.noop;
|
||||
package com.easyagents.agent.runtime.persistence.session.noop;
|
||||
|
||||
import com.easyagents.agent.runtime.persistence.AgentRuntimeState;
|
||||
import com.easyagents.agent.runtime.persistence.AgentSessionStore;
|
||||
import com.easyagents.agent.runtime.persistence.session.AgentSessionStore;
|
||||
import io.agentscope.core.state.State;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -15,20 +15,20 @@ public enum NoopAgentSessionStore implements AgentSessionStore {
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public void save(String sessionKey, String name, AgentRuntimeState state) {
|
||||
public void save(String sessionKey, String name, State state) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveList(String sessionKey, String name, List<AgentRuntimeState> states) {
|
||||
public void saveList(String sessionKey, String name, List<? extends State> states) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AgentRuntimeState> get(String sessionKey, String name) {
|
||||
public <T extends State> Optional<T> get(String sessionKey, String name, Class<T> type) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentRuntimeState> getList(String sessionKey, String name) {
|
||||
public <T extends State> List<T> getList(String sessionKey, String name, Class<T> itemType) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.easyagents.agent.runtime.prompt;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentDefinition;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 组合智能体运行时使用的系统提示词。
|
||||
*/
|
||||
public final class SystemPromptComposer {
|
||||
|
||||
private static final String KNOWLEDGE_TOOL_PROTOCOL = """
|
||||
Knowledge tool protocol:
|
||||
- Knowledge tools provide grounded information for the scopes described in their tool descriptions.
|
||||
- Before answering a request involving facts, policies, procedures, entitlements, prices, product or service details, or other information within a knowledge tool's scope, call the most relevant knowledge tool first.
|
||||
- Do not rely only on general model knowledge for claims that fall within an available knowledge tool's scope.
|
||||
- If it is uncertain whether the request falls within a knowledge tool's scope, prefer making one retrieval call.
|
||||
- Build a concise, standalone query from the current request and only the necessary conversation context. Resolve pronouns and omitted subjects, and preserve relevant names, time, location, product, membership level, constraints, and user intent. Do not copy the entire conversation.
|
||||
- If the returned results are empty or do not address the request, reformulate the query and retry once when a materially different query is possible. Do not repeat the same query or retrieve indefinitely.
|
||||
- Base knowledge-backed claims only on information actually returned by the tools. Never imply that a knowledge base contains information that was not returned.
|
||||
- If retrieval remains insufficient, follow the Agent's configured system prompt for the response strategy.
|
||||
- Skip retrieval for greetings, casual conversation, pure writing or translation, and tasks that clearly do not depend on knowledge-base facts.
|
||||
""".strip();
|
||||
|
||||
private static final String ASYNC_TOOL_PROTOCOL = """
|
||||
Async tool protocol:
|
||||
- Async tools may expose submit, observe, result, cancel, and list sub-tools. Treat these sub-tools as one user-facing tool.
|
||||
- Do not ask the user to choose submit, observe, result, cancel, or list. These are internal execution phases.
|
||||
- For a normal user request to use an async tool, call its submit sub-tool first with the user-provided arguments by default.
|
||||
- After submit returns task_id, immediately call observe with that task_id to check progress.
|
||||
- If the task is completed and result is available, use the returned result to answer the user.
|
||||
- If the task is still running after observation, tell the user that the task is running and keep task_id/next_action for later tool calls.
|
||||
- Use result, list, or cancel directly only when the user explicitly asks to get a known task result, list tasks, or cancel a task.
|
||||
""".strip();
|
||||
|
||||
/**
|
||||
* 阻止工具类被实例化。
|
||||
*/
|
||||
private SystemPromptComposer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按固定顺序组合用户提示词与运行时工具协议。
|
||||
*
|
||||
* @param definition 智能体定义
|
||||
* @return 最终系统提示词;没有任何提示词时返回原始空值
|
||||
*/
|
||||
public static String compose(AgentDefinition definition) {
|
||||
if (definition == null) {
|
||||
return null;
|
||||
}
|
||||
boolean knowledgeProtocolEnabled = hasEnabledKnowledgeTool(definition);
|
||||
boolean asyncProtocolEnabled = hasAsyncTool(definition);
|
||||
String userPrompt = definition.getSystemPrompt();
|
||||
if (!knowledgeProtocolEnabled && !asyncProtocolEnabled) {
|
||||
return userPrompt;
|
||||
}
|
||||
|
||||
List<String> promptSections = new ArrayList<>(3);
|
||||
if (userPrompt != null && !userPrompt.isBlank()) {
|
||||
promptSections.add(userPrompt.stripTrailing());
|
||||
}
|
||||
if (knowledgeProtocolEnabled) {
|
||||
promptSections.add(KNOWLEDGE_TOOL_PROTOCOL);
|
||||
}
|
||||
if (asyncProtocolEnabled) {
|
||||
promptSections.add(ASYNC_TOOL_PROTOCOL);
|
||||
}
|
||||
return String.join("\n\n", promptSections);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前定义是否会注册可调用的知识库工具。
|
||||
*
|
||||
* @param definition 智能体定义
|
||||
* @return 知识库工具可用时返回 true
|
||||
*/
|
||||
private static boolean hasEnabledKnowledgeTool(AgentDefinition definition) {
|
||||
return definition.getExecutionOptions() != null
|
||||
&& definition.getExecutionOptions().isToolCallingEnabled()
|
||||
&& definition.getKnowledgeSpecs() != null
|
||||
&& !definition.getKnowledgeSpecs().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前定义是否包含异步工具。
|
||||
*
|
||||
* @param definition 智能体定义
|
||||
* @return 包含异步工具时返回 true
|
||||
*/
|
||||
private static boolean hasAsyncTool(AgentDefinition definition) {
|
||||
if (definition.getToolSpecs() == null) {
|
||||
return false;
|
||||
}
|
||||
for (AgentToolSpec toolSpec : definition.getToolSpecs()) {
|
||||
// AsyncToolSpecExpander 会为生成的全部子工具写入该运行时元数据。
|
||||
if (toolSpec != null && Boolean.TRUE.equals(toolSpec.getMetadata().get("asyncTool"))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ public class AgentSkillBinding {
|
||||
|
||||
private final String skillId;
|
||||
private final String skillName;
|
||||
private final String skillDisplayName;
|
||||
private final String skillBoxId;
|
||||
|
||||
/**
|
||||
@@ -17,8 +18,26 @@ public class AgentSkillBinding {
|
||||
* @param skillBoxId SkillBox ID
|
||||
*/
|
||||
public AgentSkillBinding(String skillId, String skillName, String skillBoxId) {
|
||||
this(skillId, skillName, skillName, skillBoxId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带展示名称的 Skill 绑定关系。
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param skillName Skill 规范名称
|
||||
* @param skillDisplayName Skill 展示名称
|
||||
* @param skillBoxId SkillBox ID
|
||||
*/
|
||||
public AgentSkillBinding(String skillId,
|
||||
String skillName,
|
||||
String skillDisplayName,
|
||||
String skillBoxId) {
|
||||
this.skillId = skillId;
|
||||
this.skillName = skillName;
|
||||
this.skillDisplayName = skillDisplayName == null || skillDisplayName.isBlank()
|
||||
? skillName
|
||||
: skillDisplayName;
|
||||
this.skillBoxId = skillBoxId;
|
||||
}
|
||||
|
||||
@@ -40,6 +59,15 @@ public class AgentSkillBinding {
|
||||
return skillName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Skill 展示名称。
|
||||
*
|
||||
* @return Skill 展示名称
|
||||
*/
|
||||
public String getSkillDisplayName() {
|
||||
return skillDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 SkillBox ID。
|
||||
*
|
||||
@@ -49,4 +77,3 @@ public class AgentSkillBinding {
|
||||
return skillBoxId;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ public class AgentSkillLoadCall {
|
||||
private final String toolCallId;
|
||||
private final String skillId;
|
||||
private final String skillName;
|
||||
private final String skillDisplayName;
|
||||
private final String skillBoxId;
|
||||
private final String path;
|
||||
private final Map<String, Object> input;
|
||||
@@ -31,9 +32,33 @@ public class AgentSkillLoadCall {
|
||||
String skillBoxId,
|
||||
String path,
|
||||
Map<String, Object> input) {
|
||||
this(toolCallId, skillId, skillName, skillName, skillBoxId, path, input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带展示名称的 Skill 加载工具调用记录。
|
||||
*
|
||||
* @param toolCallId 工具调用 ID
|
||||
* @param skillId Skill ID
|
||||
* @param skillName Skill 规范名称
|
||||
* @param skillDisplayName Skill 展示名称
|
||||
* @param skillBoxId SkillBox ID
|
||||
* @param path 资源路径
|
||||
* @param input 工具输入
|
||||
*/
|
||||
public AgentSkillLoadCall(String toolCallId,
|
||||
String skillId,
|
||||
String skillName,
|
||||
String skillDisplayName,
|
||||
String skillBoxId,
|
||||
String path,
|
||||
Map<String, Object> input) {
|
||||
this.toolCallId = toolCallId;
|
||||
this.skillId = skillId;
|
||||
this.skillName = skillName;
|
||||
this.skillDisplayName = skillDisplayName == null || skillDisplayName.isBlank()
|
||||
? skillName
|
||||
: skillDisplayName;
|
||||
this.skillBoxId = skillBoxId;
|
||||
this.path = path;
|
||||
this.input = input == null ? new LinkedHashMap<>() : new LinkedHashMap<>(input);
|
||||
@@ -66,6 +91,15 @@ public class AgentSkillLoadCall {
|
||||
return skillName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Skill 展示名称。
|
||||
*
|
||||
* @return Skill 展示名称
|
||||
*/
|
||||
public String getSkillDisplayName() {
|
||||
return skillDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 SkillBox ID。
|
||||
*
|
||||
|
||||
@@ -43,7 +43,8 @@ public class AgentSkillRuntimeContext {
|
||||
continue;
|
||||
}
|
||||
skillBindings.put(skillSpec.getSkillId(),
|
||||
new AgentSkillBinding(skillSpec.getSkillId(), skillSpec.getName(), spec.getSkillBoxId()));
|
||||
new AgentSkillBinding(skillSpec.getSkillId(), skillSpec.getName(),
|
||||
displayName(skillSpec), spec.getSkillBoxId()));
|
||||
}
|
||||
Map<String, AgentSkillBinding> toolBindings = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<String>> entry : spec.getToolBindings().entrySet()) {
|
||||
@@ -111,6 +112,26 @@ public class AgentSkillRuntimeContext {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 AgentScope 的真实 Skill 激活状态同步本地旁路上下文。
|
||||
*
|
||||
* <p>该状态只用于 Easy-Agents 判断旁路展示事件归属,不参与 AgentScope
|
||||
* memory/session,也不决定工具是否真的可被模型调用。</p>
|
||||
*
|
||||
* @param skillId Skill ID
|
||||
* @param active 是否激活
|
||||
*/
|
||||
public void syncSkillActive(String skillId, boolean active) {
|
||||
if (skillId == null || skillId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
if (active) {
|
||||
activeSkills.put(skillId, true);
|
||||
return;
|
||||
}
|
||||
activeSkills.remove(skillId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 Skill 是否已激活。
|
||||
*
|
||||
@@ -151,6 +172,7 @@ public class AgentSkillRuntimeContext {
|
||||
AgentSkillBinding binding = getSkillBinding(skillId);
|
||||
AgentSkillLoadCall call = new AgentSkillLoadCall(toolCallId, skillId,
|
||||
binding == null ? null : binding.getSkillName(),
|
||||
binding == null ? null : binding.getSkillDisplayName(),
|
||||
binding == null ? null : binding.getSkillBoxId(), path, input);
|
||||
pendingLoadCalls.put(toolCallId, call);
|
||||
return call;
|
||||
@@ -186,4 +208,11 @@ public class AgentSkillRuntimeContext {
|
||||
private static String stringValue(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private static String displayName(AgentSkillSpec skillSpec) {
|
||||
Object value = skillSpec.getMetadata() == null ? null : skillSpec.getMetadata().get("displayName");
|
||||
return value == null || String.valueOf(value).isBlank()
|
||||
? skillSpec.getName()
|
||||
: String.valueOf(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.easyagents.agent.runtime.tool;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentRuntimeContext;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 传递给动态工具调用的上下文。
|
||||
@@ -17,6 +19,7 @@ public class AgentToolContext {
|
||||
private String toolCallId;
|
||||
private AgentRuntimeContext runtimeContext = new AgentRuntimeContext();
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
private Consumer<AgentRuntimeEvent> eventEmitter;
|
||||
|
||||
/**
|
||||
* 获取请求ID。
|
||||
@@ -143,4 +146,36 @@ public class AgentToolContext {
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发射运行时旁路事件。
|
||||
*
|
||||
* <p>该方法主要供 runtime 包装层发射业务无关事件,例如异步工具生命周期事件。
|
||||
* 未配置事件发射器时静默忽略,避免非流式测试路径产生额外副作用。</p>
|
||||
*
|
||||
* @param event 运行时事件
|
||||
*/
|
||||
public void emitEvent(AgentRuntimeEvent event) {
|
||||
if (eventEmitter != null && event != null) {
|
||||
eventEmitter.accept(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行时事件发射器。
|
||||
*
|
||||
* @return 运行时事件发射器
|
||||
*/
|
||||
public Consumer<AgentRuntimeEvent> getEventEmitter() {
|
||||
return eventEmitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置运行时事件发射器。
|
||||
*
|
||||
* @param eventEmitter 运行时事件发射器
|
||||
*/
|
||||
public void setEventEmitter(Consumer<AgentRuntimeEvent> eventEmitter) {
|
||||
this.eventEmitter = eventEmitter;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.easyagents.agent.runtime.tool;
|
||||
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalPolicy;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
@@ -18,6 +19,7 @@ public class AgentToolSpec {
|
||||
private AgentToolVisibility visibility = AgentToolVisibility.VISIBLE;
|
||||
private boolean approvalRequired;
|
||||
private AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
|
||||
private AgentToolApprovalPolicy approvalPolicy;
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
@@ -164,6 +166,24 @@ public class AgentToolSpec {
|
||||
this.approvalRequest = approvalRequest == null ? new AgentToolApprovalRequest() : approvalRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单次调用动态审批策略。
|
||||
*
|
||||
* @return 动态审批策略;未配置时返回 null
|
||||
*/
|
||||
public AgentToolApprovalPolicy getApprovalPolicy() {
|
||||
return approvalPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置单次调用动态审批策略。
|
||||
*
|
||||
* @param approvalPolicy 动态审批策略
|
||||
*/
|
||||
public void setApprovalPolicy(AgentToolApprovalPolicy approvalPolicy) {
|
||||
this.approvalPolicy = approvalPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
*
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.easyagents.agent.runtime.tool.asynctool;
|
||||
|
||||
import com.easyagents.agent.runtime.tool.AgentToolContext;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 调用方实现的异步业务子工具集合。
|
||||
*/
|
||||
public interface AsyncSubTools {
|
||||
|
||||
/**
|
||||
* 提交异步任务。
|
||||
*
|
||||
* @param arguments 模型传入的业务参数
|
||||
* @param context 工具调用上下文
|
||||
* @return 提交结果
|
||||
*/
|
||||
AsyncToolSubmitResult submit(Map<String, Object> arguments, AgentToolContext context);
|
||||
|
||||
/**
|
||||
* 非阻塞观察任务状态和增量事件。
|
||||
*
|
||||
* @param request 观察请求
|
||||
* @param context 工具调用上下文
|
||||
* @return 当前任务视图
|
||||
*/
|
||||
AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context);
|
||||
|
||||
/**
|
||||
* 获取任务结果,未完成时返回当前观察态。
|
||||
*
|
||||
* @param request 结果请求
|
||||
* @param context 工具调用上下文
|
||||
* @return 当前任务视图
|
||||
*/
|
||||
AsyncToolTaskView result(AsyncToolResultRequest request, AgentToolContext context);
|
||||
|
||||
/**
|
||||
* 请求取消任务。
|
||||
*
|
||||
* @param request 取消请求
|
||||
* @param context 工具调用上下文
|
||||
* @return 取消结果
|
||||
*/
|
||||
AsyncToolCancelResult cancel(AsyncToolCancelRequest request, AgentToolContext context);
|
||||
|
||||
/**
|
||||
* 查询当前上下文可见的任务列表。
|
||||
*
|
||||
* @param request 列表请求
|
||||
* @param context 工具调用上下文
|
||||
* @return 任务列表
|
||||
*/
|
||||
AsyncToolTaskListResult list(AsyncToolListRequest request, AgentToolContext context);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.easyagents.agent.runtime.tool.asynctool;
|
||||
|
||||
/**
|
||||
* 异步工具取消请求。
|
||||
*/
|
||||
public class AsyncToolCancelRequest {
|
||||
|
||||
private String taskId;
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 创建空取消请求。
|
||||
*/
|
||||
public AsyncToolCancelRequest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务 ID。
|
||||
*
|
||||
* @return 任务 ID
|
||||
*/
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务 ID。
|
||||
*
|
||||
* @param taskId 任务 ID
|
||||
*/
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取取消原因。
|
||||
*
|
||||
* @return 取消原因
|
||||
*/
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置取消原因。
|
||||
*
|
||||
* @param reason 取消原因
|
||||
*/
|
||||
public void setReason(String reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.easyagents.agent.runtime.tool.asynctool;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 异步工具取消结果。
|
||||
*/
|
||||
public class AsyncToolCancelResult {
|
||||
|
||||
private String taskId;
|
||||
private AsyncToolTaskStatus status;
|
||||
private String message;
|
||||
private String errorMessage;
|
||||
private Map<String, Object> payload = new LinkedHashMap<>();
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 创建空取消结果。
|
||||
*/
|
||||
public AsyncToolCancelResult() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务 ID。
|
||||
*
|
||||
* @return 任务 ID
|
||||
*/
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务 ID。
|
||||
*
|
||||
* @param taskId 任务 ID
|
||||
*/
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务状态。
|
||||
*
|
||||
* @return 任务状态
|
||||
*/
|
||||
public AsyncToolTaskStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务状态。
|
||||
*
|
||||
* @param status 任务状态
|
||||
*/
|
||||
public void setStatus(AsyncToolTaskStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取取消消息。
|
||||
*
|
||||
* @return 取消消息
|
||||
*/
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置取消消息。
|
||||
*
|
||||
* @param message 取消消息
|
||||
*/
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误消息。
|
||||
*
|
||||
* @return 错误消息
|
||||
*/
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置错误消息。
|
||||
*
|
||||
* @param errorMessage 错误消息
|
||||
*/
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取业务扩展载荷。
|
||||
*
|
||||
* @return 业务扩展载荷
|
||||
*/
|
||||
public Map<String, Object> getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置业务扩展载荷。
|
||||
*
|
||||
* @param payload 业务扩展载荷
|
||||
*/
|
||||
public void setPayload(Map<String, Object> payload) {
|
||||
this.payload = payload == null ? new LinkedHashMap<>() : payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
*
|
||||
* @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,33 @@
|
||||
package com.easyagents.agent.runtime.tool.asynctool;
|
||||
|
||||
/**
|
||||
* 异步工具列表请求。
|
||||
*/
|
||||
public class AsyncToolListRequest {
|
||||
|
||||
private AsyncToolTaskStatus status;
|
||||
|
||||
/**
|
||||
* 创建空列表请求。
|
||||
*/
|
||||
public AsyncToolListRequest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态过滤条件。
|
||||
*
|
||||
* @return 状态过滤条件
|
||||
*/
|
||||
public AsyncToolTaskStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置状态过滤条件。
|
||||
*
|
||||
* @param status 状态过滤条件
|
||||
*/
|
||||
public void setStatus(AsyncToolTaskStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.easyagents.agent.runtime.tool.asynctool;
|
||||
|
||||
/**
|
||||
* 异步工具观察请求。
|
||||
*/
|
||||
public class AsyncToolObserveRequest {
|
||||
|
||||
private String taskId;
|
||||
/**
|
||||
* 调用方已读取到的事件位置,用于增量读取任务事件,避免重复返回全量日志。
|
||||
*/
|
||||
private Long cursor;
|
||||
private Integer limit;
|
||||
|
||||
/**
|
||||
* 创建空观察请求。
|
||||
*/
|
||||
public AsyncToolObserveRequest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务 ID。
|
||||
*
|
||||
* @return 任务 ID
|
||||
*/
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务 ID。
|
||||
*
|
||||
* @param taskId 任务 ID
|
||||
*/
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已读事件位置。
|
||||
*
|
||||
* @return 已读事件位置
|
||||
*/
|
||||
public Long getCursor() {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置已读事件位置。
|
||||
*
|
||||
* @param cursor 已读事件位置
|
||||
*/
|
||||
public void setCursor(Long cursor) {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件读取数量。
|
||||
*
|
||||
* @return 事件读取数量
|
||||
*/
|
||||
public Integer getLimit() {
|
||||
return limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件读取数量。
|
||||
*
|
||||
* @param limit 事件读取数量
|
||||
*/
|
||||
public void setLimit(Integer limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package com.easyagents.agent.runtime.tool.asynctool;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 异步工具 runtime 包装层的通用选项。
|
||||
*/
|
||||
public class AsyncToolOptions {
|
||||
|
||||
private Duration submitTimeout = Duration.ofSeconds(5);
|
||||
private Duration observeTimeout = Duration.ofSeconds(3);
|
||||
private Duration resultTimeout = Duration.ofSeconds(3);
|
||||
private Duration cancelTimeout = Duration.ofSeconds(3);
|
||||
private Duration listTimeout = Duration.ofSeconds(3);
|
||||
private int defaultEventLimit = 20;
|
||||
private int maxEventLimit = 100;
|
||||
private int maxModelContentLength = 1200;
|
||||
private int maxEventTextLength = 800;
|
||||
|
||||
/**
|
||||
* 创建默认异步工具选项实例。
|
||||
*/
|
||||
public AsyncToolOptions() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建默认异步工具选项。
|
||||
*
|
||||
* @return 默认选项
|
||||
*/
|
||||
public static AsyncToolOptions defaults() {
|
||||
return new AsyncToolOptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提交子工具超时时间。
|
||||
*
|
||||
* @return 超时时间
|
||||
*/
|
||||
public Duration getSubmitTimeout() {
|
||||
return submitTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置提交子工具超时时间。
|
||||
*
|
||||
* @param submitTimeout 超时时间
|
||||
*/
|
||||
public void setSubmitTimeout(Duration submitTimeout) {
|
||||
this.submitTimeout = submitTimeout == null ? Duration.ofSeconds(5) : submitTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取观察子工具超时时间。
|
||||
*
|
||||
* @return 超时时间
|
||||
*/
|
||||
public Duration getObserveTimeout() {
|
||||
return observeTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置观察子工具超时时间。
|
||||
*
|
||||
* @param observeTimeout 超时时间
|
||||
*/
|
||||
public void setObserveTimeout(Duration observeTimeout) {
|
||||
this.observeTimeout = observeTimeout == null ? Duration.ofSeconds(3) : observeTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取结果子工具超时时间。
|
||||
*
|
||||
* @return 超时时间
|
||||
*/
|
||||
public Duration getResultTimeout() {
|
||||
return resultTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置结果子工具超时时间。
|
||||
*
|
||||
* @param resultTimeout 超时时间
|
||||
*/
|
||||
public void setResultTimeout(Duration resultTimeout) {
|
||||
this.resultTimeout = resultTimeout == null ? Duration.ofSeconds(3) : resultTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取取消子工具超时时间。
|
||||
*
|
||||
* @return 超时时间
|
||||
*/
|
||||
public Duration getCancelTimeout() {
|
||||
return cancelTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置取消子工具超时时间。
|
||||
*
|
||||
* @param cancelTimeout 超时时间
|
||||
*/
|
||||
public void setCancelTimeout(Duration cancelTimeout) {
|
||||
this.cancelTimeout = cancelTimeout == null ? Duration.ofSeconds(3) : cancelTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表子工具超时时间。
|
||||
*
|
||||
* @return 超时时间
|
||||
*/
|
||||
public Duration getListTimeout() {
|
||||
return listTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置列表子工具超时时间。
|
||||
*
|
||||
* @param listTimeout 超时时间
|
||||
*/
|
||||
public void setListTimeout(Duration listTimeout) {
|
||||
this.listTimeout = listTimeout == null ? Duration.ofSeconds(3) : listTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认事件读取数量。
|
||||
*
|
||||
* @return 默认事件数量
|
||||
*/
|
||||
public int getDefaultEventLimit() {
|
||||
return defaultEventLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认事件读取数量。
|
||||
*
|
||||
* @param defaultEventLimit 默认事件数量
|
||||
*/
|
||||
public void setDefaultEventLimit(int defaultEventLimit) {
|
||||
this.defaultEventLimit = defaultEventLimit <= 0 ? 20 : defaultEventLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最大事件读取数量。
|
||||
*
|
||||
* @return 最大事件数量
|
||||
*/
|
||||
public int getMaxEventLimit() {
|
||||
return maxEventLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最大事件读取数量。
|
||||
*
|
||||
* @param maxEventLimit 最大事件数量
|
||||
*/
|
||||
public void setMaxEventLimit(int maxEventLimit) {
|
||||
this.maxEventLimit = maxEventLimit <= 0 ? 100 : maxEventLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型可见内容最大长度。
|
||||
*
|
||||
* @return 最大长度
|
||||
*/
|
||||
public int getMaxModelContentLength() {
|
||||
return maxModelContentLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置模型可见内容最大长度。
|
||||
*
|
||||
* @param maxModelContentLength 最大长度
|
||||
*/
|
||||
public void setMaxModelContentLength(int maxModelContentLength) {
|
||||
this.maxModelContentLength = maxModelContentLength <= 0 ? 1200 : maxModelContentLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件文本最大长度。
|
||||
*
|
||||
* @return 最大长度
|
||||
*/
|
||||
public int getMaxEventTextLength() {
|
||||
return maxEventTextLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件文本最大长度。
|
||||
*
|
||||
* @param maxEventTextLength 最大长度
|
||||
*/
|
||||
public void setMaxEventTextLength(int maxEventTextLength) {
|
||||
this.maxEventTextLength = maxEventTextLength <= 0 ? 800 : maxEventTextLength;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.easyagents.agent.runtime.tool.asynctool;
|
||||
|
||||
/**
|
||||
* 异步工具结果请求。
|
||||
*/
|
||||
public class AsyncToolResultRequest {
|
||||
|
||||
private String taskId;
|
||||
/**
|
||||
* 调用方已读取到的事件位置,用于增量读取任务事件,避免 result 重复返回全量日志。
|
||||
*/
|
||||
private Long cursor;
|
||||
private Integer limit;
|
||||
|
||||
/**
|
||||
* 创建空结果请求。
|
||||
*/
|
||||
public AsyncToolResultRequest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务 ID。
|
||||
*
|
||||
* @return 任务 ID
|
||||
*/
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务 ID。
|
||||
*
|
||||
* @param taskId 任务 ID
|
||||
*/
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已读事件位置。
|
||||
*
|
||||
* @return 已读事件位置
|
||||
*/
|
||||
public Long getCursor() {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置已读事件位置。
|
||||
*
|
||||
* @param cursor 已读事件位置
|
||||
*/
|
||||
public void setCursor(Long cursor) {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件读取数量。
|
||||
*
|
||||
* @return 事件读取数量
|
||||
*/
|
||||
public Integer getLimit() {
|
||||
return limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件读取数量。
|
||||
*
|
||||
* @param limit 事件读取数量
|
||||
*/
|
||||
public void setLimit(Integer limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.easyagents.agent.runtime.tool.asynctool;
|
||||
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 异步工具声明。
|
||||
*
|
||||
* <p>一个声明会被 runtime 展开为 submit、observe、result、cancel 和 list 五个普通工具。</p>
|
||||
*/
|
||||
public class AsyncToolSpec {
|
||||
|
||||
private String name;
|
||||
private String description;
|
||||
private Map<String, Object> submitParametersSchema = new LinkedHashMap<>();
|
||||
private AsyncSubTools subTools;
|
||||
private AsyncToolOptions options = AsyncToolOptions.defaults();
|
||||
private boolean approvalRequired;
|
||||
private AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 创建空异步工具声明。
|
||||
*/
|
||||
public AsyncToolSpec() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异步工具基础名称。
|
||||
*
|
||||
* @return 工具名称
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置异步工具基础名称。
|
||||
*
|
||||
* @param name 工具名称
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具描述。
|
||||
*
|
||||
* @return 工具描述
|
||||
*/
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工具描述。
|
||||
*
|
||||
* @param description 工具描述
|
||||
*/
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提交子工具参数 Schema。
|
||||
*
|
||||
* @return 参数 Schema
|
||||
*/
|
||||
public Map<String, Object> getSubmitParametersSchema() {
|
||||
return submitParametersSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置提交子工具参数 Schema。
|
||||
*
|
||||
* @param submitParametersSchema 参数 Schema
|
||||
*/
|
||||
public void setSubmitParametersSchema(Map<String, Object> submitParametersSchema) {
|
||||
this.submitParametersSchema = submitParametersSchema == null ? new LinkedHashMap<>() : submitParametersSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取业务子工具实现。
|
||||
*
|
||||
* @return 子工具实现
|
||||
*/
|
||||
public AsyncSubTools getSubTools() {
|
||||
return subTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置业务子工具实现。
|
||||
*
|
||||
* @param subTools 子工具实现
|
||||
*/
|
||||
public void setSubTools(AsyncSubTools subTools) {
|
||||
this.subTools = subTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异步工具选项。
|
||||
*
|
||||
* @return 工具选项
|
||||
*/
|
||||
public AsyncToolOptions getOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置异步工具选项。
|
||||
*
|
||||
* @param options 工具选项
|
||||
*/
|
||||
public void setOptions(AsyncToolOptions options) {
|
||||
this.options = options == null ? AsyncToolOptions.defaults() : options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回提交子工具是否需要人工审批。
|
||||
*
|
||||
* @return 需要审批时为 true
|
||||
*/
|
||||
public boolean isApprovalRequired() {
|
||||
return approvalRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置提交子工具是否需要人工审批。
|
||||
*
|
||||
* @param approvalRequired 审批标记
|
||||
*/
|
||||
public void setApprovalRequired(boolean approvalRequired) {
|
||||
this.approvalRequired = approvalRequired;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提交子工具的审批请求配置。
|
||||
*
|
||||
* @return 审批请求配置
|
||||
*/
|
||||
public AgentToolApprovalRequest getApprovalRequest() {
|
||||
return approvalRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置提交子工具的审批请求配置。
|
||||
*
|
||||
* @param approvalRequest 审批请求配置
|
||||
*/
|
||||
public void setApprovalRequest(AgentToolApprovalRequest approvalRequest) {
|
||||
this.approvalRequest = approvalRequest == null ? new AgentToolApprovalRequest() : approvalRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
*
|
||||
* @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,691 @@
|
||||
package com.easyagents.agent.runtime.tool.asynctool;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolCategory;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolContext;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolInvoker;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolResult;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolVisibility;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 异步工具声明展开器。
|
||||
*
|
||||
* <p>该类将一个业务无关的 {@link AsyncToolSpec} 展开为五个普通
|
||||
* {@link AgentToolSpec} 与 {@link AgentToolInvoker},业务方只需要实现
|
||||
* {@link AsyncSubTools}。</p>
|
||||
*/
|
||||
public class AsyncToolSpecExpander {
|
||||
|
||||
private static final Pattern SAFE_NAME = Pattern.compile("^[a-z][a-z0-9_]*$");
|
||||
private static final String PHASE_SUBMIT = "submit";
|
||||
private static final String PHASE_OBSERVE = "observe";
|
||||
private static final String PHASE_RESULT = "result";
|
||||
private static final String PHASE_CANCEL = "cancel";
|
||||
private static final String PHASE_LIST = "list";
|
||||
private static final String ERROR_TYPE_TIMEOUT = "TIMEOUT";
|
||||
private static final String ERROR_TYPE_EXCEPTION = "EXCEPTION";
|
||||
|
||||
private final ExecutorService executor;
|
||||
|
||||
/**
|
||||
* 使用公共 ForkJoinPool 创建展开器。
|
||||
*/
|
||||
public AsyncToolSpecExpander() {
|
||||
this(ForkJoinPool.commonPool());
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定执行器创建展开器。
|
||||
*
|
||||
* @param executor 执行器
|
||||
*/
|
||||
public AsyncToolSpecExpander(Executor executor) {
|
||||
if (executor instanceof ExecutorService executorService) {
|
||||
this.executor = executorService;
|
||||
} else {
|
||||
this.executor = new DelegatingExecutorService(executor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开工具声明。
|
||||
*
|
||||
* @param spec 异步工具声明
|
||||
* @return 五个普通工具声明
|
||||
*/
|
||||
public List<AgentToolSpec> expandSpecs(AsyncToolSpec spec) {
|
||||
AsyncToolSpec safeSpec = validate(spec);
|
||||
List<AgentToolSpec> specs = new ArrayList<>(5);
|
||||
specs.add(toolSpec(safeSpec, PHASE_SUBMIT, safeSpec.getSubmitParametersSchema(), submitOutputSchema()));
|
||||
specs.add(toolSpec(safeSpec, PHASE_OBSERVE, observeSchema(safeSpec), taskViewOutputSchema()));
|
||||
specs.add(toolSpec(safeSpec, PHASE_RESULT, observeSchema(safeSpec), taskViewOutputSchema()));
|
||||
specs.add(toolSpec(safeSpec, PHASE_CANCEL, cancelSchema(), cancelOutputSchema()));
|
||||
specs.add(toolSpec(safeSpec, PHASE_LIST, listSchema(), listOutputSchema()));
|
||||
return specs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开工具调用器。
|
||||
*
|
||||
* @param spec 异步工具声明
|
||||
* @return 按工具名索引的调用器
|
||||
*/
|
||||
public Map<String, AgentToolInvoker> expandInvokers(AsyncToolSpec spec) {
|
||||
AsyncToolSpec safeSpec = validate(spec);
|
||||
Map<String, AgentToolInvoker> invokers = new LinkedHashMap<>();
|
||||
invokers.put(toolName(safeSpec, PHASE_SUBMIT), (arguments, context) -> submit(safeSpec, arguments, context));
|
||||
invokers.put(toolName(safeSpec, PHASE_OBSERVE), (arguments, context) -> observe(safeSpec, arguments, context));
|
||||
invokers.put(toolName(safeSpec, PHASE_RESULT), (arguments, context) -> result(safeSpec, arguments, context));
|
||||
invokers.put(toolName(safeSpec, PHASE_CANCEL), (arguments, context) -> cancel(safeSpec, arguments, context));
|
||||
invokers.put(toolName(safeSpec, PHASE_LIST), (arguments, context) -> list(safeSpec, arguments, context));
|
||||
return invokers;
|
||||
}
|
||||
|
||||
private AsyncToolSpec validate(AsyncToolSpec spec) {
|
||||
if (spec == null) {
|
||||
throw new AgentRuntimeException("Async tool spec is required.");
|
||||
}
|
||||
if (spec.getName() == null || spec.getName().isBlank()) {
|
||||
throw new AgentRuntimeException("Async tool name is required.");
|
||||
}
|
||||
if (!SAFE_NAME.matcher(spec.getName()).matches()) {
|
||||
throw new AgentRuntimeException("Async tool name must be safe snake_case: " + spec.getName());
|
||||
}
|
||||
if (spec.getSubTools() == null) {
|
||||
throw new AgentRuntimeException("Async sub tools are required: " + spec.getName());
|
||||
}
|
||||
if (spec.getSubmitParametersSchema() == null || spec.getSubmitParametersSchema().isEmpty()) {
|
||||
spec.setSubmitParametersSchema(emptyObjectSchema());
|
||||
}
|
||||
if (spec.getOptions() == null) {
|
||||
spec.setOptions(AsyncToolOptions.defaults());
|
||||
}
|
||||
return spec;
|
||||
}
|
||||
|
||||
private AgentToolSpec toolSpec(AsyncToolSpec spec,
|
||||
String phase,
|
||||
Map<String, Object> parametersSchema,
|
||||
Map<String, Object> outputSchema) {
|
||||
AgentToolSpec toolSpec = new AgentToolSpec();
|
||||
toolSpec.setName(toolName(spec, phase));
|
||||
toolSpec.setDescription(description(spec, phase));
|
||||
toolSpec.setCategory(AgentToolCategory.CUSTOM);
|
||||
toolSpec.setVisibility(AgentToolVisibility.VISIBLE);
|
||||
toolSpec.setParametersSchema(parametersSchema);
|
||||
toolSpec.setOutputSchema(outputSchema);
|
||||
toolSpec.setApprovalRequired(PHASE_SUBMIT.equals(phase) && spec.isApprovalRequired());
|
||||
toolSpec.setApprovalRequest(spec.getApprovalRequest());
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
metadata.putAll(spec.getMetadata());
|
||||
metadata.put("asyncTool", true);
|
||||
metadata.put("asyncToolName", spec.getName());
|
||||
metadata.put("asyncToolPhase", phase);
|
||||
toolSpec.setMetadata(metadata);
|
||||
return toolSpec;
|
||||
}
|
||||
|
||||
private String description(AsyncToolSpec spec, String phase) {
|
||||
String prefix = spec.getDescription() == null || spec.getDescription().isBlank()
|
||||
? "Async tool " + spec.getName()
|
||||
: spec.getDescription();
|
||||
return switch (phase) {
|
||||
case PHASE_SUBMIT -> prefix
|
||||
+ " This is the default entry point when the user asks to run this tool. Submit an asynchronous task with the normal tool arguments and return task_id.";
|
||||
case PHASE_OBSERVE -> prefix
|
||||
+ " Use immediately after submit with the returned task_id to check progress and incremental events. Do not ask the user for task_id immediately after submit.";
|
||||
case PHASE_RESULT -> prefix
|
||||
+ " Use only when a known task_id should return the final result, or the current observation if the task is still running.";
|
||||
case PHASE_CANCEL -> prefix
|
||||
+ " Use only when the user explicitly asks to cancel a known asynchronous task by task_id.";
|
||||
case PHASE_LIST -> prefix
|
||||
+ " Use only when the user explicitly asks to list visible asynchronous tasks in the current context.";
|
||||
default -> prefix;
|
||||
};
|
||||
}
|
||||
|
||||
private AgentToolResult submit(AsyncToolSpec spec, Map<String, Object> arguments, AgentToolContext context) {
|
||||
return execute(spec, PHASE_SUBMIT, context, spec.getOptions().getSubmitTimeout(),
|
||||
guardedContext -> {
|
||||
AsyncToolSubmitResult result = spec.getSubTools().submit(safeMap(arguments), guardedContext);
|
||||
return wrapSubmit(spec, result, guardedContext);
|
||||
});
|
||||
}
|
||||
|
||||
private AgentToolResult observe(AsyncToolSpec spec, Map<String, Object> arguments, AgentToolContext context) {
|
||||
return execute(spec, PHASE_OBSERVE, context, spec.getOptions().getObserveTimeout(),
|
||||
guardedContext -> {
|
||||
AsyncToolObserveRequest request = observeRequest(arguments, spec.getOptions());
|
||||
return wrapTaskView(spec, PHASE_OBSERVE,
|
||||
spec.getSubTools().observe(request, guardedContext), guardedContext);
|
||||
});
|
||||
}
|
||||
|
||||
private AgentToolResult result(AsyncToolSpec spec, Map<String, Object> arguments, AgentToolContext context) {
|
||||
return execute(spec, PHASE_RESULT, context, spec.getOptions().getResultTimeout(),
|
||||
guardedContext -> {
|
||||
AsyncToolResultRequest request = resultRequest(arguments, spec.getOptions());
|
||||
return wrapTaskView(spec, PHASE_RESULT,
|
||||
spec.getSubTools().result(request, guardedContext), guardedContext);
|
||||
});
|
||||
}
|
||||
|
||||
private AgentToolResult cancel(AsyncToolSpec spec, Map<String, Object> arguments, AgentToolContext context) {
|
||||
return execute(spec, PHASE_CANCEL, context, spec.getOptions().getCancelTimeout(),
|
||||
guardedContext -> {
|
||||
AsyncToolCancelRequest request = cancelRequest(arguments);
|
||||
return wrapCancel(spec, spec.getSubTools().cancel(request, guardedContext), guardedContext);
|
||||
});
|
||||
}
|
||||
|
||||
private AgentToolResult list(AsyncToolSpec spec, Map<String, Object> arguments, AgentToolContext context) {
|
||||
return execute(spec, PHASE_LIST, context, spec.getOptions().getListTimeout(),
|
||||
guardedContext -> {
|
||||
AsyncToolListRequest request = listRequest(arguments);
|
||||
return wrapList(spec, spec.getSubTools().list(request, guardedContext), guardedContext);
|
||||
});
|
||||
}
|
||||
|
||||
private AgentToolResult execute(AsyncToolSpec spec,
|
||||
String phase,
|
||||
AgentToolContext context,
|
||||
Duration timeout,
|
||||
Function<AgentToolContext, AgentToolResult> supplier) {
|
||||
AtomicBoolean active = new AtomicBoolean(true);
|
||||
AgentToolContext guardedContext = guardedContext(context, active);
|
||||
Future<AgentToolResult> future = executor.submit(() -> supplier.apply(guardedContext));
|
||||
try {
|
||||
return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException error) {
|
||||
active.set(false);
|
||||
future.cancel(true);
|
||||
AgentToolResult result = failureResult(spec, phase, null, AsyncToolTaskStatus.TIMEOUT,
|
||||
ERROR_TYPE_TIMEOUT, "Async tool " + phase + " timed out.");
|
||||
emitFailure(spec, phase, context, null, AsyncToolTaskStatus.TIMEOUT, result.getErrorMessage());
|
||||
return result;
|
||||
} catch (InterruptedException error) {
|
||||
active.set(false);
|
||||
Thread.currentThread().interrupt();
|
||||
AgentToolResult result = failureResult(spec, phase, null, AsyncToolTaskStatus.FAILED,
|
||||
ERROR_TYPE_EXCEPTION, "Async tool " + phase + " interrupted.");
|
||||
emitFailure(spec, phase, context, null, AsyncToolTaskStatus.FAILED, result.getErrorMessage());
|
||||
return result;
|
||||
} catch (ExecutionException error) {
|
||||
active.set(false);
|
||||
Throwable cause = error.getCause() == null ? error : error.getCause();
|
||||
String message = cause.getMessage() == null || cause.getMessage().isBlank()
|
||||
? "Async tool " + phase + " failed."
|
||||
: cause.getMessage();
|
||||
AgentToolResult result = failureResult(spec, phase, null, AsyncToolTaskStatus.FAILED,
|
||||
ERROR_TYPE_EXCEPTION, message);
|
||||
emitFailure(spec, phase, context, null, AsyncToolTaskStatus.FAILED, result.getErrorMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private AgentToolContext guardedContext(AgentToolContext source, AtomicBoolean active) {
|
||||
if (source == null) {
|
||||
return null;
|
||||
}
|
||||
AgentToolContext context = new AgentToolContext();
|
||||
context.setRequestId(source.getRequestId());
|
||||
context.setTraceId(source.getTraceId());
|
||||
context.setSessionId(source.getSessionId());
|
||||
context.setAgentId(source.getAgentId());
|
||||
context.setToolCallId(source.getToolCallId());
|
||||
context.setRuntimeContext(source.getRuntimeContext());
|
||||
context.setMetadata(new LinkedHashMap<>(source.getMetadata()));
|
||||
context.setEventEmitter(event -> {
|
||||
// 超时后底层业务可能仍在运行,迟到事件不能再覆盖 runtime 已返回的失败语义。
|
||||
if (active.get()) {
|
||||
source.emitEvent(event);
|
||||
}
|
||||
});
|
||||
return context;
|
||||
}
|
||||
|
||||
private AgentToolResult wrapSubmit(AsyncToolSpec spec, AsyncToolSubmitResult result, AgentToolContext context) {
|
||||
AsyncToolSubmitResult safe = result == null ? new AsyncToolSubmitResult() : result;
|
||||
AsyncToolTaskStatus status = status(safe.getStatus(), AsyncToolTaskStatus.RUNNING);
|
||||
safe.setStatus(status);
|
||||
if (safe.getTaskId() == null || safe.getTaskId().isBlank()) {
|
||||
String message = "Async tool submit must return taskId.";
|
||||
AgentToolResult toolResult = failureResult(spec, PHASE_SUBMIT, null, AsyncToolTaskStatus.FAILED,
|
||||
ERROR_TYPE_EXCEPTION, message, safe);
|
||||
emitFailure(spec, PHASE_SUBMIT, context, null, AsyncToolTaskStatus.FAILED, message);
|
||||
return toolResult;
|
||||
}
|
||||
if (safe.getNextAction() == null || safe.getNextAction().isBlank()) {
|
||||
safe.setNextAction(toolName(spec, PHASE_OBSERVE) + " 查看任务进度。");
|
||||
}
|
||||
AgentToolResult toolResult = successResult(spec, PHASE_SUBMIT, safe.getTaskId(), status,
|
||||
modelContent(safe.getTaskId(), status, safe.getNextAction(), safe.getSummary()), safe);
|
||||
emit(spec, PHASE_SUBMIT, AgentRuntimeEventType.ASYNC_TOOL_SUBMITTED, context, safe.getTaskId(), status,
|
||||
safe.getCursor(), null, safe.getSummary(), null);
|
||||
return toolResult;
|
||||
}
|
||||
|
||||
private AgentToolResult wrapTaskView(AsyncToolSpec spec,
|
||||
String phase,
|
||||
AsyncToolTaskView view,
|
||||
AgentToolContext context) {
|
||||
AsyncToolTaskView safe = view == null ? new AsyncToolTaskView() : view;
|
||||
AsyncToolTaskStatus status = status(safe.getStatus(), AsyncToolTaskStatus.RUNNING);
|
||||
safe.setStatus(status);
|
||||
if (safe.getTerminal() == null) {
|
||||
safe.setTerminal(status.isTerminal());
|
||||
}
|
||||
if (safe.getResultAvailable() == null) {
|
||||
safe.setResultAvailable(status.isSuccess() && safe.getResult() != null);
|
||||
}
|
||||
if (safe.getNextAction() == null || safe.getNextAction().isBlank()) {
|
||||
safe.setNextAction(status.isTerminal()
|
||||
? "任务已结束。"
|
||||
: toolName(spec, PHASE_OBSERVE) + " 继续查看任务进度。");
|
||||
}
|
||||
AgentToolResult toolResult = successResult(spec, phase, safe.getTaskId(), status,
|
||||
modelContent(safe.getTaskId(), status, safe.getNextAction(), safe.getSummary(),
|
||||
Boolean.TRUE.equals(safe.getResultAvailable()), safe.getResult()), safe);
|
||||
emit(spec, phase, PHASE_RESULT.equals(phase)
|
||||
? AgentRuntimeEventType.ASYNC_TOOL_RESULT
|
||||
: AgentRuntimeEventType.ASYNC_TOOL_OBSERVED,
|
||||
context, safe.getTaskId(), status, safe.getCursor(), safe.getNextCursor(), safe.getSummary(),
|
||||
safe.getErrorMessage(), safe.getResultAvailable());
|
||||
return toolResult;
|
||||
}
|
||||
|
||||
private AgentToolResult wrapCancel(AsyncToolSpec spec, AsyncToolCancelResult result, AgentToolContext context) {
|
||||
AsyncToolCancelResult safe = result == null ? new AsyncToolCancelResult() : result;
|
||||
AsyncToolTaskStatus status = status(safe.getStatus(), AsyncToolTaskStatus.CANCELLING);
|
||||
safe.setStatus(status);
|
||||
boolean success = safe.getErrorMessage() == null || safe.getErrorMessage().isBlank();
|
||||
AgentToolResult toolResult = success
|
||||
? successResult(spec, PHASE_CANCEL, safe.getTaskId(), status,
|
||||
modelContent(safe.getTaskId(), status, "继续使用 " + toolName(spec, PHASE_OBSERVE) + " 查看取消状态。",
|
||||
safe.getMessage()), safe)
|
||||
: failureResult(spec, PHASE_CANCEL, safe.getTaskId(), status, ERROR_TYPE_EXCEPTION, safe.getErrorMessage(), safe);
|
||||
emit(spec, PHASE_CANCEL, success ? AgentRuntimeEventType.ASYNC_TOOL_CANCELLED : AgentRuntimeEventType.ASYNC_TOOL_FAILED,
|
||||
context, safe.getTaskId(), status, null, null, safe.getMessage(), safe.getErrorMessage());
|
||||
return toolResult;
|
||||
}
|
||||
|
||||
private AgentToolResult wrapList(AsyncToolSpec spec, AsyncToolTaskListResult result, AgentToolContext context) {
|
||||
AsyncToolTaskListResult safe = result == null ? new AsyncToolTaskListResult() : result;
|
||||
String summary = "共 " + safe.getTasks().size() + " 个任务。";
|
||||
AgentToolResult toolResult = successResult(spec, PHASE_LIST, null, null,
|
||||
modelContent(null, null, "按 task_id 使用观察或结果工具查看详情。", summary), safe);
|
||||
emit(spec, PHASE_LIST, AgentRuntimeEventType.ASYNC_TOOL_LISTED, context, null, null, null, null, summary, null);
|
||||
return toolResult;
|
||||
}
|
||||
|
||||
private AgentToolResult successResult(AsyncToolSpec spec,
|
||||
String phase,
|
||||
String taskId,
|
||||
AsyncToolTaskStatus status,
|
||||
String modelContent,
|
||||
Object displayContent) {
|
||||
AgentToolResult result = AgentToolResult.success(truncate(modelContent, spec.getOptions().getMaxModelContentLength()));
|
||||
result.setDisplayContent(displayContent);
|
||||
result.setMetadata(metadata(spec, phase, taskId, status));
|
||||
return result;
|
||||
}
|
||||
|
||||
private AgentToolResult failureResult(AsyncToolSpec spec,
|
||||
String phase,
|
||||
String taskId,
|
||||
AsyncToolTaskStatus status,
|
||||
String errorType,
|
||||
String errorMessage) {
|
||||
return failureResult(spec, phase, taskId, status, errorType, errorMessage, null);
|
||||
}
|
||||
|
||||
private AgentToolResult failureResult(AsyncToolSpec spec,
|
||||
String phase,
|
||||
String taskId,
|
||||
AsyncToolTaskStatus status,
|
||||
String errorType,
|
||||
String errorMessage,
|
||||
Object displayContent) {
|
||||
String message = errorMessage == null || errorMessage.isBlank() ? "Async tool failed." : errorMessage;
|
||||
AgentToolResult result = AgentToolResult.failure(message);
|
||||
result.setDisplayContent(displayContent == null ? Map.of("errorType", errorType, "message", message) : displayContent);
|
||||
result.setMetadata(metadata(spec, phase, taskId, status));
|
||||
result.getMetadata().put("errorType", errorType);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void emitFailure(AsyncToolSpec spec,
|
||||
String phase,
|
||||
AgentToolContext context,
|
||||
String taskId,
|
||||
AsyncToolTaskStatus status,
|
||||
String errorMessage) {
|
||||
emit(spec, phase, AgentRuntimeEventType.ASYNC_TOOL_FAILED, context, taskId, status, null, null, null, errorMessage);
|
||||
}
|
||||
|
||||
private void emit(AsyncToolSpec spec,
|
||||
String phase,
|
||||
AgentRuntimeEventType type,
|
||||
AgentToolContext context,
|
||||
String taskId,
|
||||
AsyncToolTaskStatus status,
|
||||
Long cursor,
|
||||
Long nextCursor,
|
||||
String summary,
|
||||
String errorMessage) {
|
||||
emit(spec, phase, type, context, taskId, status, cursor, nextCursor, summary, errorMessage, null);
|
||||
}
|
||||
|
||||
private void emit(AsyncToolSpec spec,
|
||||
String phase,
|
||||
AgentRuntimeEventType type,
|
||||
AgentToolContext context,
|
||||
String taskId,
|
||||
AsyncToolTaskStatus status,
|
||||
Long cursor,
|
||||
Long nextCursor,
|
||||
String summary,
|
||||
String errorMessage,
|
||||
Boolean resultAvailable) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(type);
|
||||
event.setTraceId(context.getTraceId());
|
||||
event.setSessionId(context.getSessionId());
|
||||
event.setAgentId(context.getAgentId());
|
||||
event.setToolCallId(context.getToolCallId());
|
||||
event.getMetadata().putAll(spec.getMetadata());
|
||||
putIfNotNull(event.getMetadata(), "requestId", context.getRequestId());
|
||||
event.getPayload().put("asyncToolName", spec.getName());
|
||||
event.getPayload().put("phase", phase);
|
||||
putIfNotNull(event.getPayload(), "toolDisplayName", spec.getMetadata().get("toolDisplayName"));
|
||||
putIfNotNull(event.getPayload(), "taskId", taskId);
|
||||
putIfNotNull(event.getPayload(), "status", status == null ? null : status.name());
|
||||
putIfNotNull(event.getPayload(), "cursor", cursor);
|
||||
putIfNotNull(event.getPayload(), "nextCursor", nextCursor);
|
||||
putIfNotNull(event.getPayload(), "summary", truncate(summary, spec.getOptions().getMaxEventTextLength()));
|
||||
putIfNotNull(event.getPayload(), "errorMessage", truncate(errorMessage, spec.getOptions().getMaxEventTextLength()));
|
||||
putIfNotNull(event.getPayload(), "resultAvailable", resultAvailable);
|
||||
event.getMetadata().put("asyncTool", true);
|
||||
event.getMetadata().put("asyncToolName", spec.getName());
|
||||
event.getMetadata().put("asyncToolPhase", phase);
|
||||
context.emitEvent(event);
|
||||
}
|
||||
|
||||
private Map<String, Object> metadata(AsyncToolSpec spec, String phase, String taskId, AsyncToolTaskStatus status) {
|
||||
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
metadata.putAll(spec.getMetadata());
|
||||
metadata.put("asyncTool", true);
|
||||
metadata.put("asyncToolName", spec.getName());
|
||||
metadata.put("asyncToolPhase", phase);
|
||||
putIfNotNull(metadata, "taskId", taskId);
|
||||
putIfNotNull(metadata, "status", status == null ? null : status.name());
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private AsyncToolObserveRequest observeRequest(Map<String, Object> arguments, AsyncToolOptions options) {
|
||||
AsyncToolObserveRequest request = new AsyncToolObserveRequest();
|
||||
request.setTaskId(stringValue(arguments, "taskId"));
|
||||
request.setCursor(longValue(arguments, "cursor"));
|
||||
request.setLimit(limit(arguments, options));
|
||||
return request;
|
||||
}
|
||||
|
||||
private AsyncToolResultRequest resultRequest(Map<String, Object> arguments, AsyncToolOptions options) {
|
||||
AsyncToolResultRequest request = new AsyncToolResultRequest();
|
||||
request.setTaskId(stringValue(arguments, "taskId"));
|
||||
request.setCursor(longValue(arguments, "cursor"));
|
||||
request.setLimit(limit(arguments, options));
|
||||
return request;
|
||||
}
|
||||
|
||||
private AsyncToolCancelRequest cancelRequest(Map<String, Object> arguments) {
|
||||
AsyncToolCancelRequest request = new AsyncToolCancelRequest();
|
||||
request.setTaskId(stringValue(arguments, "taskId"));
|
||||
request.setReason(stringValue(arguments, "reason"));
|
||||
return request;
|
||||
}
|
||||
|
||||
private AsyncToolListRequest listRequest(Map<String, Object> arguments) {
|
||||
AsyncToolListRequest request = new AsyncToolListRequest();
|
||||
String status = stringValue(arguments, "status");
|
||||
if (status != null && !status.isBlank()) {
|
||||
request.setStatus(AsyncToolTaskStatus.valueOf(status.trim().toUpperCase(Locale.ROOT)));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private Integer limit(Map<String, Object> arguments, AsyncToolOptions options) {
|
||||
Integer limit = intValue(arguments, "limit");
|
||||
if (limit == null || limit <= 0) {
|
||||
return options.getDefaultEventLimit();
|
||||
}
|
||||
return Math.min(limit, options.getMaxEventLimit());
|
||||
}
|
||||
|
||||
private String modelContent(String taskId, AsyncToolTaskStatus status, String nextAction, String summary) {
|
||||
return modelContent(taskId, status, nextAction, summary, false, null);
|
||||
}
|
||||
|
||||
private String modelContent(String taskId,
|
||||
AsyncToolTaskStatus status,
|
||||
String nextAction,
|
||||
String summary,
|
||||
boolean resultAvailable,
|
||||
Object result) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (taskId != null && !taskId.isBlank()) {
|
||||
builder.append("task_id: ").append(taskId).append('\n');
|
||||
}
|
||||
if (status != null) {
|
||||
builder.append("status: ").append(status.name()).append('\n');
|
||||
}
|
||||
if (summary != null && !summary.isBlank()) {
|
||||
builder.append("summary: ").append(summary).append('\n');
|
||||
}
|
||||
if (resultAvailable) {
|
||||
builder.append("result_available: true").append('\n');
|
||||
builder.append("result: ").append(modelResult(result)).append('\n');
|
||||
}
|
||||
if (nextAction != null && !nextAction.isBlank()) {
|
||||
builder.append("next_action: ").append(nextAction);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String modelResult(Object result) {
|
||||
if (result == null) {
|
||||
return "";
|
||||
}
|
||||
if (result instanceof CharSequence
|
||||
|| result instanceof Number
|
||||
|| result instanceof Boolean
|
||||
|| result instanceof Character
|
||||
|| result instanceof Enum<?>) {
|
||||
return String.valueOf(result);
|
||||
}
|
||||
try {
|
||||
return JSON.toJSONString(result);
|
||||
} catch (Exception ignored) {
|
||||
return String.valueOf(result);
|
||||
}
|
||||
}
|
||||
|
||||
private String toolName(AsyncToolSpec spec, String phase) {
|
||||
return spec.getName() + "_" + phase;
|
||||
}
|
||||
|
||||
private AsyncToolTaskStatus status(AsyncToolTaskStatus status, AsyncToolTaskStatus defaultStatus) {
|
||||
return status == null ? defaultStatus : status;
|
||||
}
|
||||
|
||||
private Map<String, Object> emptyObjectSchema() {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("type", "object");
|
||||
schema.put("properties", new LinkedHashMap<>());
|
||||
return schema;
|
||||
}
|
||||
|
||||
private Map<String, Object> observeSchema(AsyncToolSpec spec) {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("type", "object");
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("taskId", property("string", "Task id returned by submit."));
|
||||
properties.put("cursor", property("integer", "Event cursor returned by previous observe/result."));
|
||||
properties.put("limit", property("integer", "Maximum number of incremental events."));
|
||||
schema.put("properties", properties);
|
||||
schema.put("required", List.of("taskId"));
|
||||
return schema;
|
||||
}
|
||||
|
||||
private Map<String, Object> cancelSchema() {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("type", "object");
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("taskId", property("string", "Task id returned by submit."));
|
||||
properties.put("reason", property("string", "Optional cancellation reason."));
|
||||
schema.put("properties", properties);
|
||||
schema.put("required", List.of("taskId"));
|
||||
return schema;
|
||||
}
|
||||
|
||||
private Map<String, Object> listSchema() {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("type", "object");
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
properties.put("status", property("string", "Optional task status filter."));
|
||||
schema.put("properties", properties);
|
||||
return schema;
|
||||
}
|
||||
|
||||
private Map<String, Object> submitOutputSchema() {
|
||||
return outputSchema("AsyncToolSubmitResult");
|
||||
}
|
||||
|
||||
private Map<String, Object> taskViewOutputSchema() {
|
||||
return outputSchema("AsyncToolTaskView");
|
||||
}
|
||||
|
||||
private Map<String, Object> cancelOutputSchema() {
|
||||
return outputSchema("AsyncToolCancelResult");
|
||||
}
|
||||
|
||||
private Map<String, Object> listOutputSchema() {
|
||||
return outputSchema("AsyncToolTaskListResult");
|
||||
}
|
||||
|
||||
private Map<String, Object> outputSchema(String title) {
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("type", "object");
|
||||
schema.put("title", title);
|
||||
return schema;
|
||||
}
|
||||
|
||||
private Map<String, Object> property(String type, String description) {
|
||||
Map<String, Object> property = new LinkedHashMap<>();
|
||||
property.put("type", type);
|
||||
property.put("description", description);
|
||||
return property;
|
||||
}
|
||||
|
||||
private Map<String, Object> safeMap(Map<String, Object> arguments) {
|
||||
return arguments == null ? new LinkedHashMap<>() : new LinkedHashMap<>(arguments);
|
||||
}
|
||||
|
||||
private String stringValue(Map<String, Object> arguments, String key) {
|
||||
Object value = arguments == null ? null : arguments.get(key);
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private Long longValue(Map<String, Object> arguments, String key) {
|
||||
Object value = arguments == null ? null : arguments.get(key);
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (value == null || String.valueOf(value).isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return Long.parseLong(String.valueOf(value));
|
||||
}
|
||||
|
||||
private Integer intValue(Map<String, Object> arguments, String key) {
|
||||
Object value = arguments == null ? null : arguments.get(key);
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
if (value == null || String.valueOf(value).isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return Integer.parseInt(String.valueOf(value));
|
||||
}
|
||||
|
||||
private void putIfNotNull(Map<String, Object> target, String key, Object value) {
|
||||
if (value != null) {
|
||||
target.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private String truncate(String value, int maxLength) {
|
||||
if (value == null || value.length() <= maxLength) {
|
||||
return value;
|
||||
}
|
||||
return value.substring(0, Math.max(0, maxLength)) + "...";
|
||||
}
|
||||
|
||||
private static class DelegatingExecutorService extends AbstractExecutorService {
|
||||
|
||||
private final Executor executor;
|
||||
private volatile boolean shutdown;
|
||||
|
||||
private DelegatingExecutorService(Executor executor) {
|
||||
this.executor = executor == null ? ForkJoinPool.commonPool() : executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
shutdown = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Runnable> shutdownNow() {
|
||||
shutdown = true;
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShutdown() {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitTermination(long timeout, TimeUnit unit) {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
executor.execute(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.easyagents.agent.runtime.tool.asynctool;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 异步工具提交结果。
|
||||
*/
|
||||
public class AsyncToolSubmitResult {
|
||||
|
||||
private String taskId;
|
||||
private AsyncToolTaskStatus status;
|
||||
/**
|
||||
* 提交后调用方已读取到的事件位置,后续 observe 可从该位置继续增量读取。
|
||||
*/
|
||||
private Long cursor;
|
||||
private String summary;
|
||||
private String nextAction;
|
||||
private Map<String, Object> payload = new LinkedHashMap<>();
|
||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 创建空提交结果。
|
||||
*/
|
||||
public AsyncToolSubmitResult() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务 ID。
|
||||
*
|
||||
* @return 任务 ID
|
||||
*/
|
||||
public String getTaskId() {
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务 ID。
|
||||
*
|
||||
* @param taskId 任务 ID
|
||||
*/
|
||||
public void setTaskId(String taskId) {
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务状态。
|
||||
*
|
||||
* @return 任务状态
|
||||
*/
|
||||
public AsyncToolTaskStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置任务状态。
|
||||
*
|
||||
* @param status 任务状态
|
||||
*/
|
||||
public void setStatus(AsyncToolTaskStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前事件读取位置。
|
||||
*
|
||||
* @return 当前事件读取位置
|
||||
*/
|
||||
public Long getCursor() {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前事件读取位置。
|
||||
*
|
||||
* @param cursor 当前事件读取位置
|
||||
*/
|
||||
public void setCursor(Long cursor) {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取摘要。
|
||||
*
|
||||
* @return 摘要
|
||||
*/
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置摘要。
|
||||
*
|
||||
* @param summary 摘要
|
||||
*/
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一步建议。
|
||||
*
|
||||
* @return 下一步建议
|
||||
*/
|
||||
public String getNextAction() {
|
||||
return nextAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置下一步建议。
|
||||
*
|
||||
* @param nextAction 下一步建议
|
||||
*/
|
||||
public void setNextAction(String nextAction) {
|
||||
this.nextAction = nextAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取业务扩展载荷。
|
||||
*
|
||||
* @return 业务扩展载荷
|
||||
*/
|
||||
public Map<String, Object> getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置业务扩展载荷。
|
||||
*
|
||||
* @param payload 业务扩展载荷
|
||||
*/
|
||||
public void setPayload(Map<String, Object> payload) {
|
||||
this.payload = payload == null ? new LinkedHashMap<>() : payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元数据。
|
||||
*
|
||||
* @return 元数据
|
||||
*/
|
||||
public Map<String, Object> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置元数据。
|
||||
*
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public void setMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user