Compare commits
72 Commits
735a840945
...
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 | |||
| 8356560c26 | |||
| 787ca328f9 | |||
| e16f1f00dc | |||
| 8b34b4ec40 | |||
| 56ee149e7c | |||
| b66876d0fd | |||
| 547d4f6ee0 | |||
| 0c7b362173 | |||
| aa3e90b990 | |||
| 090eca5df5 | |||
| f57544daa2 | |||
| 941995d1b8 | |||
| 2f20064ee1 |
53
README.md
53
README.md
@@ -10,11 +10,14 @@ Easy-Agents 是一个轻量、可扩展的 Java AI 应用开发框架,覆盖
|
|||||||
- MCP 客户端能力(调用、拦截、缓存与管理)
|
- MCP 客户端能力(调用、拦截、缓存与管理)
|
||||||
- 文档读取与切分、向量存储与检索
|
- 文档读取与切分、向量存储与检索
|
||||||
- 工作流执行引擎(Flow)与 Easy-Agents 适配支持
|
- 工作流执行引擎(Flow)与 Easy-Agents 适配支持
|
||||||
|
- 基于 Calcite 的 SQL 编译、方言适配与流式 JDBC 查询
|
||||||
|
- 基于 Quartz JDBC JobStore 的嵌入式分布式定时调度
|
||||||
|
|
||||||
## 模块说明
|
## 模块说明
|
||||||
|
|
||||||
- `easy-agents-bom`:依赖版本管理(BOM)。
|
- `easy-agents-bom`:依赖版本管理(BOM)。
|
||||||
- `easy-agents-core`:核心抽象与基础能力。
|
- `easy-agents-core`:核心抽象与基础能力。
|
||||||
|
- `easy-agents-document`:统一文档解析能力域,当前提供 PDF、PPTX、XLSX 解析抽象与 MinerU 复用能力。
|
||||||
- `easy-agents-chat`:对话模型接入实现集合。
|
- `easy-agents-chat`:对话模型接入实现集合。
|
||||||
- `easy-agents-embedding`:向量化模型实现集合。
|
- `easy-agents-embedding`:向量化模型实现集合。
|
||||||
- `easy-agents-rerank`:重排模型实现集合。
|
- `easy-agents-rerank`:重排模型实现集合。
|
||||||
@@ -23,14 +26,17 @@ Easy-Agents 是一个轻量、可扩展的 Java AI 应用开发框架,覆盖
|
|||||||
- `easy-agents-search-engine`:检索引擎实现。
|
- `easy-agents-search-engine`:检索引擎实现。
|
||||||
- `easy-agents-tool`:工具调用能力。
|
- `easy-agents-tool`:工具调用能力。
|
||||||
- `easy-agents-mcp`:MCP 集成。
|
- `easy-agents-mcp`:MCP 集成。
|
||||||
|
- `easy-agents-skill`:标准 Agent Skills 包模型、安全校验、资源存储与 ZIP 双向编解码。
|
||||||
- `easy-agents-flow`:流程编排核心引擎。
|
- `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-support`:Flow 与 Easy-Agents 适配模块。
|
||||||
- `easy-agents-spring-boot-starter`:Spring Boot 自动配置支持。
|
- `easy-agents-spring-boot-starter`:Spring Boot 自动配置支持。
|
||||||
- `easy-agents-samples`:示例工程。
|
- `easy-agents-samples`:示例工程。
|
||||||
|
|
||||||
## 环境要求
|
## 环境要求
|
||||||
|
|
||||||
- JDK 8+
|
- JDK 17+
|
||||||
- Maven 3.8+
|
- Maven 3.8+
|
||||||
|
|
||||||
## 构建与安装
|
## 构建与安装
|
||||||
@@ -38,11 +44,17 @@ Easy-Agents 是一个轻量、可扩展的 Java AI 应用开发框架,覆盖
|
|||||||
在项目根目录执行:
|
在项目根目录执行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mvn -DskipTests -Dmaven.javadoc.skip=true -Dgpg.skip=true clean install
|
mvn -DskipTests clean install
|
||||||
```
|
```
|
||||||
|
|
||||||
构建完成后,相关构件会安装到本地 Maven 仓库,可供 `easyflow` 等项目直接依赖。
|
构建完成后,相关构件会安装到本地 Maven 仓库,可供 `easyflow` 等项目直接依赖。
|
||||||
|
|
||||||
|
发布时启用 `release` profile,生成源码包和 Javadoc 包,并调用 Maven Central 发布插件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn -Prelease -DskipTests deploy
|
||||||
|
```
|
||||||
|
|
||||||
## 快速示例
|
## 快速示例
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@@ -68,7 +80,9 @@ public static void main(String[] args) {
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.easyagents</groupId>
|
<groupId>com.easyagents</groupId>
|
||||||
<artifactId>easy-agents-bom</artifactId>
|
<artifactId>easy-agents-bom</artifactId>
|
||||||
<version>0.0.1</version>
|
<version>1.2.0-RC</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
@@ -85,3 +99,36 @@ public static void main(String[] args) {
|
|||||||
</dependencies>
|
</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`。
|
||||||
|
|||||||
55
easy-agents-agent-runtime/pom.xml
Normal file
55
easy-agents-agent-runtime/pom.xml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>com.easyagents</groupId>
|
||||||
|
<artifactId>easy-agents</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<name>easy-agents-agent-runtime</name>
|
||||||
|
<artifactId>easy-agents-agent-runtime</artifactId>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.agentscope</groupId>
|
||||||
|
<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>
|
||||||
|
<version>2.14.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.genai</groupId>
|
||||||
|
<artifactId>google-genai</artifactId>
|
||||||
|
<version>1.38.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
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;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主入口
|
||||||
|
* ReAct 智能体的声明式定义
|
||||||
|
*/
|
||||||
|
public class AgentDefinition {
|
||||||
|
|
||||||
|
private String agentId;
|
||||||
|
private String agentName;
|
||||||
|
private String description;
|
||||||
|
private String systemPrompt;
|
||||||
|
private AgentModelSpec modelSpec;
|
||||||
|
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();
|
||||||
|
private AgentSkillBoxSpec skillBoxSpec;
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取agent id。
|
||||||
|
*
|
||||||
|
* @return 智能体ID
|
||||||
|
*/
|
||||||
|
public String getAgentId() {
|
||||||
|
return agentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置agent id。
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
*/
|
||||||
|
public void setAgentId(String agentId) {
|
||||||
|
this.agentId = agentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取agent name。
|
||||||
|
*
|
||||||
|
* @return 智能体名称
|
||||||
|
*/
|
||||||
|
public String getAgentName() {
|
||||||
|
return agentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置agent name。
|
||||||
|
*
|
||||||
|
* @param agentName 智能体名称
|
||||||
|
*/
|
||||||
|
public void setAgentName(String agentName) {
|
||||||
|
this.agentName = agentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取agent description。
|
||||||
|
*
|
||||||
|
* @return 智能体描述
|
||||||
|
*/
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置agent description。
|
||||||
|
*
|
||||||
|
* @param description 智能体描述
|
||||||
|
*/
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取system prompt。
|
||||||
|
*
|
||||||
|
* @return 系统提示词
|
||||||
|
*/
|
||||||
|
public String getSystemPrompt() {
|
||||||
|
return systemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置system prompt。
|
||||||
|
*
|
||||||
|
* @param systemPrompt 系统提示词
|
||||||
|
*/
|
||||||
|
public void setSystemPrompt(String systemPrompt) {
|
||||||
|
this.systemPrompt = systemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型设置。
|
||||||
|
*
|
||||||
|
* @return 模型设置
|
||||||
|
*/
|
||||||
|
public AgentModelSpec getModelSpec() {
|
||||||
|
return modelSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置模型设置。
|
||||||
|
*
|
||||||
|
* @param modelSpec 模型设置
|
||||||
|
*/
|
||||||
|
public void setModelSpec(AgentModelSpec modelSpec) {
|
||||||
|
this.modelSpec = modelSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取生成参数。
|
||||||
|
*
|
||||||
|
* @return 生成参数
|
||||||
|
*/
|
||||||
|
public AgentGenerationOptions getGenerationOptions() {
|
||||||
|
return generationOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置生成参数。
|
||||||
|
*
|
||||||
|
* @param generationOptions 生成参数
|
||||||
|
*/
|
||||||
|
public void setGenerationOptions(AgentGenerationOptions generationOptions) {
|
||||||
|
this.generationOptions = generationOptions == null ? new AgentGenerationOptions() : generationOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取执行参数。
|
||||||
|
*
|
||||||
|
* @return 执行参数
|
||||||
|
*/
|
||||||
|
public AgentExecutionOptions getExecutionOptions() {
|
||||||
|
return executionOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置执行参数。
|
||||||
|
*
|
||||||
|
* @param executionOptions 执行参数
|
||||||
|
*/
|
||||||
|
public void setExecutionOptions(AgentExecutionOptions executionOptions) {
|
||||||
|
this.executionOptions = executionOptions == null ? new AgentExecutionOptions() : executionOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具定义。
|
||||||
|
*
|
||||||
|
* @return 工具定义
|
||||||
|
*/
|
||||||
|
public List<AgentToolSpec> getToolSpecs() {
|
||||||
|
return toolSpecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置工具定义。
|
||||||
|
*
|
||||||
|
* @param toolSpecs 工具定义
|
||||||
|
*/
|
||||||
|
public void setToolSpecs(List<AgentToolSpec> toolSpecs) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库定义。
|
||||||
|
*
|
||||||
|
* @return 知识库定义
|
||||||
|
*/
|
||||||
|
public List<AgentKnowledgeSpec> getKnowledgeSpecs() {
|
||||||
|
return knowledgeSpecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置知识库定义。
|
||||||
|
*
|
||||||
|
* @param knowledgeSpecs 知识库定义
|
||||||
|
*/
|
||||||
|
public void setKnowledgeSpecs(List<AgentKnowledgeSpec> knowledgeSpecs) {
|
||||||
|
this.knowledgeSpecs = knowledgeSpecs == null ? new ArrayList<>() : new ArrayList<>(knowledgeSpecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取记忆策略。
|
||||||
|
*
|
||||||
|
* @return 记忆策略
|
||||||
|
*/
|
||||||
|
public AgentMemoryPolicy getMemoryPolicy() {
|
||||||
|
return memoryPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置记忆策略。
|
||||||
|
*
|
||||||
|
* @param memoryPolicy 记忆策略
|
||||||
|
*/
|
||||||
|
public void setMemoryPolicy(AgentMemoryPolicy memoryPolicy) {
|
||||||
|
this.memoryPolicy = memoryPolicy == null ? AgentMemoryPolicy.autoContext() : memoryPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取持久化策略。
|
||||||
|
*
|
||||||
|
* @return 持久化策略
|
||||||
|
*/
|
||||||
|
public AgentPersistencePolicy getPersistencePolicy() {
|
||||||
|
return persistencePolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置持久化策略。
|
||||||
|
*
|
||||||
|
* @param persistencePolicy 持久化策略
|
||||||
|
*/
|
||||||
|
public void setPersistencePolicy(AgentPersistencePolicy persistencePolicy) {
|
||||||
|
this.persistencePolicy = persistencePolicy == null ? AgentPersistencePolicy.disabled() : persistencePolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取SkillBox 设置。
|
||||||
|
*
|
||||||
|
* @return SkillBox 设置
|
||||||
|
*/
|
||||||
|
public AgentSkillBoxSpec getSkillBoxSpec() {
|
||||||
|
return skillBoxSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置SkillBox 设置。
|
||||||
|
*
|
||||||
|
* @param skillBoxSpec SkillBox 设置
|
||||||
|
*/
|
||||||
|
public void setSkillBoxSpec(AgentSkillBoxSpec skillBoxSpec) {
|
||||||
|
this.skillBoxSpec = skillBoxSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @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,86 @@
|
|||||||
|
package com.easyagents.agent.runtime;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时执行参数。
|
||||||
|
*/
|
||||||
|
public class AgentExecutionOptions {
|
||||||
|
|
||||||
|
private int maxIters = 10;
|
||||||
|
private Duration timeout = Duration.ofMinutes(5);
|
||||||
|
private boolean reasoningEnabled = true;
|
||||||
|
private boolean toolCallingEnabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最大 ReAct 迭代次数。
|
||||||
|
*
|
||||||
|
* @return 最大迭代次数
|
||||||
|
*/
|
||||||
|
public int getMaxIters() {
|
||||||
|
return maxIters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最大 ReAct 迭代次数。
|
||||||
|
*
|
||||||
|
* @param maxIters 最大迭代次数
|
||||||
|
*/
|
||||||
|
public void setMaxIters(int maxIters) {
|
||||||
|
this.maxIters = maxIters <= 0 ? 10 : maxIters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取超时时间。
|
||||||
|
*
|
||||||
|
* @return 超时时间
|
||||||
|
*/
|
||||||
|
public Duration getTimeout() {
|
||||||
|
return timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置超时时间。
|
||||||
|
*
|
||||||
|
* @param timeout 超时时间
|
||||||
|
*/
|
||||||
|
public void setTimeout(Duration timeout) {
|
||||||
|
this.timeout = timeout == null ? Duration.ofMinutes(5) : timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回是否reasoning events are enabled。
|
||||||
|
*
|
||||||
|
* @return 启用时为 true
|
||||||
|
*/
|
||||||
|
public boolean isReasoningEnabled() {
|
||||||
|
return reasoningEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置whether reasoning events are enabled。
|
||||||
|
*
|
||||||
|
* @param reasoningEnabled 启用时为 true
|
||||||
|
*/
|
||||||
|
public void setReasoningEnabled(boolean reasoningEnabled) {
|
||||||
|
this.reasoningEnabled = reasoningEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回是否tool calls are enabled。
|
||||||
|
*
|
||||||
|
* @return 启用时为 true
|
||||||
|
*/
|
||||||
|
public boolean isToolCallingEnabled() {
|
||||||
|
return toolCallingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置whether tool calls are enabled。
|
||||||
|
*
|
||||||
|
* @param toolCallingEnabled 启用时为 true
|
||||||
|
*/
|
||||||
|
public void setToolCallingEnabled(boolean toolCallingEnabled) {
|
||||||
|
this.toolCallingEnabled = toolCallingEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.easyagents.agent.runtime;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体运行的最终结果。
|
||||||
|
*/
|
||||||
|
public class AgentRunResult {
|
||||||
|
|
||||||
|
private String content;
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最终内容。
|
||||||
|
*
|
||||||
|
* @return 最终内容
|
||||||
|
*/
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最终内容。
|
||||||
|
*
|
||||||
|
* @param content 最终内容
|
||||||
|
*/
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @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,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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 有状态智能体运行器。
|
||||||
|
*/
|
||||||
|
public interface AgentRuntime {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化智能体运行器。
|
||||||
|
*
|
||||||
|
* @param request 初始化请求
|
||||||
|
*/
|
||||||
|
void init(AgentInitRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送用户消息并流式输出运行事件。
|
||||||
|
*
|
||||||
|
* @param userMessage 用户消息
|
||||||
|
* @return 运行事件流
|
||||||
|
*/
|
||||||
|
Flux<AgentRuntimeEvent> stream(AgentMessage userMessage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复一次已挂起的运行。
|
||||||
|
*
|
||||||
|
* @param request 恢复请求
|
||||||
|
* @return 运行事件流
|
||||||
|
*/
|
||||||
|
Flux<AgentRuntimeEvent> resume(AgentResumeRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭运行器并释放底层资源。
|
||||||
|
*/
|
||||||
|
default void close() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package com.easyagents.agent.runtime;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 传递给工具和检索器的中立运行时上下文。
|
||||||
|
*/
|
||||||
|
public class AgentRuntimeContext {
|
||||||
|
|
||||||
|
private String tenantId;
|
||||||
|
private String userId;
|
||||||
|
private String userName;
|
||||||
|
private String sessionId;
|
||||||
|
private String traceId;
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取租户ID。
|
||||||
|
*
|
||||||
|
* @return 租户ID
|
||||||
|
*/
|
||||||
|
public String getTenantId() {
|
||||||
|
return tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置租户ID。
|
||||||
|
*
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
*/
|
||||||
|
public void setTenantId(String tenantId) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户ID。
|
||||||
|
*
|
||||||
|
* @return 用户ID
|
||||||
|
*/
|
||||||
|
public String getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户ID。
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
*/
|
||||||
|
public void setUserId(String userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户名。
|
||||||
|
*
|
||||||
|
* @return 用户名
|
||||||
|
*/
|
||||||
|
public String getUserName() {
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户名。
|
||||||
|
*
|
||||||
|
* @param userName 用户名
|
||||||
|
*/
|
||||||
|
public void setUserName(String userName) {
|
||||||
|
this.userName = userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话ID。
|
||||||
|
*
|
||||||
|
* @return 会话ID
|
||||||
|
*/
|
||||||
|
public String getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置会话ID。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话ID
|
||||||
|
*/
|
||||||
|
public void setSessionId(String sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取链路ID。
|
||||||
|
*
|
||||||
|
* @return 链路ID
|
||||||
|
*/
|
||||||
|
public String getTraceId() {
|
||||||
|
return traceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置链路ID。
|
||||||
|
*
|
||||||
|
* @param traceId 链路ID
|
||||||
|
*/
|
||||||
|
public void setTraceId(String traceId) {
|
||||||
|
this.traceId = traceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @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,26 @@
|
|||||||
|
package com.easyagents.agent.runtime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体运行时基础设施抛出的异常。
|
||||||
|
*/
|
||||||
|
public class AgentRuntimeException extends RuntimeException {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建包含错误消息的异常。
|
||||||
|
*
|
||||||
|
* @param message 错误消息
|
||||||
|
*/
|
||||||
|
public AgentRuntimeException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建包含错误消息和根因的异常。
|
||||||
|
*
|
||||||
|
* @param message 错误消息
|
||||||
|
* @param cause 根因
|
||||||
|
*/
|
||||||
|
public AgentRuntimeException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
package com.easyagents.agent.runtime;
|
||||||
|
|
||||||
|
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.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 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<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按知识库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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求ID。
|
||||||
|
*
|
||||||
|
* @return 请求ID
|
||||||
|
*/
|
||||||
|
public String getRequestId() {
|
||||||
|
return requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置请求ID。
|
||||||
|
*
|
||||||
|
* @param requestId 请求ID
|
||||||
|
*/
|
||||||
|
public void setRequestId(String requestId) {
|
||||||
|
this.requestId = requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取链路ID。
|
||||||
|
*
|
||||||
|
* @return 链路ID
|
||||||
|
*/
|
||||||
|
public String getTraceId() {
|
||||||
|
return traceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置链路ID。
|
||||||
|
*
|
||||||
|
* @param traceId 链路ID
|
||||||
|
*/
|
||||||
|
public void setTraceId(String traceId) {
|
||||||
|
this.traceId = traceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话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 AgentRuntimeContext getRuntimeContext() {
|
||||||
|
return runtimeContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置运行时上下文。
|
||||||
|
*
|
||||||
|
* @param runtimeContext 运行时上下文
|
||||||
|
*/
|
||||||
|
public void setRuntimeContext(AgentRuntimeContext runtimeContext) {
|
||||||
|
this.runtimeContext = runtimeContext == null ? new AgentRuntimeContext() : runtimeContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户消息。
|
||||||
|
*
|
||||||
|
* @return 用户消息
|
||||||
|
*/
|
||||||
|
public AgentMessage getUserMessage() {
|
||||||
|
return userMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户消息。
|
||||||
|
*
|
||||||
|
* @param userMessage 用户消息
|
||||||
|
*/
|
||||||
|
public void setUserMessage(AgentMessage userMessage) {
|
||||||
|
this.userMessage = userMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取记忆快照。
|
||||||
|
*
|
||||||
|
* @return 记忆快照
|
||||||
|
*/
|
||||||
|
public AgentMemorySnapshot getMemorySnapshot() {
|
||||||
|
return memorySnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置记忆快照。
|
||||||
|
*
|
||||||
|
* @param memorySnapshot 记忆快照
|
||||||
|
*/
|
||||||
|
public void setMemorySnapshot(AgentMemorySnapshot memorySnapshot) {
|
||||||
|
this.memorySnapshot = memorySnapshot == null ? new AgentMemorySnapshot() : memorySnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具调用器。
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话存储。
|
||||||
|
*
|
||||||
|
* @return 会话存储
|
||||||
|
*/
|
||||||
|
public AgentSessionStore getSessionStore() {
|
||||||
|
return sessionStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置会话存储。
|
||||||
|
*
|
||||||
|
* @param sessionStore 会话存储
|
||||||
|
*/
|
||||||
|
public void setSessionStore(AgentSessionStore sessionStore) {
|
||||||
|
this.sessionStore = sessionStore == null ? NoopAgentSessionStore.INSTANCE : sessionStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对话记录器。
|
||||||
|
*
|
||||||
|
* @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 String getCancelReason() {
|
||||||
|
return cancelReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置取消原因。
|
||||||
|
*
|
||||||
|
* @param cancelReason 取消原因
|
||||||
|
*/
|
||||||
|
public void setCancelReason(String cancelReason) {
|
||||||
|
this.cancelReason = cancelReason;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
package com.easyagents.agent.runtime.agentscope;
|
||||||
|
|
||||||
|
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.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.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 一库一工具。
|
||||||
|
*/
|
||||||
|
public class AgentScopeKnowledgeAdapter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 Agent 知识库声明创建模型可见工具定义。
|
||||||
|
*
|
||||||
|
* @param context 运行时上下文
|
||||||
|
* @return 知识库工具定义
|
||||||
|
* @throws AgentRuntimeException 声明、运行名或 Retriever 绑定不合法时抛出
|
||||||
|
*/
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
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 Toolkit。
|
||||||
|
*
|
||||||
|
* @param context 运行时上下文
|
||||||
|
* @param toolSpecs 知识库工具定义
|
||||||
|
* @param toolkit AgentScope Toolkit
|
||||||
|
* @param toolAdapter 中立工具适配器
|
||||||
|
* @param approvalCoordinator 工具审批协调器
|
||||||
|
* @param turnContextHolder 当前运行轮次上下文持有器
|
||||||
|
*/
|
||||||
|
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 finalDocuments;
|
||||||
|
}
|
||||||
|
for (AgentKnowledgeDocument document : documents) {
|
||||||
|
if (document == null || !passesThreshold(document, knowledgeSpec.getScoreThreshold())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
preserveKnowledgeMetadata(knowledgeSpec, document);
|
||||||
|
finalDocuments.add(document);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断文档最终分数是否达到绑定阈值。
|
||||||
|
*
|
||||||
|
* @param document 检索文档
|
||||||
|
* @param scoreThreshold 分数阈值
|
||||||
|
* @return 达到阈值时为 true
|
||||||
|
*/
|
||||||
|
private boolean passesThreshold(AgentKnowledgeDocument document, double scoreThreshold) {
|
||||||
|
if (scoreThreshold <= 0D) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return document.getScore() != null && document.getScore() >= scoreThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将知识库归属信息合并到文档元数据中。
|
||||||
|
*
|
||||||
|
* @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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将最终文档转换为 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package com.easyagents.agent.runtime.agentscope;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.memory.AgentMemoryCompressionParameter;
|
||||||
|
import com.easyagents.agent.runtime.memory.AgentMemoryPolicy;
|
||||||
|
import com.easyagents.agent.runtime.memory.AgentMemorySnapshot;
|
||||||
|
import com.easyagents.agent.runtime.message.AgentMessage;
|
||||||
|
import io.agentscope.core.memory.InMemoryMemory;
|
||||||
|
import io.agentscope.core.memory.Memory;
|
||||||
|
import io.agentscope.core.memory.autocontext.AutoContextConfig;
|
||||||
|
import io.agentscope.core.memory.autocontext.AutoContextMemory;
|
||||||
|
import io.agentscope.core.model.Model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将运行时记忆设置适配为 AgentScope 记忆。
|
||||||
|
*/
|
||||||
|
public class AgentScopeMemoryAdapter {
|
||||||
|
|
||||||
|
private final AgentScopeMessageAdapter messageAdapter = new AgentScopeMessageAdapter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建记忆并附加恢复后的历史消息。
|
||||||
|
*
|
||||||
|
* @param snapshot 记忆快照
|
||||||
|
* @param policy 记忆策略
|
||||||
|
* @param model 用于自动压缩的模型
|
||||||
|
* @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()) {
|
||||||
|
autoContextConfig = toAutoContextConfig(safePolicy.getCompressionParameter());
|
||||||
|
memory = new AutoContextMemory(autoContextConfig, new AgentScopeAutoContextCompressionModel(model));
|
||||||
|
} else {
|
||||||
|
memory = new InMemoryMemory();
|
||||||
|
}
|
||||||
|
attachMessages(memory, snapshot, safePolicy.getMaxAttachedMessageCount());
|
||||||
|
return new AgentScopeMemoryBuildResult(memory, autoContextConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将自动压缩参数转换为 AgentScope 配置。
|
||||||
|
*
|
||||||
|
* @param parameter 自动压缩参数
|
||||||
|
* @return AgentScope 配置
|
||||||
|
*/
|
||||||
|
public AutoContextConfig toAutoContextConfig(AgentMemoryCompressionParameter parameter) {
|
||||||
|
AgentMemoryCompressionParameter safeParameter = parameter == null ? new AgentMemoryCompressionParameter() : parameter;
|
||||||
|
Integer compressionThreshold = safeParameter.getMinCompressionTokenThreshold();
|
||||||
|
return AutoContextConfig.builder()
|
||||||
|
.msgThreshold(safeParameter.getMsgThreshold() == null ? Integer.MAX_VALUE : safeParameter.getMsgThreshold())
|
||||||
|
.lastKeep(safeParameter.getLastKeep())
|
||||||
|
.tokenRatio(1.0D)
|
||||||
|
.maxToken(compressionThreshold == null ? Long.MAX_VALUE : compressionThreshold)
|
||||||
|
.largePayloadThreshold(safeParameter.getLargePayloadThreshold())
|
||||||
|
.minCompressionTokenThreshold(compressionThreshold == null ? Integer.MAX_VALUE : compressionThreshold)
|
||||||
|
.currentRoundCompressionRatio(safeParameter.getCurrentRoundCompressionRatio())
|
||||||
|
.minConsecutiveToolMessages(safeParameter.getMinConsecutiveToolMessages())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void attachMessages(Memory memory, AgentMemorySnapshot snapshot, int maxAttachedMessageCount) {
|
||||||
|
if (snapshot == null || snapshot.getMessages().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<AgentMessage> messages = snapshot.getMessages();
|
||||||
|
int fromIndex = Math.max(0, messages.size() - maxAttachedMessageCount);
|
||||||
|
for (AgentMessage message : messages.subList(fromIndex, messages.size())) {
|
||||||
|
memory.addMessage(messageAdapter.toMsg(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
package com.easyagents.agent.runtime.agentscope;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.message.*;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时消息与 AgentScope 消息的双向适配器。
|
||||||
|
*/
|
||||||
|
public class AgentScopeMessageAdapter {
|
||||||
|
|
||||||
|
/** AgentScope URLSource 中用于持久化业务媒体引用的私有协议。 */
|
||||||
|
public static final String MEDIA_REFERENCE_SCHEME = "easyagents-media://";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将运行时消息转换为 AgentScope 消息。
|
||||||
|
*
|
||||||
|
* @param message 运行时消息
|
||||||
|
* @return AgentScope 消息
|
||||||
|
*/
|
||||||
|
public Msg toMsg(AgentMessage message) {
|
||||||
|
if (message == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Msg.builder()
|
||||||
|
.id(message.getMessageId())
|
||||||
|
.name(message.getName())
|
||||||
|
.role(toRole(message.getRole()))
|
||||||
|
.content(toContentBlocks(message.getContentBlocks()))
|
||||||
|
.metadata(copyMap(message.getMetadata()))
|
||||||
|
.timestamp(message.getCreatedAt() == null ? null : message.getCreatedAt().toString())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 AgentScope 消息转换为运行时消息。
|
||||||
|
*
|
||||||
|
* @param msg AgentScope 消息
|
||||||
|
* @return 运行时消息
|
||||||
|
*/
|
||||||
|
public AgentMessage toAgentMessage(Msg msg) {
|
||||||
|
if (msg == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
AgentMessage message = new AgentMessage();
|
||||||
|
message.setMessageId(msg.getId());
|
||||||
|
message.setName(msg.getName());
|
||||||
|
message.setRole(toRole(msg.getRole()));
|
||||||
|
message.setMetadata(msg.getMetadata());
|
||||||
|
if (msg.getTimestamp() != null && !msg.getTimestamp().isBlank()) {
|
||||||
|
Instant parsed = parseTimestamp(msg.getTimestamp());
|
||||||
|
if (parsed != null) {
|
||||||
|
message.setCreatedAt(parsed);
|
||||||
|
} else {
|
||||||
|
message.getMetadata().put("rawTimestamp", msg.getTimestamp());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message.setContentBlocks(toBlocks(msg.getContent()));
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 AgentScope 块转换为运行时块。
|
||||||
|
*
|
||||||
|
* @param block AgentScope 块
|
||||||
|
* @return 运行时块
|
||||||
|
*/
|
||||||
|
public AgentContentBlock toAgentBlock(ContentBlock block) {
|
||||||
|
if (block == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (block instanceof TextBlock textBlock) {
|
||||||
|
return new AgentTextBlock(textBlock.getText());
|
||||||
|
}
|
||||||
|
if (block instanceof ThinkingBlock thinkingBlock) {
|
||||||
|
AgentThinkingBlock converted = new AgentThinkingBlock(thinkingBlock.getThinking());
|
||||||
|
converted.getMetadata().putAll(copyMap(thinkingBlock.getMetadata()));
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
if (block instanceof ToolUseBlock toolUseBlock) {
|
||||||
|
AgentToolUseBlock converted = new AgentToolUseBlock(toolUseBlock.getId(), toolUseBlock.getName(), copyMap(toolUseBlock.getInput()));
|
||||||
|
converted.setContent(toolUseBlock.getContent());
|
||||||
|
converted.getMetadata().putAll(copyMap(toolUseBlock.getMetadata()));
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
if (block instanceof ToolResultBlock toolResultBlock) {
|
||||||
|
AgentToolResultBlock converted = new AgentToolResultBlock(toolResultBlock.getId(), toolResultBlock.getName());
|
||||||
|
converted.setOutput(toBlocks(toolResultBlock.getOutput()));
|
||||||
|
converted.setSuspended(toolResultBlock.isSuspended());
|
||||||
|
converted.getMetadata().putAll(copyMap(toolResultBlock.getMetadata()));
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
if (block instanceof ImageBlock imageBlock) {
|
||||||
|
return toMediaBlock(imageBlock, "image");
|
||||||
|
}
|
||||||
|
if (block instanceof AudioBlock audioBlock) {
|
||||||
|
return toMediaBlock(audioBlock, "audio");
|
||||||
|
}
|
||||||
|
if (block instanceof VideoBlock videoBlock) {
|
||||||
|
return toMediaBlock(videoBlock, "video");
|
||||||
|
}
|
||||||
|
AgentUnknownBlock converted = new AgentUnknownBlock();
|
||||||
|
converted.setSourceClassName(block.getClass().getName());
|
||||||
|
converted.setSourceTypeName(resolveTypeName(block));
|
||||||
|
converted.setRawText(block.toString());
|
||||||
|
converted.getRawMetadata().put("className", block.getClass().getName());
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将运行时块转换为 AgentScope 块。
|
||||||
|
*
|
||||||
|
* @param block 运行时块
|
||||||
|
* @return AgentScope 块
|
||||||
|
*/
|
||||||
|
public ContentBlock toContentBlock(AgentContentBlock block) {
|
||||||
|
if (block == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (block instanceof AgentTextBlock textBlock) {
|
||||||
|
return TextBlock.builder().text(textBlock.getText()).build();
|
||||||
|
}
|
||||||
|
if (block instanceof AgentThinkingBlock thinkingBlock) {
|
||||||
|
return ThinkingBlock.builder()
|
||||||
|
.thinking(thinkingBlock.getThinking())
|
||||||
|
.metadata(copyMap(thinkingBlock.getMetadata()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
if (block instanceof AgentToolUseBlock toolUseBlock) {
|
||||||
|
return ToolUseBlock.builder()
|
||||||
|
.id(toolUseBlock.getId())
|
||||||
|
.name(toolUseBlock.getName())
|
||||||
|
.input(copyMap(toolUseBlock.getInput()))
|
||||||
|
.content(toolUseBlock.getContent())
|
||||||
|
.metadata(copyMap(toolUseBlock.getMetadata()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
if (block instanceof AgentToolResultBlock toolResultBlock) {
|
||||||
|
Map<String, Object> metadata = copyMap(toolResultBlock.getMetadata());
|
||||||
|
metadata.put("suspended", toolResultBlock.isSuspended());
|
||||||
|
return ToolResultBlock.of(
|
||||||
|
toolResultBlock.getId(),
|
||||||
|
toolResultBlock.getName(),
|
||||||
|
toContentBlocks(toolResultBlock.getOutput()),
|
||||||
|
metadata);
|
||||||
|
}
|
||||||
|
if (block instanceof AgentMediaBlock mediaBlock) {
|
||||||
|
return toMediaBlock(mediaBlock);
|
||||||
|
}
|
||||||
|
if (block instanceof AgentUnknownBlock unknownBlock) {
|
||||||
|
return toUnknownContentBlock(unknownBlock);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 AgentScope 块列表转换为运行时块列表。
|
||||||
|
*
|
||||||
|
* @param blocks AgentScope 块列表
|
||||||
|
* @return 运行时块列表
|
||||||
|
*/
|
||||||
|
public List<AgentContentBlock> toBlocks(List<ContentBlock> blocks) {
|
||||||
|
List<AgentContentBlock> converted = new ArrayList<>();
|
||||||
|
if (blocks == null) {
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
for (ContentBlock block : blocks) {
|
||||||
|
AgentContentBlock agentBlock = toAgentBlock(block);
|
||||||
|
if (agentBlock != null) {
|
||||||
|
converted.add(agentBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将运行时块列表转换为 AgentScope 块列表。
|
||||||
|
*
|
||||||
|
* @param blocks 运行时块列表
|
||||||
|
* @return AgentScope 块列表
|
||||||
|
*/
|
||||||
|
public List<ContentBlock> toContentBlocks(List<AgentContentBlock> blocks) {
|
||||||
|
List<ContentBlock> converted = new ArrayList<>();
|
||||||
|
if (blocks == null) {
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
for (AgentContentBlock block : blocks) {
|
||||||
|
ContentBlock contentBlock = toContentBlock(block);
|
||||||
|
if (contentBlock != null) {
|
||||||
|
converted.add(contentBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MsgRole toRole(AgentMessageRole role) {
|
||||||
|
if (role == AgentMessageRole.ASSISTANT) {
|
||||||
|
return MsgRole.ASSISTANT;
|
||||||
|
}
|
||||||
|
if (role == AgentMessageRole.SYSTEM) {
|
||||||
|
return MsgRole.SYSTEM;
|
||||||
|
}
|
||||||
|
if (role == AgentMessageRole.TOOL) {
|
||||||
|
return MsgRole.TOOL;
|
||||||
|
}
|
||||||
|
return MsgRole.USER;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentMessageRole toRole(MsgRole role) {
|
||||||
|
if (role == MsgRole.ASSISTANT) {
|
||||||
|
return AgentMessageRole.ASSISTANT;
|
||||||
|
}
|
||||||
|
if (role == MsgRole.SYSTEM) {
|
||||||
|
return AgentMessageRole.SYSTEM;
|
||||||
|
}
|
||||||
|
if (role == MsgRole.TOOL) {
|
||||||
|
return AgentMessageRole.TOOL;
|
||||||
|
}
|
||||||
|
return AgentMessageRole.USER;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> copyMap(Map<String, Object> metadata) {
|
||||||
|
return metadata == null ? new LinkedHashMap<>() : new LinkedHashMap<>(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContentBlock toMediaBlock(AgentMediaBlock block) {
|
||||||
|
if (block == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ("audio".equalsIgnoreCase(block.getMediaKind())) {
|
||||||
|
return AudioBlock.builder().source(mediaSource(block)).build();
|
||||||
|
}
|
||||||
|
if ("video".equalsIgnoreCase(block.getMediaKind())) {
|
||||||
|
return VideoBlock.builder()
|
||||||
|
.source(mediaSource(block))
|
||||||
|
.fps(block.getFps())
|
||||||
|
.maxFrames(block.getMaxFrames())
|
||||||
|
.minPixels(block.getMinPixels())
|
||||||
|
.maxPixels(block.getMaxPixels())
|
||||||
|
.totalPixels(block.getTotalPixels())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
return ImageBlock.builder()
|
||||||
|
.source(mediaSource(block))
|
||||||
|
.minPixels(block.getMinPixels())
|
||||||
|
.maxPixels(block.getMaxPixels())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentContentBlock toMediaBlock(ContentBlock block, String mediaKind) {
|
||||||
|
AgentMediaBlock converted = new AgentMediaBlock(mediaKind);
|
||||||
|
if (block instanceof ImageBlock imageBlock) {
|
||||||
|
readSource(imageBlock.getSource(), converted);
|
||||||
|
converted.setMinPixels(imageBlock.getMinPixels());
|
||||||
|
converted.setMaxPixels(imageBlock.getMaxPixels());
|
||||||
|
} else if (block instanceof AudioBlock audioBlock) {
|
||||||
|
readSource(audioBlock.getSource(), converted);
|
||||||
|
} else if (block instanceof VideoBlock videoBlock) {
|
||||||
|
readSource(videoBlock.getSource(), converted);
|
||||||
|
converted.setFps(videoBlock.getFps());
|
||||||
|
converted.setMaxFrames(videoBlock.getMaxFrames());
|
||||||
|
converted.setMinPixels(videoBlock.getMinPixels());
|
||||||
|
converted.setMaxPixels(videoBlock.getMaxPixels());
|
||||||
|
converted.setTotalPixels(videoBlock.getTotalPixels());
|
||||||
|
}
|
||||||
|
return converted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private io.agentscope.core.message.Source mediaSource(AgentMediaBlock block) {
|
||||||
|
if (block.getData() != null && !block.getData().isBlank()) {
|
||||||
|
return Base64Source.builder()
|
||||||
|
.mediaType(block.getMimeType())
|
||||||
|
.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readSource(io.agentscope.core.message.Source source, AgentMediaBlock block) {
|
||||||
|
if (source instanceof Base64Source base64Source) {
|
||||||
|
block.setMimeType(base64Source.getMediaType());
|
||||||
|
block.setData(base64Source.getData());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (source instanceof URLSource urlSource) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContentBlock toUnknownContentBlock(AgentUnknownBlock block) {
|
||||||
|
String text = block.getRawText();
|
||||||
|
if (text == null || text.isBlank()) {
|
||||||
|
text = "[unsupported content block: " + block.getSourceTypeName() + "]";
|
||||||
|
}
|
||||||
|
return TextBlock.builder()
|
||||||
|
.text(text)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Instant parseTimestamp(String timestamp) {
|
||||||
|
try {
|
||||||
|
return Instant.parse(timestamp);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// 继续尝试其他格式。
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return OffsetDateTime.parse(timestamp).toInstant();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// 继续尝试其他格式。
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Instant.ofEpochMilli(Long.parseLong(timestamp));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveTypeName(ContentBlock block) {
|
||||||
|
if (block instanceof TextBlock) {
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
if (block instanceof ThinkingBlock) {
|
||||||
|
return "thinking";
|
||||||
|
}
|
||||||
|
if (block instanceof ToolUseBlock) {
|
||||||
|
return "tool_use";
|
||||||
|
}
|
||||||
|
if (block instanceof ToolResultBlock) {
|
||||||
|
return "tool_result";
|
||||||
|
}
|
||||||
|
if (block instanceof ImageBlock) {
|
||||||
|
return "image";
|
||||||
|
}
|
||||||
|
if (block instanceof AudioBlock) {
|
||||||
|
return "audio";
|
||||||
|
}
|
||||||
|
if (block instanceof VideoBlock) {
|
||||||
|
return "video";
|
||||||
|
}
|
||||||
|
return block.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
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.*;
|
||||||
|
import io.agentscope.core.model.ollama.OllamaOptions;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据运行时模型声明创建 AgentScope 模型实例。
|
||||||
|
*/
|
||||||
|
public class AgentScopeModelFactory implements AgentModelFactory<Model> {
|
||||||
|
|
||||||
|
private static final String DEEPSEEK_BASE_URL = "https://api.deepseek.com";
|
||||||
|
private static final String GLM_BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
|
||||||
|
private static final String MINIMAX_BASE_URL = "https://api.minimax.io/v1";
|
||||||
|
private static final String MOONSHOT_BASE_URL = "https://api.moonshot.cn/v1";
|
||||||
|
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) {
|
||||||
|
throw new AgentRuntimeException("Agent model spec is required.");
|
||||||
|
}
|
||||||
|
GenerateOptions options = toGenerateOptions(modelSpec, generationOptions);
|
||||||
|
AgentModelProviderType providerType = modelSpec.getProviderType();
|
||||||
|
validateHttpTransportSupport(providerType, modelSpec.getHttpVersionPolicy());
|
||||||
|
if (providerType == AgentModelProviderType.OLLAMA) {
|
||||||
|
return buildOllama(modelSpec, options);
|
||||||
|
}
|
||||||
|
if (providerType == AgentModelProviderType.OPENAI) {
|
||||||
|
return buildOpenAi(modelSpec, options);
|
||||||
|
}
|
||||||
|
if (providerType == AgentModelProviderType.ANTHROPIC) {
|
||||||
|
return buildAnthropic(modelSpec, options);
|
||||||
|
}
|
||||||
|
if (providerType == AgentModelProviderType.GEMINI) {
|
||||||
|
return buildGemini(modelSpec, options);
|
||||||
|
}
|
||||||
|
if (providerType == AgentModelProviderType.DEEPSEEK) {
|
||||||
|
return buildDeepSeek(modelSpec, options);
|
||||||
|
}
|
||||||
|
if (providerType == AgentModelProviderType.GLM) {
|
||||||
|
return buildGlm(modelSpec, options);
|
||||||
|
}
|
||||||
|
if (providerType == AgentModelProviderType.MINIMAX) {
|
||||||
|
return buildOpenAiCompatible(modelSpec, options, MINIMAX_BASE_URL);
|
||||||
|
}
|
||||||
|
if (providerType == AgentModelProviderType.MOONSHOT) {
|
||||||
|
return buildOpenAiCompatible(modelSpec, options, MOONSHOT_BASE_URL);
|
||||||
|
}
|
||||||
|
if (providerType == AgentModelProviderType.ARK) {
|
||||||
|
return buildOpenAiCompatible(modelSpec, options, ARK_BASE_URL);
|
||||||
|
}
|
||||||
|
if (providerType == AgentModelProviderType.SILICONFLOW) {
|
||||||
|
return buildOpenAiCompatible(modelSpec, options, SILICONFLOW_BASE_URL);
|
||||||
|
}
|
||||||
|
if (providerType == AgentModelProviderType.DASHSCOPE) {
|
||||||
|
return buildDashScope(modelSpec, generationOptions, options);
|
||||||
|
}
|
||||||
|
if (providerType == AgentModelProviderType.OPENAI_COMPATIBLE || providerType == AgentModelProviderType.CUSTOM) {
|
||||||
|
return buildOpenAiCompatible(modelSpec, options, null);
|
||||||
|
}
|
||||||
|
throw new AgentRuntimeException("Unsupported agent model provider: " + providerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将中立参数转换为 AgentScope 参数。
|
||||||
|
*
|
||||||
|
* @param modelSpec 模型声明
|
||||||
|
* @param options 中立参数
|
||||||
|
* @return AgentScope 参数
|
||||||
|
*/
|
||||||
|
public GenerateOptions toGenerateOptions(AgentModelSpec modelSpec, AgentGenerationOptions options) {
|
||||||
|
AgentGenerationOptions safeOptions = options == null ? new AgentGenerationOptions() : options;
|
||||||
|
GenerateOptions.Builder builder = GenerateOptions.builder()
|
||||||
|
.apiKey(modelSpec.getApiKey())
|
||||||
|
.baseUrl(modelSpec.getBaseUrl())
|
||||||
|
.endpointPath(modelSpec.getEndpointPath())
|
||||||
|
.modelName(modelSpec.getModelName())
|
||||||
|
.stream(safeOptions.getStream())
|
||||||
|
.temperature(safeOptions.getTemperature())
|
||||||
|
.topP(safeOptions.getTopP())
|
||||||
|
.topK(safeOptions.getTopK())
|
||||||
|
.maxTokens(safeOptions.getMaxTokens())
|
||||||
|
.maxCompletionTokens(safeOptions.getMaxCompletionTokens())
|
||||||
|
.thinkingBudget(safeOptions.getThinkingBudget())
|
||||||
|
.reasoningEffort(safeOptions.getReasoningEffort());
|
||||||
|
for (Map.Entry<String, Object> entry : safeOptions.getAdditionalBodyParams().entrySet()) {
|
||||||
|
builder.additionalBodyParam(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, String> entry : safeOptions.getAdditionalHeaders().entrySet()) {
|
||||||
|
builder.additionalHeader(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, String> entry : safeOptions.getAdditionalQueryParams().entrySet()) {
|
||||||
|
builder.additionalQueryParam(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 OpenAI 原生模型。
|
||||||
|
*
|
||||||
|
* @param modelSpec 模型声明
|
||||||
|
* @param options 生成参数
|
||||||
|
* @return 模型
|
||||||
|
*/
|
||||||
|
private Model buildOpenAi(AgentModelSpec modelSpec, GenerateOptions options) {
|
||||||
|
return buildOpenAiCompatible(modelSpec, options, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 OpenAI-compatible 协议构建模型。
|
||||||
|
*
|
||||||
|
* @param modelSpec 模型声明
|
||||||
|
* @param options 生成参数
|
||||||
|
* @param defaultBaseUrl 默认基础 URL
|
||||||
|
* @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(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 Anthropic 模型。
|
||||||
|
*
|
||||||
|
* @param modelSpec 模型声明
|
||||||
|
* @param options 生成参数
|
||||||
|
* @return 模型
|
||||||
|
*/
|
||||||
|
private Model buildAnthropic(AgentModelSpec modelSpec, GenerateOptions options) {
|
||||||
|
return AnthropicChatModel.builder()
|
||||||
|
.apiKey(modelSpec.getApiKey())
|
||||||
|
.modelName(modelSpec.getModelName())
|
||||||
|
.baseUrl(modelSpec.getBaseUrl())
|
||||||
|
.stream(Boolean.TRUE.equals(options.getStream()))
|
||||||
|
.defaultOptions(options)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 Gemini 模型。
|
||||||
|
*
|
||||||
|
* @param modelSpec 模型声明
|
||||||
|
* @param options 生成参数
|
||||||
|
* @return 模型
|
||||||
|
*/
|
||||||
|
private Model buildGemini(AgentModelSpec modelSpec, GenerateOptions options) {
|
||||||
|
return GeminiChatModel.builder()
|
||||||
|
.apiKey(modelSpec.getApiKey())
|
||||||
|
.modelName(modelSpec.getModelName())
|
||||||
|
.baseUrl(modelSpec.getBaseUrl())
|
||||||
|
.streamEnabled(Boolean.TRUE.equals(options.getStream()))
|
||||||
|
.defaultOptions(options)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 OpenAI-compatible 协议构建 DeepSeek 模型。
|
||||||
|
*
|
||||||
|
* @param modelSpec 模型声明
|
||||||
|
* @param options 生成参数
|
||||||
|
* @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(baseUrl)
|
||||||
|
.endpointPath(modelSpec.getEndpointPath())
|
||||||
|
.stream(Boolean.TRUE.equals(options.getStream()))
|
||||||
|
.httpTransport(httpTransportProvider.getTransport(modelSpec.getHttpVersionPolicy(), baseUrl))
|
||||||
|
.formatter(modelSpec.getMessageContentFormat() == AgentMessageContentFormat.TEXT_PARTS
|
||||||
|
? new AgentDeepSeekChatFormatter()
|
||||||
|
: new DeepSeekFormatter())
|
||||||
|
.generateOptions(options);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 OpenAI-compatible 协议构建智谱 GLM 模型。
|
||||||
|
*
|
||||||
|
* @param modelSpec 模型声明
|
||||||
|
* @param options 生成参数
|
||||||
|
* @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(baseUrl)
|
||||||
|
.endpointPath(modelSpec.getEndpointPath())
|
||||||
|
.stream(Boolean.TRUE.equals(options.getStream()))
|
||||||
|
.httpTransport(httpTransportProvider.getTransport(modelSpec.getHttpVersionPolicy(), baseUrl))
|
||||||
|
.formatter(modelSpec.getMessageContentFormat() == AgentMessageContentFormat.TEXT_PARTS
|
||||||
|
? new AgentGLMChatFormatter()
|
||||||
|
: new GLMFormatter())
|
||||||
|
.generateOptions(options);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 Ollama 模型。
|
||||||
|
*
|
||||||
|
* @param modelSpec 模型声明
|
||||||
|
* @param options 生成参数
|
||||||
|
* @return 模型
|
||||||
|
*/
|
||||||
|
private Model buildOllama(AgentModelSpec modelSpec, GenerateOptions options) {
|
||||||
|
return OllamaChatModel.builder()
|
||||||
|
.modelName(modelSpec.getModelName())
|
||||||
|
.baseUrl(modelSpec.getBaseUrl())
|
||||||
|
.defaultOptions(OllamaOptions.fromGenerateOptions(options))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 DashScope 模型。
|
||||||
|
*
|
||||||
|
* @param modelSpec 模型声明
|
||||||
|
* @param generationOptions 中立生成参数
|
||||||
|
* @param options AgentScope 生成参数
|
||||||
|
* @return 模型
|
||||||
|
*/
|
||||||
|
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(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,未传入时使用供应商默认地址。
|
||||||
|
*
|
||||||
|
* @param modelSpec 模型声明
|
||||||
|
* @param defaultBaseUrl 默认基础 URL
|
||||||
|
* @return 最终基础 URL
|
||||||
|
*/
|
||||||
|
private String resolveBaseUrl(AgentModelSpec modelSpec, String defaultBaseUrl) {
|
||||||
|
if (modelSpec.getBaseUrl() == null || modelSpec.getBaseUrl().isBlank()) {
|
||||||
|
return defaultBaseUrl;
|
||||||
|
}
|
||||||
|
return modelSpec.getBaseUrl();
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package com.easyagents.agent.runtime.agentscope;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.persistence.AgentPersistencePolicy;
|
||||||
|
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.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 AgentScope Session 桥接到运行时 AgentSessionStore。
|
||||||
|
*/
|
||||||
|
public class AgentScopeSessionAdapter implements Session {
|
||||||
|
|
||||||
|
private final AgentSessionStore sessionStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建适配器。
|
||||||
|
*
|
||||||
|
* @param sessionStore 运行时会话存储
|
||||||
|
*/
|
||||||
|
public AgentScopeSessionAdapter(AgentSessionStore sessionStore) {
|
||||||
|
this.sessionStore = sessionStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(SessionKey sessionKey, String name, State state) {
|
||||||
|
sessionStore.save(toKey(sessionKey), name, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将状态列表保存到运行时存储。
|
||||||
|
*
|
||||||
|
* @param sessionKey 会话键
|
||||||
|
* @param name 状态名称
|
||||||
|
* @param states 状态列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void save(SessionKey sessionKey, String name, List<? extends State> states) {
|
||||||
|
sessionStore.saveList(toKey(sessionKey), name, states);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从运行时存储获取单个指定类型的状态。
|
||||||
|
*
|
||||||
|
* @param sessionKey 会话键
|
||||||
|
* @param name 状态名称
|
||||||
|
* @param clazz 状态类型
|
||||||
|
* @param <T> 状态类型
|
||||||
|
* @return 可选状态
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public <T extends State> Optional<T> get(SessionKey sessionKey, String name, Class<T> clazz) {
|
||||||
|
return sessionStore.get(toKey(sessionKey), name, clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从运行时存储获取指定类型的状态列表。
|
||||||
|
*
|
||||||
|
* @param sessionKey 会话键
|
||||||
|
* @param name 状态名称
|
||||||
|
* @param clazz 状态类型
|
||||||
|
* @param <T> 状态类型
|
||||||
|
* @return 状态列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public <T extends State> List<T> getList(SessionKey sessionKey, String name, Class<T> clazz) {
|
||||||
|
return sessionStore.getList(toKey(sessionKey), name, clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回会话键是否存在。
|
||||||
|
*
|
||||||
|
* @param sessionKey 会话键
|
||||||
|
* @return 存在时为 true
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean exists(SessionKey sessionKey) {
|
||||||
|
return sessionStore.exists(toKey(sessionKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除会话键。
|
||||||
|
*
|
||||||
|
* @param sessionKey 会话键
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void delete(SessionKey sessionKey) {
|
||||||
|
sessionStore.delete(toKey(sessionKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出已存储的会话键。
|
||||||
|
*
|
||||||
|
* @return 会话键列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Set<SessionKey> listSessionKeys() {
|
||||||
|
return sessionStore.listSessionKeys().stream()
|
||||||
|
.map(RuntimeSessionKey::new)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将持久化策略转换为 AgentScope 持久化标记。
|
||||||
|
*
|
||||||
|
* @param policy 运行时策略
|
||||||
|
* @return AgentScope 状态持久化配置
|
||||||
|
*/
|
||||||
|
public static StatePersistence toStatePersistence(AgentPersistencePolicy policy) {
|
||||||
|
if (policy == null || !policy.isEnabled()) {
|
||||||
|
return StatePersistence.memoryOnly();
|
||||||
|
}
|
||||||
|
return StatePersistence.builder()
|
||||||
|
.memoryManaged(policy.isMemoryManaged())
|
||||||
|
.toolkitManaged(policy.isToolkitManaged())
|
||||||
|
.planNotebookManaged(policy.isPlanNotebookManaged())
|
||||||
|
.statefulToolsManaged(policy.isStatefulToolsManaged())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建会话键。
|
||||||
|
*
|
||||||
|
* @param value 键值
|
||||||
|
* @return 会话键
|
||||||
|
*/
|
||||||
|
public static SessionKey sessionKey(String value) {
|
||||||
|
return new RuntimeSessionKey(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toKey(SessionKey sessionKey) {
|
||||||
|
return sessionKey == null ? "" : sessionKey.toIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时支撑的 AgentScope 会话键。
|
||||||
|
*/
|
||||||
|
private record RuntimeSessionKey(String value) implements SessionKey {
|
||||||
|
/**
|
||||||
|
* 将键转换为稳定标识符。
|
||||||
|
*
|
||||||
|
* @return 标识符
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toIdentifier() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
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.List;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将运行时 Skill 适配为 AgentScope SkillBox。
|
||||||
|
*/
|
||||||
|
public class AgentScopeSkillAdapter implements AgentSkillCompiler<AgentSkill> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AgentSkill compile(AgentSkillSpec skillSpec) {
|
||||||
|
validateSkillSpec(skillSpec);
|
||||||
|
return new EasyAgentsAgentSkill(
|
||||||
|
skillSpec.getSkillId(),
|
||||||
|
skillSpec.getName(),
|
||||||
|
skillSpec.getDescription(),
|
||||||
|
skillSpec.getSkillContent(),
|
||||||
|
skillSpec.getResources(),
|
||||||
|
skillSpec.getSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建并绑定 AgentScope SkillBox。
|
||||||
|
*
|
||||||
|
* @param spec SkillBox 声明
|
||||||
|
* @param toolkit Toolkit 实例
|
||||||
|
* @return SkillBox;未配置 Skill 时返回 null
|
||||||
|
*/
|
||||||
|
public SkillBox createSkillBox(AgentSkillBoxSpec spec, Toolkit toolkit) {
|
||||||
|
if (spec == null || spec.getSkills().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
SkillBox skillBox = spec.getSkillBoxId() == null || spec.getSkillBoxId().isEmpty()
|
||||||
|
? new SkillBox(toolkit)
|
||||||
|
: new SkillBox(toolkit, spec.getSkillBoxId());
|
||||||
|
skillBox.setExposeAllSkillMetadata(spec.isExposeAllSkillMetadata());
|
||||||
|
for (AgentSkillSpec skillSpec : spec.getSkills()) {
|
||||||
|
AgentSkill skill = compile(skillSpec);
|
||||||
|
SkillBox.SkillRegistration registration = skillBox.registration()
|
||||||
|
.skill(skill)
|
||||||
|
.toolkit(toolkit)
|
||||||
|
.enableTools(spec.getEnabledToolNames())
|
||||||
|
.disableTools(spec.getDisabledToolNames())
|
||||||
|
.presetParameters(spec.getPresetParameters());
|
||||||
|
registration.apply();
|
||||||
|
}
|
||||||
|
skillBox.syncToolGroupStates();
|
||||||
|
return skillBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建并绑定带 Skill 工具的 AgentScope SkillBox。
|
||||||
|
*
|
||||||
|
* @param spec SkillBox 声明
|
||||||
|
* @param toolkit Toolkit 实例
|
||||||
|
* @param skillTools 按 Skill ID 分组的工具
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
SkillBox skillBox = spec.getSkillBoxId() == null || spec.getSkillBoxId().isEmpty()
|
||||||
|
? 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());
|
||||||
|
List<McpSkillRegistration> mcpRegistrations = mcpBySkill.remove(skillSpec.getSkillId());
|
||||||
|
if (mcpRegistrations == null) {
|
||||||
|
mcpRegistrations = List.of();
|
||||||
|
}
|
||||||
|
if (tools.isEmpty() && mcpRegistrations.isEmpty()) {
|
||||||
|
skillBox.registration()
|
||||||
|
.skill(skill)
|
||||||
|
.toolkit(toolkit)
|
||||||
|
.enableTools(spec.getEnabledToolNames())
|
||||||
|
.disableTools(spec.getDisabledToolNames())
|
||||||
|
.presetParameters(spec.getPresetParameters())
|
||||||
|
.apply();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (AgentTool tool : tools) {
|
||||||
|
skillBox.registration()
|
||||||
|
.skill(skill)
|
||||||
|
.toolkit(toolkit)
|
||||||
|
.enableTools(spec.getEnabledToolNames())
|
||||||
|
.disableTools(spec.getDisabledToolNames())
|
||||||
|
.presetParameters(spec.getPresetParameters())
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,638 @@
|
|||||||
|
package com.easyagents.agent.runtime.agentscope;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||||
|
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;
|
||||||
|
import com.easyagents.agent.runtime.skill.AgentSkillBinding;
|
||||||
|
import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext;
|
||||||
|
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 io.agentscope.core.message.TextBlock;
|
||||||
|
import io.agentscope.core.message.ToolResultBlock;
|
||||||
|
import io.agentscope.core.message.ToolUseBlock;
|
||||||
|
import io.agentscope.core.tool.AgentTool;
|
||||||
|
import io.agentscope.core.tool.ToolCallParam;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.publisher.Sinks;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将运行时工具适配为 AgentScope 工具。
|
||||||
|
*/
|
||||||
|
public class AgentScopeToolAdapter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将运行时工具声明和调用器转换为 AgentScope AgentTool。
|
||||||
|
*
|
||||||
|
* @param toolSpec 工具声明
|
||||||
|
* @param invoker 工具调用器
|
||||||
|
* @param request 运行请求
|
||||||
|
* @return AgentScope 工具
|
||||||
|
*/
|
||||||
|
public AgentTool adapt(AgentToolSpec toolSpec, AgentToolInvoker invoker, AgentRuntimeExecutionContext request) {
|
||||||
|
return adapt(toolSpec, invoker, request, AgentToolApprovalCoordinator.disabled(),
|
||||||
|
(Sinks.Many<AgentRuntimeEvent>) null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将运行时工具声明和调用器转换为带事件 sink 的 AgentScope AgentTool。
|
||||||
|
*
|
||||||
|
* @param toolSpec 工具声明
|
||||||
|
* @param invoker 工具调用器
|
||||||
|
* @param request 运行请求
|
||||||
|
* @param eventSink 事件 sink
|
||||||
|
* @return AgentScope 工具
|
||||||
|
*/
|
||||||
|
public AgentTool adapt(AgentToolSpec toolSpec,
|
||||||
|
AgentToolInvoker invoker,
|
||||||
|
AgentRuntimeExecutionContext request,
|
||||||
|
Sinks.Many<AgentRuntimeEvent> eventSink) {
|
||||||
|
return adapt(toolSpec, invoker, request, AgentToolApprovalCoordinator.disabled(), fixedHolder(request, eventSink),
|
||||||
|
null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将运行时工具声明和调用器转换为带审批协调器和事件 sink 的 AgentScope AgentTool。
|
||||||
|
*
|
||||||
|
* @param toolSpec 工具声明
|
||||||
|
* @param invoker 工具调用器
|
||||||
|
* @param request 运行请求
|
||||||
|
* @param approvalCoordinator 审批协调器
|
||||||
|
* @param eventSink 事件 sink
|
||||||
|
* @return AgentScope 工具
|
||||||
|
*/
|
||||||
|
public AgentTool adapt(AgentToolSpec toolSpec,
|
||||||
|
AgentToolInvoker invoker,
|
||||||
|
AgentRuntimeExecutionContext request,
|
||||||
|
AgentToolApprovalCoordinator approvalCoordinator,
|
||||||
|
Sinks.Many<AgentRuntimeEvent> eventSink) {
|
||||||
|
return adapt(toolSpec, invoker, request, approvalCoordinator, fixedHolder(request, eventSink), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将运行时工具声明和调用器转换为带 Skill 归属的 AgentScope AgentTool。
|
||||||
|
*
|
||||||
|
* @param toolSpec 工具声明
|
||||||
|
* @param invoker 工具调用器
|
||||||
|
* @param request 运行请求
|
||||||
|
* @param approvalCoordinator 审批协调器
|
||||||
|
* @param eventSink 事件 sink
|
||||||
|
* @param skillBinding Skill 绑定关系
|
||||||
|
* @return AgentScope 工具
|
||||||
|
*/
|
||||||
|
public AgentTool adapt(AgentToolSpec toolSpec,
|
||||||
|
AgentToolInvoker invoker,
|
||||||
|
AgentRuntimeExecutionContext request,
|
||||||
|
AgentToolApprovalCoordinator approvalCoordinator,
|
||||||
|
Sinks.Many<AgentRuntimeEvent> eventSink,
|
||||||
|
AgentSkillBinding skillBinding) {
|
||||||
|
return adapt(toolSpec, invoker, request, approvalCoordinator, fixedHolder(request, eventSink), null, skillBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将运行时工具声明和调用器转换为带 Skill 上下文的 AgentScope AgentTool。
|
||||||
|
*
|
||||||
|
* @param toolSpec 工具声明
|
||||||
|
* @param invoker 工具调用器
|
||||||
|
* @param request 运行请求
|
||||||
|
* @param approvalCoordinator 审批协调器
|
||||||
|
* @param eventSink 事件 sink
|
||||||
|
* @param skillContext Skill 运行时上下文
|
||||||
|
* @param skillBinding Skill 静态绑定关系
|
||||||
|
* @return AgentScope 工具
|
||||||
|
*/
|
||||||
|
public AgentTool adapt(AgentToolSpec toolSpec,
|
||||||
|
AgentToolInvoker invoker,
|
||||||
|
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, 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,
|
||||||
|
AgentRuntimeExecutionContext request,
|
||||||
|
AgentToolApprovalCoordinator approvalCoordinator,
|
||||||
|
AgentRuntimeTurnContextHolder turnContextHolder,
|
||||||
|
AgentSkillRuntimeContext skillContext,
|
||||||
|
AgentSkillBinding skillBinding,
|
||||||
|
boolean emitNormalToolResult,
|
||||||
|
boolean emitSkillStep,
|
||||||
|
boolean handleApprovalInTool,
|
||||||
|
ClassLoader invocationClassLoader) implements AgentTool {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具名称。
|
||||||
|
*
|
||||||
|
* @return 工具名称
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return toolSpec.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具描述。
|
||||||
|
*
|
||||||
|
* @return 工具描述
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return toolSpec.getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具参数 Schema。
|
||||||
|
*
|
||||||
|
* @return 参数 Schema
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getParameters() {
|
||||||
|
return toolSpec.getParametersSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取输出 Schema。
|
||||||
|
*
|
||||||
|
* @return 输出 Schema
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getOutputSchema() {
|
||||||
|
return toolSpec.getOutputSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用运行时工具。
|
||||||
|
*
|
||||||
|
* @param param 工具调用参数
|
||||||
|
* @return 工具结果块
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Mono<ToolResultBlock> callAsync(ToolCallParam param) {
|
||||||
|
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 (handleApprovalInTool && toolSpec.isApprovalRequired()) {
|
||||||
|
if (approvalCoordinator == null) {
|
||||||
|
throw new AgentRuntimeException("Agent tool approval coordinator is required: " + toolSpec.getName());
|
||||||
|
}
|
||||||
|
Instant expiresAt = Instant.now().plus(
|
||||||
|
toolSpec.getApprovalRequest() != null && toolSpec.getApprovalRequest().getTimeout() != null
|
||||||
|
? toolSpec.getApprovalRequest().getTimeout()
|
||||||
|
: java.time.Duration.ofMinutes(30));
|
||||||
|
AgentPendingState pendingState = approvalCoordinator.register(
|
||||||
|
request.getSessionId(),
|
||||||
|
request.getAgentDefinition().getAgentId(),
|
||||||
|
param != null && param.getToolUseBlock() != null ? param.getToolUseBlock().getId() : null,
|
||||||
|
toolSpec.getName(),
|
||||||
|
approvalPrompt(toolSpec.getApprovalRequest() == null ? null : toolSpec.getApprovalRequest().getApprovalPrompt()),
|
||||||
|
input,
|
||||||
|
toolSpec.getApprovalRequest() == null ? new LinkedHashMap<>() : toolSpec.getApprovalRequest().getMetadata(),
|
||||||
|
expiresAt);
|
||||||
|
AgentRuntimeEvent approvalEvent = toolApprovalRequiredEvent(param == null ? null : param.getToolUseBlock(), pendingState);
|
||||||
|
pendingState.setEventId(approvalEvent.getEventId());
|
||||||
|
emit(approvalEvent);
|
||||||
|
return approvalCoordinator.await(pendingState.getResumeToken())
|
||||||
|
.flatMap(response -> {
|
||||||
|
if (!response.isApproved()) {
|
||||||
|
return Mono.error(new AgentToolApprovalRejectedException(
|
||||||
|
response.getRejectReason()));
|
||||||
|
}
|
||||||
|
return Mono.fromCallable(() -> invokeTool(param, input));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Mono.fromCallable(() -> invokeTool(param, input));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建中立工具上下文。
|
||||||
|
*
|
||||||
|
* @param param 工具调用参数
|
||||||
|
* @return 工具上下文
|
||||||
|
*/
|
||||||
|
private AgentToolContext buildContext(ToolCallParam param) {
|
||||||
|
AgentRuntimeExecutionContext currentRequest = currentRequest();
|
||||||
|
AgentToolContext context = new AgentToolContext();
|
||||||
|
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());
|
||||||
|
context.getMetadata().putAll(toolSpec.getMetadata());
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 AgentToolResult 转换为 AgentScope 结果块。
|
||||||
|
*
|
||||||
|
* @param param 工具调用参数
|
||||||
|
* @param result 运行时结果
|
||||||
|
* @return AgentScope 结果块
|
||||||
|
*/
|
||||||
|
private ToolResultBlock toToolResultBlock(ToolCallParam param, AgentToolResult result) {
|
||||||
|
AgentToolResult safeResult = result == null ? AgentToolResult.failure("Tool returned null result.") : result;
|
||||||
|
Map<String, Object> metadata = new LinkedHashMap<>(safeResult.getMetadata());
|
||||||
|
metadata.put("success", safeResult.isSuccess());
|
||||||
|
metadata.put("visibility", toolSpec.getVisibility().name());
|
||||||
|
metadata.put("displayContent", safeResult.getDisplayContent());
|
||||||
|
appendSkillPayload(metadata, activeSkillBinding());
|
||||||
|
String output = safeResult.getModelContent();
|
||||||
|
if (output == null || output.isEmpty()) {
|
||||||
|
output = safeResult.isSuccess() ? "" : safeResult.getErrorMessage();
|
||||||
|
}
|
||||||
|
String id = param != null && param.getToolUseBlock() != null ? param.getToolUseBlock().getId() : null;
|
||||||
|
return ToolResultBlock.of(id, toolSpec.getName(), TextBlock.builder().text(output).build(), metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行真实工具。
|
||||||
|
*
|
||||||
|
* @param param 工具调用参数
|
||||||
|
* @param input 工具输入
|
||||||
|
* @return 工具结果块
|
||||||
|
*/
|
||||||
|
private ToolResultBlock invokeTool(ToolCallParam param, Map<String, Object> input) {
|
||||||
|
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 Skill 步骤事件,普通工具返回 null
|
||||||
|
*/
|
||||||
|
private AgentRuntimeEvent toolExecutionStartEvent(ToolUseBlock block) {
|
||||||
|
AgentSkillBinding activeBinding = activeSkillBinding();
|
||||||
|
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());
|
||||||
|
event.getPayload().put("toolName", block.getName());
|
||||||
|
event.getPayload().put("input", block.getInput());
|
||||||
|
event.getPayload().put("content", block.getContent());
|
||||||
|
event.getMetadata().putAll(block.getMetadata() == null ? new LinkedHashMap<>() : block.getMetadata());
|
||||||
|
} else {
|
||||||
|
event.getPayload().put("name", toolSpec.getName());
|
||||||
|
event.getPayload().put("toolName", toolSpec.getName());
|
||||||
|
}
|
||||||
|
if (activeBinding != null) {
|
||||||
|
appendSkillPayload(event.getPayload(), activeBinding);
|
||||||
|
event.getPayload().put("stepType", "TOOL_CALL");
|
||||||
|
event.getPayload().put("stepName", toolSpec.getName());
|
||||||
|
event.getPayload().put("status", "RUNNING");
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建工具结果事件。
|
||||||
|
*
|
||||||
|
* @param block 工具结果块
|
||||||
|
* @return 运行时事件
|
||||||
|
*/
|
||||||
|
private AgentRuntimeEvent toolResultEvent(ToolResultBlock block) {
|
||||||
|
AgentSkillBinding activeBinding = activeSkillBinding();
|
||||||
|
AgentRuntimeEvent event = baseEvent(activeBinding == null
|
||||||
|
? AgentRuntimeEventType.TOOL_RESULT
|
||||||
|
: AgentRuntimeEventType.SKILL_STEP);
|
||||||
|
if (block != null) {
|
||||||
|
event.setToolCallId(block.getId());
|
||||||
|
event.getPayload().put("name", block.getName());
|
||||||
|
event.getPayload().put("toolName", block.getName());
|
||||||
|
event.getPayload().put("text", block.toString());
|
||||||
|
event.getPayload().put("suspended", block.isSuspended());
|
||||||
|
event.getMetadata().putAll(block.getMetadata() == null ? new LinkedHashMap<>() : block.getMetadata());
|
||||||
|
}
|
||||||
|
if (activeBinding != null) {
|
||||||
|
appendSkillPayload(event.getPayload(), activeBinding);
|
||||||
|
event.getPayload().put("stepType", "TOOL_RESULT");
|
||||||
|
event.getPayload().put("stepName", toolSpec.getName());
|
||||||
|
event.getPayload().put("status", success(event) ? "SUCCESS" : "FAILED");
|
||||||
|
event.getPayload().put("success", success(event));
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据挂起的工具结果构建工具审批事件。
|
||||||
|
*
|
||||||
|
* @param toolUseBlock 工具使用块
|
||||||
|
* @param resultBlock 工具结果块
|
||||||
|
* @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());
|
||||||
|
}
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
payload.put("resumeToken", pendingState == null || pendingState.getResumeToken() == null
|
||||||
|
? UUID.randomUUID().toString()
|
||||||
|
: pendingState.getResumeToken().getValue());
|
||||||
|
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());
|
||||||
|
payload.put("toolName", pendingState == null ? toolSpec.getName() : pendingState.getToolName());
|
||||||
|
payload.put("expiresAt", pendingState == null || pendingState.getExpiresAt() == null ? null : pendingState.getExpiresAt().toString());
|
||||||
|
appendSkillPayload(payload, activeSkillBinding());
|
||||||
|
if (toolUseBlock != null) {
|
||||||
|
payload.put("toolCallId", toolUseBlock.getId());
|
||||||
|
payload.put("toolName", toolUseBlock.getName());
|
||||||
|
payload.put("input", toolUseBlock.getInput());
|
||||||
|
}
|
||||||
|
event.setPayload(payload);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析审批文案。
|
||||||
|
*
|
||||||
|
* @param approvalPrompt 审批文案
|
||||||
|
* @return 审批文案
|
||||||
|
*/
|
||||||
|
private String approvalPrompt(String approvalPrompt) {
|
||||||
|
if (approvalPrompt != null && !approvalPrompt.isBlank()) {
|
||||||
|
return approvalPrompt;
|
||||||
|
}
|
||||||
|
return "是否批准执行该工具?";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建带请求身份信息的事件。
|
||||||
|
*
|
||||||
|
* @param type 事件类型
|
||||||
|
* @return 运行时事件
|
||||||
|
*/
|
||||||
|
private AgentRuntimeEvent baseEvent(AgentRuntimeEventType type) {
|
||||||
|
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());
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 追加 Skill 归属字段。
|
||||||
|
*
|
||||||
|
* @param target 目标 Map
|
||||||
|
* @param binding Skill 绑定关系
|
||||||
|
*/
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前已激活的 Skill 绑定关系。
|
||||||
|
*
|
||||||
|
* @return 已激活绑定关系
|
||||||
|
*/
|
||||||
|
private AgentSkillBinding activeSkillBinding() {
|
||||||
|
if (skillBinding == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (skillContext == null) {
|
||||||
|
return skillBinding;
|
||||||
|
}
|
||||||
|
return skillContext.getActiveToolBinding(toolSpec.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从事件元数据判断工具结果是否成功。
|
||||||
|
*
|
||||||
|
* @param event 运行时事件
|
||||||
|
* @return 成功时为 true
|
||||||
|
*/
|
||||||
|
private boolean success(AgentRuntimeEvent event) {
|
||||||
|
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,236 @@
|
|||||||
|
package com.easyagents.agent.runtime.event;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.message.AgentMessage;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体执行过程中的中立运行时事件。
|
||||||
|
*/
|
||||||
|
public class AgentRuntimeEvent {
|
||||||
|
|
||||||
|
private String eventId = UUID.randomUUID().toString();
|
||||||
|
private AgentRuntimeEventType eventType;
|
||||||
|
private String traceId;
|
||||||
|
private String sessionId;
|
||||||
|
private String agentId;
|
||||||
|
private String messageId;
|
||||||
|
private String toolCallId;
|
||||||
|
private AgentMessage message;
|
||||||
|
private Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
private Instant createdAt = Instant.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建指定类型的事件。
|
||||||
|
*
|
||||||
|
* @param eventType 事件类型
|
||||||
|
* @return 新事件
|
||||||
|
*/
|
||||||
|
public static AgentRuntimeEvent of(AgentRuntimeEventType eventType) {
|
||||||
|
AgentRuntimeEvent event = new AgentRuntimeEvent();
|
||||||
|
event.setEventType(eventType);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件ID。
|
||||||
|
*
|
||||||
|
* @return 事件ID
|
||||||
|
*/
|
||||||
|
public String getEventId() {
|
||||||
|
return eventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置事件ID。
|
||||||
|
*
|
||||||
|
* @param eventId 事件ID
|
||||||
|
*/
|
||||||
|
public void setEventId(String eventId) {
|
||||||
|
this.eventId = eventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件类型。
|
||||||
|
*
|
||||||
|
* @return 事件类型
|
||||||
|
*/
|
||||||
|
public AgentRuntimeEventType getEventType() {
|
||||||
|
return eventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置事件类型。
|
||||||
|
*
|
||||||
|
* @param eventType 事件类型
|
||||||
|
*/
|
||||||
|
public void setEventType(AgentRuntimeEventType eventType) {
|
||||||
|
this.eventType = eventType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取链路ID。
|
||||||
|
*
|
||||||
|
* @return 链路ID
|
||||||
|
*/
|
||||||
|
public String getTraceId() {
|
||||||
|
return traceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置链路ID。
|
||||||
|
*
|
||||||
|
* @param traceId 链路ID
|
||||||
|
*/
|
||||||
|
public void setTraceId(String traceId) {
|
||||||
|
this.traceId = traceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话ID。
|
||||||
|
*
|
||||||
|
* @return 会话ID
|
||||||
|
*/
|
||||||
|
public String getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置会话ID。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话ID
|
||||||
|
*/
|
||||||
|
public void setSessionId(String sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能体ID。
|
||||||
|
*
|
||||||
|
* @return 智能体ID
|
||||||
|
*/
|
||||||
|
public String getAgentId() {
|
||||||
|
return agentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置智能体ID。
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
*/
|
||||||
|
public void setAgentId(String agentId) {
|
||||||
|
this.agentId = agentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息ID。
|
||||||
|
*
|
||||||
|
* @return 消息ID
|
||||||
|
*/
|
||||||
|
public String getMessageId() {
|
||||||
|
return messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置消息ID。
|
||||||
|
*
|
||||||
|
* @param messageId 消息ID
|
||||||
|
*/
|
||||||
|
public void setMessageId(String messageId) {
|
||||||
|
this.messageId = messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具调用ID。
|
||||||
|
*
|
||||||
|
* @return 工具调用ID
|
||||||
|
*/
|
||||||
|
public String getToolCallId() {
|
||||||
|
return toolCallId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置工具调用ID。
|
||||||
|
*
|
||||||
|
* @param toolCallId 工具调用ID
|
||||||
|
*/
|
||||||
|
public void setToolCallId(String toolCallId) {
|
||||||
|
this.toolCallId = toolCallId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取结构化消息。
|
||||||
|
*
|
||||||
|
* @return 结构化消息
|
||||||
|
*/
|
||||||
|
public AgentMessage getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置结构化消息。
|
||||||
|
*
|
||||||
|
* @param message 结构化消息
|
||||||
|
*/
|
||||||
|
public void setMessage(AgentMessage message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取载荷。
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取创建时间。
|
||||||
|
*
|
||||||
|
* @return 创建时间
|
||||||
|
*/
|
||||||
|
public Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置创建时间。
|
||||||
|
*
|
||||||
|
* @param createdAt 创建时间
|
||||||
|
*/
|
||||||
|
public void setCreatedAt(Instant createdAt) {
|
||||||
|
this.createdAt = createdAt == null ? Instant.now() : createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.easyagents.agent.runtime.event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体运行输出的运行时事件类型。
|
||||||
|
*/
|
||||||
|
public enum AgentRuntimeEventType {
|
||||||
|
/**
|
||||||
|
* 智能体运行已开始。
|
||||||
|
*/
|
||||||
|
STARTED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式推理内容,用于展示思考过程。
|
||||||
|
*/
|
||||||
|
REASONING_DELTA,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体开始一次模型推理。
|
||||||
|
*/
|
||||||
|
REASONING_STARTED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体完成一次模型推理。
|
||||||
|
*/
|
||||||
|
REASONING_COMPLETED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流式输出内容,用于流式展示聊天内容。
|
||||||
|
*/
|
||||||
|
MESSAGE_DELTA,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体发起工具调用。
|
||||||
|
*/
|
||||||
|
TOOL_CALL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具调用完成并返回结果。
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具执行前需要人工审批。
|
||||||
|
*/
|
||||||
|
TOOL_APPROVAL_REQUIRED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体开始加载并调用 Skill。
|
||||||
|
*/
|
||||||
|
SKILL_CALL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skill 内部执行步骤。
|
||||||
|
*/
|
||||||
|
SKILL_STEP,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skill 加载或调用完成。
|
||||||
|
*/
|
||||||
|
SKILL_RESULT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skill 加载或调用失败。
|
||||||
|
*/
|
||||||
|
SKILL_FAILED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体处理完成。
|
||||||
|
*/
|
||||||
|
COMPLETED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体运行已暂停,等待外部输入后继续。
|
||||||
|
*/
|
||||||
|
SUSPENDED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体运行失败。
|
||||||
|
*/
|
||||||
|
FAILED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体运行被取消。
|
||||||
|
*/
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
package com.easyagents.agent.runtime.hitl;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待工具执行审批的状态。
|
||||||
|
*/
|
||||||
|
public class AgentPendingState {
|
||||||
|
|
||||||
|
private AgentResumeToken resumeToken = AgentResumeToken.create();
|
||||||
|
private String sessionId;
|
||||||
|
private String agentId;
|
||||||
|
private String eventId;
|
||||||
|
private String toolCallId;
|
||||||
|
private String toolName;
|
||||||
|
private String approvalPrompt;
|
||||||
|
private Map<String, Object> toolInput = new LinkedHashMap<>();
|
||||||
|
private Instant expiresAt;
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取恢复令牌。
|
||||||
|
*
|
||||||
|
* @return 恢复令牌
|
||||||
|
*/
|
||||||
|
public AgentResumeToken getResumeToken() {
|
||||||
|
return resumeToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置恢复令牌。
|
||||||
|
*
|
||||||
|
* @param resumeToken 恢复令牌
|
||||||
|
*/
|
||||||
|
public void setResumeToken(AgentResumeToken resumeToken) {
|
||||||
|
this.resumeToken = resumeToken == null ? AgentResumeToken.create() : resumeToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话ID。
|
||||||
|
*
|
||||||
|
* @return 会话ID
|
||||||
|
*/
|
||||||
|
public String getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置会话ID。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话ID
|
||||||
|
*/
|
||||||
|
public void setSessionId(String sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能体ID。
|
||||||
|
*
|
||||||
|
* @return 智能体ID
|
||||||
|
*/
|
||||||
|
public String getAgentId() {
|
||||||
|
return agentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置智能体ID。
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
*/
|
||||||
|
public void setAgentId(String agentId) {
|
||||||
|
this.agentId = agentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件ID。
|
||||||
|
*
|
||||||
|
* @return 事件ID
|
||||||
|
*/
|
||||||
|
public String getEventId() {
|
||||||
|
return eventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置事件ID。
|
||||||
|
*
|
||||||
|
* @param eventId 事件ID
|
||||||
|
*/
|
||||||
|
public void setEventId(String eventId) {
|
||||||
|
this.eventId = eventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具调用ID。
|
||||||
|
*
|
||||||
|
* @return 工具调用ID
|
||||||
|
*/
|
||||||
|
public String getToolCallId() {
|
||||||
|
return toolCallId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置工具调用ID。
|
||||||
|
*
|
||||||
|
* @param toolCallId 工具调用ID
|
||||||
|
*/
|
||||||
|
public void setToolCallId(String toolCallId) {
|
||||||
|
this.toolCallId = toolCallId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具名称。
|
||||||
|
*
|
||||||
|
* @return 工具名称
|
||||||
|
*/
|
||||||
|
public String getToolName() {
|
||||||
|
return toolName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置工具名称。
|
||||||
|
*
|
||||||
|
* @param toolName 工具名称
|
||||||
|
*/
|
||||||
|
public void setToolName(String toolName) {
|
||||||
|
this.toolName = toolName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审批提示文案。
|
||||||
|
*
|
||||||
|
* @return 审批提示文案
|
||||||
|
*/
|
||||||
|
public String getApprovalPrompt() {
|
||||||
|
return approvalPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置审批提示文案。
|
||||||
|
*
|
||||||
|
* @param approvalPrompt 审批提示文案
|
||||||
|
*/
|
||||||
|
public void setApprovalPrompt(String approvalPrompt) {
|
||||||
|
this.approvalPrompt = approvalPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具调用参数。
|
||||||
|
*
|
||||||
|
* @return 工具调用参数
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getToolInput() {
|
||||||
|
return toolInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置工具调用参数。
|
||||||
|
*
|
||||||
|
* @param toolInput 工具调用参数
|
||||||
|
*/
|
||||||
|
public void setToolInput(Map<String, Object> toolInput) {
|
||||||
|
this.toolInput = toolInput == null ? new LinkedHashMap<>() : toolInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取过期时间。
|
||||||
|
*
|
||||||
|
* @return 过期时间
|
||||||
|
*/
|
||||||
|
public Instant getExpiresAt() {
|
||||||
|
return expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置过期时间。
|
||||||
|
*
|
||||||
|
* @param expiresAt 过期时间
|
||||||
|
*/
|
||||||
|
public void setExpiresAt(Instant expiresAt) {
|
||||||
|
this.expiresAt = expiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @return 元数据
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getMetadata() {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置元数据。
|
||||||
|
*
|
||||||
|
* @param metadata 元数据
|
||||||
|
*/
|
||||||
|
public void setMetadata(Map<String, Object> metadata) {
|
||||||
|
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.easyagents.agent.runtime.hitl;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于恢复挂起运行的令牌。
|
||||||
|
*/
|
||||||
|
public class AgentResumeToken {
|
||||||
|
|
||||||
|
private String value = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建a new token。
|
||||||
|
*
|
||||||
|
* @return 令牌
|
||||||
|
*/
|
||||||
|
public static AgentResumeToken create() {
|
||||||
|
return new AgentResumeToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取令牌值。
|
||||||
|
*
|
||||||
|
* @return 令牌值
|
||||||
|
*/
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置令牌值。
|
||||||
|
*
|
||||||
|
* @param value 令牌值
|
||||||
|
*/
|
||||||
|
public void setValue(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.easyagents.agent.runtime.hitl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已挂起的运行时状态。
|
||||||
|
*/
|
||||||
|
public class AgentSuspendState {
|
||||||
|
|
||||||
|
private AgentPendingState pendingState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取挂起状态。
|
||||||
|
*
|
||||||
|
* @return 挂起状态
|
||||||
|
*/
|
||||||
|
public AgentPendingState getPendingState() {
|
||||||
|
return pendingState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置挂起状态。
|
||||||
|
*
|
||||||
|
* @param pendingState 挂起状态
|
||||||
|
*/
|
||||||
|
public void setPendingState(AgentPendingState pendingState) {
|
||||||
|
this.pendingState = pendingState;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,714 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具审批协调器。
|
||||||
|
*/
|
||||||
|
public class AgentToolApprovalCoordinator {
|
||||||
|
|
||||||
|
/** MCP 工具类型。 */
|
||||||
|
private static final String MCP_TOOL_TYPE = "MCP";
|
||||||
|
|
||||||
|
/** 是否启用内存审批协调。 */
|
||||||
|
private final boolean enabled;
|
||||||
|
/** 恢复令牌到待审批项的索引。 */
|
||||||
|
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<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建已启用的协调器。
|
||||||
|
*
|
||||||
|
* @return 已启用的协调器
|
||||||
|
*/
|
||||||
|
public static AgentToolApprovalCoordinator enabled() {
|
||||||
|
return new AgentToolApprovalCoordinator(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建禁用的协调器。
|
||||||
|
*
|
||||||
|
* @return 禁用的协调器
|
||||||
|
*/
|
||||||
|
public static AgentToolApprovalCoordinator disabled() {
|
||||||
|
return new AgentToolApprovalCoordinator(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建协调器。
|
||||||
|
*
|
||||||
|
* @param enabled 是否启用
|
||||||
|
*/
|
||||||
|
public AgentToolApprovalCoordinator(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册一个待审批请求。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话ID
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
* @param toolCallId 工具调用ID
|
||||||
|
* @param toolName 工具名称
|
||||||
|
* @param approvalPrompt 审批文案
|
||||||
|
* @param toolInput 工具入参
|
||||||
|
* @param metadata 元数据
|
||||||
|
* @param expiresAt 过期时间
|
||||||
|
* @return 待审批状态
|
||||||
|
*/
|
||||||
|
public AgentPendingState register(String sessionId,
|
||||||
|
String agentId,
|
||||||
|
String toolCallId,
|
||||||
|
String toolName,
|
||||||
|
String approvalPrompt,
|
||||||
|
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);
|
||||||
|
state.setToolCallId(toolCallId);
|
||||||
|
state.setToolName(toolName);
|
||||||
|
state.setApprovalPrompt(approvalPrompt);
|
||||||
|
state.setToolInput(toolInput);
|
||||||
|
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) {
|
||||||
|
PendingApproval pendingApproval = new PendingApproval(
|
||||||
|
state, effectiveBatchId, new CompletableFuture<>());
|
||||||
|
approvals.put(token, pendingApproval);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待指定审批令牌的响应。
|
||||||
|
*
|
||||||
|
* @param resumeToken 恢复令牌
|
||||||
|
* @return 恢复请求
|
||||||
|
*/
|
||||||
|
public synchronized Mono<AgentResumeRequest> await(AgentResumeToken resumeToken) {
|
||||||
|
if (!enabled) {
|
||||||
|
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("缺少审批令牌。"));
|
||||||
|
}
|
||||||
|
PendingApproval pendingApproval = approvals.get(resumeToken.getValue());
|
||||||
|
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 request 恢复请求
|
||||||
|
* @return 审批决议
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消所有挂起审批。
|
||||||
|
*
|
||||||
|
* @param reason 取消原因
|
||||||
|
*/
|
||||||
|
public synchronized void cancelAll(String reason) {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String batchId : new ArrayList<>(approvalBatches.keySet())) {
|
||||||
|
closeBatch(batchId, reason);
|
||||||
|
}
|
||||||
|
approvals.clear();
|
||||||
|
tokensByToolCallId.clear();
|
||||||
|
executionAuthorizations.clear();
|
||||||
|
reusableApprovalScopes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否已启用。
|
||||||
|
*
|
||||||
|
* @return 启用标记
|
||||||
|
*/
|
||||||
|
public boolean isEnabled() {
|
||||||
|
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 String batchId;
|
||||||
|
private final CompletableFuture<AgentResumeRequest> future;
|
||||||
|
private ApprovalDecision decision = ApprovalDecision.PENDING;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建待审批项。
|
||||||
|
*
|
||||||
|
* @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,30 @@
|
|||||||
|
package com.easyagents.agent.runtime.hitl;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具审批被拒绝时抛出的异常。
|
||||||
|
*/
|
||||||
|
public class AgentToolApprovalRejectedException extends AgentRuntimeException {
|
||||||
|
|
||||||
|
private final String rejectReason;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建工具审批拒绝异常。
|
||||||
|
*
|
||||||
|
* @param rejectReason 拒绝原因
|
||||||
|
*/
|
||||||
|
public AgentToolApprovalRejectedException(String rejectReason) {
|
||||||
|
super(rejectReason == null || rejectReason.isBlank() ? "工具执行已被拒绝。" : rejectReason);
|
||||||
|
this.rejectReason = rejectReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取拒绝原因。
|
||||||
|
*
|
||||||
|
* @return 拒绝原因
|
||||||
|
*/
|
||||||
|
public String getRejectReason() {
|
||||||
|
return rejectReason;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.easyagents.agent.runtime.hitl;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具执行审批请求。
|
||||||
|
*/
|
||||||
|
public class AgentToolApprovalRequest {
|
||||||
|
|
||||||
|
private String approvalPrompt;
|
||||||
|
private Duration timeout = Duration.ofMinutes(30);
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审批提示文案。
|
||||||
|
*
|
||||||
|
* @return 审批提示文案
|
||||||
|
*/
|
||||||
|
public String getApprovalPrompt() {
|
||||||
|
return approvalPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置审批提示文案。
|
||||||
|
*
|
||||||
|
* @param approvalPrompt 审批提示文案
|
||||||
|
*/
|
||||||
|
public void setApprovalPrompt(String approvalPrompt) {
|
||||||
|
this.approvalPrompt = approvalPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审批等待超时时间。
|
||||||
|
*
|
||||||
|
* @return 超时时间
|
||||||
|
*/
|
||||||
|
public Duration getTimeout() {
|
||||||
|
return timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置审批等待超时时间。
|
||||||
|
*
|
||||||
|
* @param timeout 超时时间
|
||||||
|
*/
|
||||||
|
public void setTimeout(Duration timeout) {
|
||||||
|
this.timeout = timeout == null ? Duration.ofMinutes(30) : timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审批元数据。
|
||||||
|
*
|
||||||
|
* @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,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package com.easyagents.agent.runtime.knowledge;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保留元数据的知识库检索文档。
|
||||||
|
*/
|
||||||
|
public class AgentKnowledgeDocument {
|
||||||
|
|
||||||
|
private String documentId;
|
||||||
|
private String documentName;
|
||||||
|
private String chunkId;
|
||||||
|
private String content;
|
||||||
|
private String sourceUri;
|
||||||
|
private Double score;
|
||||||
|
private Map<String, Object> knowledgeMetadata = new LinkedHashMap<>();
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文档ID。
|
||||||
|
*
|
||||||
|
* @return 文档ID
|
||||||
|
*/
|
||||||
|
public String getDocumentId() {
|
||||||
|
return documentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置文档ID。
|
||||||
|
*
|
||||||
|
* @param documentId 文档ID
|
||||||
|
*/
|
||||||
|
public void setDocumentId(String documentId) {
|
||||||
|
this.documentId = documentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文档名称。
|
||||||
|
*
|
||||||
|
* @return 文档名称
|
||||||
|
*/
|
||||||
|
public String getDocumentName() {
|
||||||
|
return documentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置文档名称。
|
||||||
|
*
|
||||||
|
* @param documentName 文档名称
|
||||||
|
*/
|
||||||
|
public void setDocumentName(String documentName) {
|
||||||
|
this.documentName = documentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分片ID。
|
||||||
|
*
|
||||||
|
* @return 分片ID
|
||||||
|
*/
|
||||||
|
public String getChunkId() {
|
||||||
|
return chunkId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置分片ID。
|
||||||
|
*
|
||||||
|
* @param chunkId 分片ID
|
||||||
|
*/
|
||||||
|
public void setChunkId(String chunkId) {
|
||||||
|
this.chunkId = chunkId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取内容。
|
||||||
|
*
|
||||||
|
* @return 内容
|
||||||
|
*/
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置内容。
|
||||||
|
*
|
||||||
|
* @param content 内容
|
||||||
|
*/
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取来源 URI。
|
||||||
|
*
|
||||||
|
* @return 来源 URI
|
||||||
|
*/
|
||||||
|
public String getSourceUri() {
|
||||||
|
return sourceUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置来源 URI。
|
||||||
|
*
|
||||||
|
* @param sourceUri 来源 URI
|
||||||
|
*/
|
||||||
|
public void setSourceUri(String sourceUri) {
|
||||||
|
this.sourceUri = sourceUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分数。
|
||||||
|
*
|
||||||
|
* @return 分数
|
||||||
|
*/
|
||||||
|
public Double getScore() {
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置分数。
|
||||||
|
*
|
||||||
|
* @param score 分数
|
||||||
|
*/
|
||||||
|
public void setScore(Double score) {
|
||||||
|
this.score = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库级元数据。
|
||||||
|
*
|
||||||
|
* @return 知识库级元数据
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getKnowledgeMetadata() {
|
||||||
|
return knowledgeMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置知识库级元数据。
|
||||||
|
*
|
||||||
|
* @param knowledgeMetadata 知识库级元数据
|
||||||
|
*/
|
||||||
|
public void setKnowledgeMetadata(Map<String, Object> knowledgeMetadata) {
|
||||||
|
this.knowledgeMetadata = knowledgeMetadata == null ? new LinkedHashMap<>() : knowledgeMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文档级元数据。
|
||||||
|
*
|
||||||
|
* @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,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package com.easyagents.agent.runtime.knowledge;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.AgentRuntimeContext;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 传递给知识库检索器的请求。
|
||||||
|
*/
|
||||||
|
public class AgentKnowledgeRetrievalRequest {
|
||||||
|
|
||||||
|
private String query;
|
||||||
|
private int limit;
|
||||||
|
private double scoreThreshold;
|
||||||
|
private AgentKnowledgeSpec knowledgeSpec;
|
||||||
|
private AgentRuntimeContext runtimeContext = new AgentRuntimeContext();
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取查询。
|
||||||
|
*
|
||||||
|
* @return 查询
|
||||||
|
*/
|
||||||
|
public String getQuery() {
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置查询。
|
||||||
|
*
|
||||||
|
* @param query 查询
|
||||||
|
*/
|
||||||
|
public void setQuery(String query) {
|
||||||
|
this.query = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取检索数量限制。
|
||||||
|
*
|
||||||
|
* @return 检索数量限制
|
||||||
|
*/
|
||||||
|
public int getLimit() {
|
||||||
|
return limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置检索数量限制。
|
||||||
|
*
|
||||||
|
* @param limit 检索数量限制
|
||||||
|
*/
|
||||||
|
public void setLimit(int limit) {
|
||||||
|
this.limit = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分数阈值。
|
||||||
|
*
|
||||||
|
* @return 分数阈值
|
||||||
|
*/
|
||||||
|
public double getScoreThreshold() {
|
||||||
|
return scoreThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置分数阈值。
|
||||||
|
*
|
||||||
|
* @param scoreThreshold 分数阈值
|
||||||
|
*/
|
||||||
|
public void setScoreThreshold(double scoreThreshold) {
|
||||||
|
this.scoreThreshold = scoreThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库声明。
|
||||||
|
*
|
||||||
|
* @return 知识库声明
|
||||||
|
*/
|
||||||
|
public AgentKnowledgeSpec getKnowledgeSpec() {
|
||||||
|
return knowledgeSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置知识库声明。
|
||||||
|
*
|
||||||
|
* @param knowledgeSpec 知识库声明
|
||||||
|
*/
|
||||||
|
public void setKnowledgeSpec(AgentKnowledgeSpec knowledgeSpec) {
|
||||||
|
this.knowledgeSpec = knowledgeSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取运行时上下文。
|
||||||
|
*
|
||||||
|
* @return 运行时上下文
|
||||||
|
*/
|
||||||
|
public AgentRuntimeContext getRuntimeContext() {
|
||||||
|
return runtimeContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置运行时上下文。
|
||||||
|
*
|
||||||
|
* @param runtimeContext 运行时上下文
|
||||||
|
*/
|
||||||
|
public void setRuntimeContext(AgentRuntimeContext runtimeContext) {
|
||||||
|
this.runtimeContext = runtimeContext == null ? new AgentRuntimeContext() : runtimeContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @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,63 @@
|
|||||||
|
package com.easyagents.agent.runtime.knowledge;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库检索结果。
|
||||||
|
*/
|
||||||
|
public class AgentKnowledgeRetrievalResult {
|
||||||
|
|
||||||
|
private List<AgentKnowledgeDocument> documents = new ArrayList<>();
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据检索文档创建结果。
|
||||||
|
*
|
||||||
|
* @param documents 检索文档
|
||||||
|
* @return 检索结果
|
||||||
|
*/
|
||||||
|
public static AgentKnowledgeRetrievalResult of(List<AgentKnowledgeDocument> documents) {
|
||||||
|
AgentKnowledgeRetrievalResult result = new AgentKnowledgeRetrievalResult();
|
||||||
|
result.setDocuments(documents);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文档列表。
|
||||||
|
*
|
||||||
|
* @return 文档列表
|
||||||
|
*/
|
||||||
|
public List<AgentKnowledgeDocument> getDocuments() {
|
||||||
|
return documents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置文档列表。
|
||||||
|
*
|
||||||
|
* @param documents 文档列表
|
||||||
|
*/
|
||||||
|
public void setDocuments(List<AgentKnowledgeDocument> documents) {
|
||||||
|
this.documents = documents == null ? new ArrayList<>() : documents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @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,16 @@
|
|||||||
|
package com.easyagents.agent.runtime.knowledge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从单个知识源检索文档。
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface AgentKnowledgeRetriever {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检索相关文档。
|
||||||
|
*
|
||||||
|
* @param request 检索请求
|
||||||
|
* @return 检索结果
|
||||||
|
*/
|
||||||
|
AgentKnowledgeRetrievalResult retrieve(AgentKnowledgeRetrievalRequest request);
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package com.easyagents.agent.runtime.knowledge;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库声明。
|
||||||
|
*/
|
||||||
|
public class AgentKnowledgeSpec {
|
||||||
|
|
||||||
|
private String knowledgeId;
|
||||||
|
private String runtimeName;
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private int limit = 5;
|
||||||
|
private double scoreThreshold = 0D;
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库ID。
|
||||||
|
*
|
||||||
|
* @return 知识库ID
|
||||||
|
*/
|
||||||
|
public String getKnowledgeId() {
|
||||||
|
return knowledgeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置知识库ID。
|
||||||
|
*
|
||||||
|
* @param knowledgeId 知识库ID
|
||||||
|
*/
|
||||||
|
public void setKnowledgeId(String knowledgeId) {
|
||||||
|
this.knowledgeId = knowledgeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取调用方提供的英文运行名。
|
||||||
|
*
|
||||||
|
* @return 英文运行名
|
||||||
|
*/
|
||||||
|
public String getRuntimeName() {
|
||||||
|
return runtimeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置调用方提供的英文运行名。
|
||||||
|
*
|
||||||
|
* @param runtimeName 英文运行名
|
||||||
|
*/
|
||||||
|
public void setRuntimeName(String runtimeName) {
|
||||||
|
this.runtimeName = runtimeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库名称。
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取限制数量。
|
||||||
|
*
|
||||||
|
* @return 限制数量
|
||||||
|
*/
|
||||||
|
public int getLimit() {
|
||||||
|
return limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置限制数量。
|
||||||
|
*
|
||||||
|
* @param limit 限制数量
|
||||||
|
*/
|
||||||
|
public void setLimit(int limit) {
|
||||||
|
this.limit = limit <= 0 ? 5 : limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分数阈值。
|
||||||
|
*
|
||||||
|
* @return 分数阈值
|
||||||
|
*/
|
||||||
|
public double getScoreThreshold() {
|
||||||
|
return scoreThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置分数阈值。
|
||||||
|
*
|
||||||
|
* @param scoreThreshold 分数阈值
|
||||||
|
*/
|
||||||
|
public void setScoreThreshold(double scoreThreshold) {
|
||||||
|
this.scoreThreshold = Math.max(0D, scoreThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @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,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package com.easyagents.agent.runtime.memory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AutoContext 自动压缩参数。
|
||||||
|
*/
|
||||||
|
public class AgentMemoryCompressionParameter {
|
||||||
|
|
||||||
|
private boolean enabled = true;
|
||||||
|
private Integer msgThreshold;
|
||||||
|
private int lastKeep = 8;
|
||||||
|
private Double tokenRatio;
|
||||||
|
private Long maxToken;
|
||||||
|
private long largePayloadThreshold = 2048L;
|
||||||
|
private Integer minCompressionTokenThreshold;
|
||||||
|
private double currentRoundCompressionRatio = 0.5D;
|
||||||
|
private int minConsecutiveToolMessages = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否启用自动压缩。
|
||||||
|
*
|
||||||
|
* @return 启用时返回 true
|
||||||
|
*/
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置压缩开关。
|
||||||
|
*
|
||||||
|
* @param enabled 压缩开关
|
||||||
|
*/
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息阈值。
|
||||||
|
*
|
||||||
|
* @return 消息阈值
|
||||||
|
*/
|
||||||
|
public Integer getMsgThreshold() {
|
||||||
|
return msgThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置消息阈值。
|
||||||
|
*
|
||||||
|
* @param msgThreshold 消息阈值
|
||||||
|
*/
|
||||||
|
public void setMsgThreshold(int msgThreshold) {
|
||||||
|
this.msgThreshold = msgThreshold <= 0 ? null : msgThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近保留消息数。
|
||||||
|
*
|
||||||
|
* @return 最近保留消息数
|
||||||
|
*/
|
||||||
|
public int getLastKeep() {
|
||||||
|
return lastKeep;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最近保留消息数。
|
||||||
|
*
|
||||||
|
* @param lastKeep 最近保留消息数
|
||||||
|
*/
|
||||||
|
public void setLastKeep(int lastKeep) {
|
||||||
|
this.lastKeep = Math.max(0, lastKeep);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Token 比例。
|
||||||
|
*
|
||||||
|
* @return Token 比例
|
||||||
|
*/
|
||||||
|
public Double getTokenRatio() {
|
||||||
|
return tokenRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置Token 比例。
|
||||||
|
*
|
||||||
|
* @param tokenRatio Token 比例
|
||||||
|
*/
|
||||||
|
public void setTokenRatio(double tokenRatio) {
|
||||||
|
this.tokenRatio = tokenRatio <= 0D ? null : tokenRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最大 Token 数。
|
||||||
|
*
|
||||||
|
* @return 最大 Token 数
|
||||||
|
*/
|
||||||
|
public Long getMaxToken() {
|
||||||
|
return maxToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最大 Token 数。
|
||||||
|
*
|
||||||
|
* @param maxToken 最大 Token 数
|
||||||
|
*/
|
||||||
|
public void setMaxToken(long maxToken) {
|
||||||
|
this.maxToken = maxToken <= 0L ? null : maxToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取大负载阈值。
|
||||||
|
*
|
||||||
|
* @return 大负载阈值
|
||||||
|
*/
|
||||||
|
public long getLargePayloadThreshold() {
|
||||||
|
return largePayloadThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置大负载阈值。
|
||||||
|
*
|
||||||
|
* @param largePayloadThreshold 大负载阈值
|
||||||
|
*/
|
||||||
|
public void setLargePayloadThreshold(long largePayloadThreshold) {
|
||||||
|
this.largePayloadThreshold = largePayloadThreshold <= 0L ? 2048L : largePayloadThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最小压缩 Token 阈值。
|
||||||
|
*
|
||||||
|
* @return 最小压缩 Token 阈值
|
||||||
|
*/
|
||||||
|
public Integer getMinCompressionTokenThreshold() {
|
||||||
|
return minCompressionTokenThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最小压缩 Token 阈值。
|
||||||
|
*
|
||||||
|
* @param minCompressionTokenThreshold 最小压缩 Token 阈值
|
||||||
|
*/
|
||||||
|
public void setMinCompressionTokenThreshold(int minCompressionTokenThreshold) {
|
||||||
|
this.minCompressionTokenThreshold = minCompressionTokenThreshold <= 0 ? null : minCompressionTokenThreshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前轮压缩比例。
|
||||||
|
*
|
||||||
|
* @return 当前轮压缩比例
|
||||||
|
*/
|
||||||
|
public double getCurrentRoundCompressionRatio() {
|
||||||
|
return currentRoundCompressionRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前轮压缩比例。
|
||||||
|
*
|
||||||
|
* @param currentRoundCompressionRatio 当前轮压缩比例
|
||||||
|
*/
|
||||||
|
public void setCurrentRoundCompressionRatio(double currentRoundCompressionRatio) {
|
||||||
|
this.currentRoundCompressionRatio = currentRoundCompressionRatio <= 0D ? 0.5D : currentRoundCompressionRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最小连续工具消息数。
|
||||||
|
*
|
||||||
|
* @return 最小连续工具消息数
|
||||||
|
*/
|
||||||
|
public int getMinConsecutiveToolMessages() {
|
||||||
|
return minConsecutiveToolMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最小连续工具消息数。
|
||||||
|
*
|
||||||
|
* @param minConsecutiveToolMessages 最小连续工具消息数
|
||||||
|
*/
|
||||||
|
public void setMinConsecutiveToolMessages(int minConsecutiveToolMessages) {
|
||||||
|
this.minConsecutiveToolMessages = Math.max(0, minConsecutiveToolMessages);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.easyagents.agent.runtime.memory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时记忆策略。
|
||||||
|
*/
|
||||||
|
public class AgentMemoryPolicy {
|
||||||
|
|
||||||
|
private AgentMemoryType type = AgentMemoryType.AUTO_CONTEXT;
|
||||||
|
private int maxAttachedMessageCount = 50;
|
||||||
|
private AgentMemoryCompressionParameter compressionParameter = new AgentMemoryCompressionParameter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建默认 AutoContext 记忆策略。
|
||||||
|
*
|
||||||
|
* @return AutoContext 记忆策略
|
||||||
|
*/
|
||||||
|
public static AgentMemoryPolicy autoContext() {
|
||||||
|
AgentMemoryPolicy policy = new AgentMemoryPolicy();
|
||||||
|
policy.setType(AgentMemoryType.AUTO_CONTEXT);
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建内存策略。
|
||||||
|
*
|
||||||
|
* @return 内存策略
|
||||||
|
*/
|
||||||
|
public static AgentMemoryPolicy inMemory() {
|
||||||
|
AgentMemoryPolicy policy = new AgentMemoryPolicy();
|
||||||
|
policy.setType(AgentMemoryType.IN_MEMORY);
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取记忆类型。
|
||||||
|
*
|
||||||
|
* @return 记忆类型
|
||||||
|
*/
|
||||||
|
public AgentMemoryType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置记忆类型。
|
||||||
|
*
|
||||||
|
* @param type 记忆类型
|
||||||
|
*/
|
||||||
|
public void setType(AgentMemoryType type) {
|
||||||
|
this.type = type == null ? AgentMemoryType.AUTO_CONTEXT : type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最大附加消息数。
|
||||||
|
*
|
||||||
|
* @return 最大附加消息数
|
||||||
|
*/
|
||||||
|
public int getMaxAttachedMessageCount() {
|
||||||
|
return maxAttachedMessageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最大附加消息数。
|
||||||
|
*
|
||||||
|
* @param maxAttachedMessageCount 最大附加消息数
|
||||||
|
*/
|
||||||
|
public void setMaxAttachedMessageCount(int maxAttachedMessageCount) {
|
||||||
|
this.maxAttachedMessageCount = maxAttachedMessageCount <= 0 ? 50 : maxAttachedMessageCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取自动压缩参数。
|
||||||
|
*
|
||||||
|
* @return 自动压缩参数
|
||||||
|
*/
|
||||||
|
public AgentMemoryCompressionParameter getCompressionParameter() {
|
||||||
|
return compressionParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置自动压缩参数。
|
||||||
|
*
|
||||||
|
* @param compressionParameter 自动压缩参数
|
||||||
|
*/
|
||||||
|
public void setCompressionParameter(AgentMemoryCompressionParameter compressionParameter) {
|
||||||
|
this.compressionParameter = compressionParameter == null
|
||||||
|
? new AgentMemoryCompressionParameter()
|
||||||
|
: compressionParameter;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.easyagents.agent.runtime.memory;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.message.AgentMessage;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 由调用方恢复的会话历史快照。
|
||||||
|
*/
|
||||||
|
public class AgentMemorySnapshot {
|
||||||
|
|
||||||
|
private List<AgentMessage> messages = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息列表。
|
||||||
|
*
|
||||||
|
* @return 消息列表
|
||||||
|
*/
|
||||||
|
public List<AgentMessage> getMessages() {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置消息列表。
|
||||||
|
*
|
||||||
|
* @param messages 消息列表
|
||||||
|
*/
|
||||||
|
public void setMessages(List<AgentMessage> messages) {
|
||||||
|
this.messages = messages == null ? new ArrayList<>() : messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加消息。
|
||||||
|
*
|
||||||
|
* @param message 消息
|
||||||
|
*/
|
||||||
|
public void addMessage(AgentMessage message) {
|
||||||
|
if (message != null) {
|
||||||
|
messages.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.easyagents.agent.runtime.memory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记忆实现类型。
|
||||||
|
*/
|
||||||
|
public enum AgentMemoryType {
|
||||||
|
IN_MEMORY,
|
||||||
|
AUTO_CONTEXT
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.easyagents.agent.runtime.memory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 中立消息角色。
|
||||||
|
*/
|
||||||
|
public enum AgentMessageRole {
|
||||||
|
SYSTEM,
|
||||||
|
USER,
|
||||||
|
ASSISTANT,
|
||||||
|
TOOL
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.easyagents.agent.runtime.message;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时消息内容块。
|
||||||
|
*/
|
||||||
|
public abstract class AgentContentBlock {
|
||||||
|
|
||||||
|
private final AgentContentBlockType type;
|
||||||
|
private final Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建内容块。
|
||||||
|
*
|
||||||
|
* @param type 块类型
|
||||||
|
*/
|
||||||
|
protected AgentContentBlock(AgentContentBlockType type) {
|
||||||
|
this.type = type == null ? AgentContentBlockType.UNKNOWN : type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取块类型。
|
||||||
|
*
|
||||||
|
* @return 块类型
|
||||||
|
*/
|
||||||
|
public AgentContentBlockType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @return 元数据
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getMetadata() {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.easyagents.agent.runtime.message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息块类型。
|
||||||
|
*/
|
||||||
|
public enum AgentContentBlockType {
|
||||||
|
TEXT,
|
||||||
|
THINKING,
|
||||||
|
TOOL_USE,
|
||||||
|
TOOL_RESULT,
|
||||||
|
IMAGE,
|
||||||
|
AUDIO,
|
||||||
|
VIDEO,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package com.easyagents.agent.runtime.message;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 知识库引用信息。
|
||||||
|
*/
|
||||||
|
public class AgentKnowledgeReference {
|
||||||
|
|
||||||
|
private String knowledgeId;
|
||||||
|
private String knowledgeName;
|
||||||
|
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<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库ID。
|
||||||
|
*
|
||||||
|
* @return 知识库ID
|
||||||
|
*/
|
||||||
|
public String getKnowledgeId() {
|
||||||
|
return knowledgeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置知识库ID。
|
||||||
|
*
|
||||||
|
* @param knowledgeId 知识库ID
|
||||||
|
*/
|
||||||
|
public void setKnowledgeId(String knowledgeId) {
|
||||||
|
this.knowledgeId = knowledgeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库名称。
|
||||||
|
*
|
||||||
|
* @return 知识库名称
|
||||||
|
*/
|
||||||
|
public String getKnowledgeName() {
|
||||||
|
return knowledgeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置知识库名称。
|
||||||
|
*
|
||||||
|
* @param knowledgeName 知识库名称
|
||||||
|
*/
|
||||||
|
public void setKnowledgeName(String knowledgeName) {
|
||||||
|
this.knowledgeName = knowledgeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文档ID。
|
||||||
|
*
|
||||||
|
* @return 文档ID
|
||||||
|
*/
|
||||||
|
public String getDocumentId() {
|
||||||
|
return documentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置文档ID。
|
||||||
|
*
|
||||||
|
* @param documentId 文档ID
|
||||||
|
*/
|
||||||
|
public void setDocumentId(String documentId) {
|
||||||
|
this.documentId = documentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文档名称。
|
||||||
|
*
|
||||||
|
* @return 文档名称
|
||||||
|
*/
|
||||||
|
public String getDocumentName() {
|
||||||
|
return documentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置文档名称。
|
||||||
|
*
|
||||||
|
* @param documentName 文档名称
|
||||||
|
*/
|
||||||
|
public void setDocumentName(String documentName) {
|
||||||
|
this.documentName = documentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分片ID。
|
||||||
|
*
|
||||||
|
* @return 分片ID
|
||||||
|
*/
|
||||||
|
public String getChunkId() {
|
||||||
|
return chunkId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置分片ID。
|
||||||
|
*
|
||||||
|
* @param chunkId 分片ID
|
||||||
|
*/
|
||||||
|
public void setChunkId(String chunkId) {
|
||||||
|
this.chunkId = chunkId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取命中分片原文。
|
||||||
|
*
|
||||||
|
* @return 命中分片原文
|
||||||
|
*/
|
||||||
|
public String getChunkContent() {
|
||||||
|
return chunkContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置命中分片原文。
|
||||||
|
*
|
||||||
|
* @param chunkContent 命中分片原文
|
||||||
|
*/
|
||||||
|
public void setChunkContent(String chunkContent) {
|
||||||
|
this.chunkContent = chunkContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取来源 URI。
|
||||||
|
*
|
||||||
|
* @return 来源 URI
|
||||||
|
*/
|
||||||
|
public String getSourceUri() {
|
||||||
|
return sourceUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置来源 URI。
|
||||||
|
*
|
||||||
|
* @param sourceUri 来源 URI
|
||||||
|
*/
|
||||||
|
public void setSourceUri(String sourceUri) {
|
||||||
|
this.sourceUri = sourceUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取分数。
|
||||||
|
*
|
||||||
|
* @return 分数
|
||||||
|
*/
|
||||||
|
public Double getScore() {
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置分数。
|
||||||
|
*
|
||||||
|
* @param score 分数
|
||||||
|
*/
|
||||||
|
public void setScore(Double score) {
|
||||||
|
this.score = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @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,241 @@
|
|||||||
|
package com.easyagents.agent.runtime.message;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 媒体内容块。
|
||||||
|
*/
|
||||||
|
public class AgentMediaBlock extends AgentContentBlock {
|
||||||
|
|
||||||
|
private String mimeType;
|
||||||
|
private String reference;
|
||||||
|
private String url;
|
||||||
|
private String data;
|
||||||
|
private Integer minPixels;
|
||||||
|
private Integer maxPixels;
|
||||||
|
private Float fps;
|
||||||
|
private Integer maxFrames;
|
||||||
|
private Integer totalPixels;
|
||||||
|
private String mediaKind;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建媒体块。
|
||||||
|
*
|
||||||
|
* @param mediaKind 媒体类型
|
||||||
|
*/
|
||||||
|
public AgentMediaBlock(String mediaKind) {
|
||||||
|
super(resolveType(mediaKind));
|
||||||
|
this.mediaKind = mediaKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取媒体类型。
|
||||||
|
*
|
||||||
|
* @return 媒体类型
|
||||||
|
*/
|
||||||
|
public String getMediaKind() {
|
||||||
|
return mediaKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置媒体类型。
|
||||||
|
*
|
||||||
|
* @param mediaKind 媒体类型
|
||||||
|
*/
|
||||||
|
public void setMediaKind(String mediaKind) {
|
||||||
|
this.mediaKind = mediaKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 MIME 类型。
|
||||||
|
*
|
||||||
|
* @return MIME 类型
|
||||||
|
*/
|
||||||
|
public String getMimeType() {
|
||||||
|
return mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 MIME 类型。
|
||||||
|
*
|
||||||
|
* @param mimeType MIME 类型
|
||||||
|
*/
|
||||||
|
public void setMimeType(String mimeType) {
|
||||||
|
this.mimeType = mimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取由业务侧解析的稳定媒体引用。
|
||||||
|
*
|
||||||
|
* @return 媒体引用
|
||||||
|
*/
|
||||||
|
public String getReference() {
|
||||||
|
return reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置由业务侧解析的稳定媒体引用。
|
||||||
|
*
|
||||||
|
* @param reference 媒体引用
|
||||||
|
*/
|
||||||
|
public void setReference(String reference) {
|
||||||
|
this.reference = reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 URL。
|
||||||
|
*
|
||||||
|
* @return URL
|
||||||
|
*/
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 URL。
|
||||||
|
*
|
||||||
|
* @param url URL
|
||||||
|
*/
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Base64 数据。
|
||||||
|
*
|
||||||
|
* @return Base64 数据
|
||||||
|
*/
|
||||||
|
public String getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Base64 数据。
|
||||||
|
*
|
||||||
|
* @param data Base64 数据
|
||||||
|
*/
|
||||||
|
public void setData(String data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最小像素。
|
||||||
|
*
|
||||||
|
* @return 最小像素
|
||||||
|
*/
|
||||||
|
public Integer getMinPixels() {
|
||||||
|
return minPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最小像素。
|
||||||
|
*
|
||||||
|
* @param minPixels 最小像素
|
||||||
|
*/
|
||||||
|
public void setMinPixels(Integer minPixels) {
|
||||||
|
this.minPixels = minPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最大像素。
|
||||||
|
*
|
||||||
|
* @return 最大像素
|
||||||
|
*/
|
||||||
|
public Integer getMaxPixels() {
|
||||||
|
return maxPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最大像素。
|
||||||
|
*
|
||||||
|
* @param maxPixels 最大像素
|
||||||
|
*/
|
||||||
|
public void setMaxPixels(Integer maxPixels) {
|
||||||
|
this.maxPixels = maxPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取帧率。
|
||||||
|
*
|
||||||
|
* @return 帧率
|
||||||
|
*/
|
||||||
|
public Float getFps() {
|
||||||
|
return fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置帧率。
|
||||||
|
*
|
||||||
|
* @param fps 帧率
|
||||||
|
*/
|
||||||
|
public void setFps(Float fps) {
|
||||||
|
this.fps = fps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最大帧数。
|
||||||
|
*
|
||||||
|
* @return 最大帧数
|
||||||
|
*/
|
||||||
|
public Integer getMaxFrames() {
|
||||||
|
return maxFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最大帧数。
|
||||||
|
*
|
||||||
|
* @param maxFrames 最大帧数
|
||||||
|
*/
|
||||||
|
public void setMaxFrames(Integer maxFrames) {
|
||||||
|
this.maxFrames = maxFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取总像素。
|
||||||
|
*
|
||||||
|
* @return 总像素
|
||||||
|
*/
|
||||||
|
public Integer getTotalPixels() {
|
||||||
|
return totalPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置总像素。
|
||||||
|
*
|
||||||
|
* @param totalPixels 总像素
|
||||||
|
*/
|
||||||
|
public void setTotalPixels(Integer totalPixels) {
|
||||||
|
this.totalPixels = totalPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AgentContentBlockType resolveType(String mediaKind) {
|
||||||
|
if ("audio".equalsIgnoreCase(mediaKind)) {
|
||||||
|
return AgentContentBlockType.AUDIO;
|
||||||
|
}
|
||||||
|
if ("video".equalsIgnoreCase(mediaKind)) {
|
||||||
|
return AgentContentBlockType.VIDEO;
|
||||||
|
}
|
||||||
|
return AgentContentBlockType.IMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 额外元数据。
|
||||||
|
*
|
||||||
|
* @return 额外元数据
|
||||||
|
*/
|
||||||
|
public Map<String, Object> toMetadata() {
|
||||||
|
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);
|
||||||
|
metadata.put("maxPixels", maxPixels);
|
||||||
|
metadata.put("fps", fps);
|
||||||
|
metadata.put("maxFrames", maxFrames);
|
||||||
|
metadata.put("totalPixels", totalPixels);
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package com.easyagents.agent.runtime.message;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体聊天消息
|
||||||
|
*/
|
||||||
|
public class AgentMessage {
|
||||||
|
|
||||||
|
private String messageId = UUID.randomUUID().toString();
|
||||||
|
private String name;
|
||||||
|
/**
|
||||||
|
* 聊天发起者角色
|
||||||
|
*/
|
||||||
|
private AgentMessageRole role = AgentMessageRole.USER;
|
||||||
|
/**
|
||||||
|
* 消息内容块
|
||||||
|
*/
|
||||||
|
private List<AgentContentBlock> contentBlocks = new ArrayList<>();
|
||||||
|
/**
|
||||||
|
* 知识库引用
|
||||||
|
*/
|
||||||
|
private List<AgentKnowledgeReference> knowledgeReferences = new ArrayList<>();
|
||||||
|
/**
|
||||||
|
* 元数据
|
||||||
|
*/
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private Instant createdAt = Instant.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建纯文本消息。
|
||||||
|
*
|
||||||
|
* @param role 消息角色
|
||||||
|
* @param content 文本内容
|
||||||
|
* @return 消息
|
||||||
|
*/
|
||||||
|
public static AgentMessage text(AgentMessageRole role, String content) {
|
||||||
|
AgentMessage message = new AgentMessage();
|
||||||
|
message.setRole(role);
|
||||||
|
message.setContentBlocks(List.of(new AgentTextBlock(content)));
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息ID。
|
||||||
|
*
|
||||||
|
* @return 消息ID
|
||||||
|
*/
|
||||||
|
public String getMessageId() {
|
||||||
|
return messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置消息ID。
|
||||||
|
*
|
||||||
|
* @param messageId 消息ID
|
||||||
|
*/
|
||||||
|
public void setMessageId(String messageId) {
|
||||||
|
this.messageId = messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息名称。
|
||||||
|
*
|
||||||
|
* @return 消息名称
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置消息名称。
|
||||||
|
*
|
||||||
|
* @param name 消息名称
|
||||||
|
*/
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色。
|
||||||
|
*
|
||||||
|
* @return 角色
|
||||||
|
*/
|
||||||
|
public AgentMessageRole getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置角色。
|
||||||
|
*
|
||||||
|
* @param role 角色
|
||||||
|
*/
|
||||||
|
public void setRole(AgentMessageRole role) {
|
||||||
|
this.role = role == null ? AgentMessageRole.USER : role;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取内容块列表。
|
||||||
|
*
|
||||||
|
* @return 内容块列表
|
||||||
|
*/
|
||||||
|
public List<AgentContentBlock> getContentBlocks() {
|
||||||
|
return contentBlocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置内容块列表。
|
||||||
|
*
|
||||||
|
* @param contentBlocks 内容块列表
|
||||||
|
*/
|
||||||
|
public void setContentBlocks(List<AgentContentBlock> contentBlocks) {
|
||||||
|
this.contentBlocks = contentBlocks == null ? new ArrayList<>() : new ArrayList<>(contentBlocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取知识库引用。
|
||||||
|
*
|
||||||
|
* @return 知识库引用
|
||||||
|
*/
|
||||||
|
public List<AgentKnowledgeReference> getKnowledgeReferences() {
|
||||||
|
return knowledgeReferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置知识库引用。
|
||||||
|
*
|
||||||
|
* @param knowledgeReferences 知识库引用
|
||||||
|
*/
|
||||||
|
public void setKnowledgeReferences(List<AgentKnowledgeReference> knowledgeReferences) {
|
||||||
|
this.knowledgeReferences = knowledgeReferences == null ? new ArrayList<>() : new ArrayList<>(knowledgeReferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取创建时间。
|
||||||
|
*
|
||||||
|
* @return 创建时间
|
||||||
|
*/
|
||||||
|
public Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置创建时间。
|
||||||
|
*
|
||||||
|
* @param createdAt 创建时间
|
||||||
|
*/
|
||||||
|
public void setCreatedAt(Instant createdAt) {
|
||||||
|
this.createdAt = createdAt == null ? Instant.now() : createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.easyagents.agent.runtime.message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 中立消息角色。
|
||||||
|
*/
|
||||||
|
public enum AgentMessageRole {
|
||||||
|
SYSTEM,
|
||||||
|
USER,
|
||||||
|
ASSISTANT,
|
||||||
|
TOOL
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.easyagents.agent.runtime.message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本内容块。
|
||||||
|
*/
|
||||||
|
public class AgentTextBlock extends AgentContentBlock {
|
||||||
|
|
||||||
|
private String text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建文本块。
|
||||||
|
*
|
||||||
|
* @param text 文本内容
|
||||||
|
*/
|
||||||
|
public AgentTextBlock(String text) {
|
||||||
|
super(AgentContentBlockType.TEXT);
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文本。
|
||||||
|
*
|
||||||
|
* @return 文本
|
||||||
|
*/
|
||||||
|
public String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置文本。
|
||||||
|
*
|
||||||
|
* @param text 文本
|
||||||
|
*/
|
||||||
|
public void setText(String text) {
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.easyagents.agent.runtime.message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 思考内容块。
|
||||||
|
*/
|
||||||
|
public class AgentThinkingBlock extends AgentContentBlock {
|
||||||
|
|
||||||
|
private String thinking;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建思考块。
|
||||||
|
*
|
||||||
|
* @param thinking 思考内容
|
||||||
|
*/
|
||||||
|
public AgentThinkingBlock(String thinking) {
|
||||||
|
super(AgentContentBlockType.THINKING);
|
||||||
|
this.thinking = thinking;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取思考内容。
|
||||||
|
*
|
||||||
|
* @return 思考内容
|
||||||
|
*/
|
||||||
|
public String getThinking() {
|
||||||
|
return thinking;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置思考内容。
|
||||||
|
*
|
||||||
|
* @param thinking 思考内容
|
||||||
|
*/
|
||||||
|
public void setThinking(String thinking) {
|
||||||
|
this.thinking = thinking;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.easyagents.agent.runtime.message;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具结果内容块。
|
||||||
|
*/
|
||||||
|
public class AgentToolResultBlock extends AgentContentBlock {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
private String name;
|
||||||
|
private List<AgentContentBlock> output = new ArrayList<>();
|
||||||
|
private boolean suspended;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建工具结果块。
|
||||||
|
*
|
||||||
|
* @param id 调用ID
|
||||||
|
* @param name 工具名
|
||||||
|
*/
|
||||||
|
public AgentToolResultBlock(String id, String name) {
|
||||||
|
super(AgentContentBlockType.TOOL_RESULT);
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取调用ID。
|
||||||
|
*
|
||||||
|
* @return 调用ID
|
||||||
|
*/
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置调用ID。
|
||||||
|
*
|
||||||
|
* @param id 调用ID
|
||||||
|
*/
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具名。
|
||||||
|
*
|
||||||
|
* @return 工具名
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置工具名。
|
||||||
|
*
|
||||||
|
* @param name 工具名
|
||||||
|
*/
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取输出块列表。
|
||||||
|
*
|
||||||
|
* @return 输出块列表
|
||||||
|
*/
|
||||||
|
public List<AgentContentBlock> getOutput() {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置输出块列表。
|
||||||
|
*
|
||||||
|
* @param output 输出块列表
|
||||||
|
*/
|
||||||
|
public void setOutput(List<AgentContentBlock> output) {
|
||||||
|
this.output = output == null ? new ArrayList<>() : new ArrayList<>(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否挂起。
|
||||||
|
*
|
||||||
|
* @return 是否挂起
|
||||||
|
*/
|
||||||
|
public boolean isSuspended() {
|
||||||
|
return suspended;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置挂起状态。
|
||||||
|
*
|
||||||
|
* @param suspended 挂起状态
|
||||||
|
*/
|
||||||
|
public void setSuspended(boolean suspended) {
|
||||||
|
this.suspended = suspended;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.easyagents.agent.runtime.message;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具调用内容块。
|
||||||
|
*/
|
||||||
|
public class AgentToolUseBlock extends AgentContentBlock {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
private String name;
|
||||||
|
private Map<String, Object> input = new LinkedHashMap<>();
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建工具调用块。
|
||||||
|
*
|
||||||
|
* @param id 调用ID
|
||||||
|
* @param name 工具名
|
||||||
|
* @param input 输入参数
|
||||||
|
*/
|
||||||
|
public AgentToolUseBlock(String id, String name, Map<String, Object> input) {
|
||||||
|
super(AgentContentBlockType.TOOL_USE);
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
setInput(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取调用ID。
|
||||||
|
*
|
||||||
|
* @return 调用ID
|
||||||
|
*/
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置调用ID。
|
||||||
|
*
|
||||||
|
* @param id 调用ID
|
||||||
|
*/
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具名。
|
||||||
|
*
|
||||||
|
* @return 工具名
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置工具名。
|
||||||
|
*
|
||||||
|
* @param name 工具名
|
||||||
|
*/
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取输入参数。
|
||||||
|
*
|
||||||
|
* @return 输入参数
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getInput() {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置输入参数。
|
||||||
|
*
|
||||||
|
* @param input 输入参数
|
||||||
|
*/
|
||||||
|
public void setInput(Map<String, Object> input) {
|
||||||
|
this.input = input == null ? new LinkedHashMap<>() : new LinkedHashMap<>(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取原始内容。
|
||||||
|
*
|
||||||
|
* @return 原始内容
|
||||||
|
*/
|
||||||
|
public String getContent() {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置原始内容。
|
||||||
|
*
|
||||||
|
* @param content 原始内容
|
||||||
|
*/
|
||||||
|
public void setContent(String content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.easyagents.agent.runtime.message;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未识别的内容消息块
|
||||||
|
* 兜底表示。
|
||||||
|
*/
|
||||||
|
public class AgentUnknownBlock extends AgentContentBlock {
|
||||||
|
|
||||||
|
private String sourceClassName;
|
||||||
|
private String sourceTypeName;
|
||||||
|
private String rawText;
|
||||||
|
private Map<String, Object> rawMetadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建未知内容块。
|
||||||
|
*/
|
||||||
|
public AgentUnknownBlock() {
|
||||||
|
super(AgentContentBlockType.UNKNOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取来源类名。
|
||||||
|
*
|
||||||
|
* @return 来源类名
|
||||||
|
*/
|
||||||
|
public String getSourceClassName() {
|
||||||
|
return sourceClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置来源类名。
|
||||||
|
*
|
||||||
|
* @param sourceClassName 来源类名
|
||||||
|
*/
|
||||||
|
public void setSourceClassName(String sourceClassName) {
|
||||||
|
this.sourceClassName = sourceClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取来源类型名。
|
||||||
|
*
|
||||||
|
* @return 来源类型名
|
||||||
|
*/
|
||||||
|
public String getSourceTypeName() {
|
||||||
|
return sourceTypeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置来源类型名。
|
||||||
|
*
|
||||||
|
* @param sourceTypeName 来源类型名
|
||||||
|
*/
|
||||||
|
public void setSourceTypeName(String sourceTypeName) {
|
||||||
|
this.sourceTypeName = sourceTypeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取原始文本表示。
|
||||||
|
*
|
||||||
|
* @return 原始文本表示
|
||||||
|
*/
|
||||||
|
public String getRawText() {
|
||||||
|
return rawText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置原始文本表示。
|
||||||
|
*
|
||||||
|
* @param rawText 原始文本表示
|
||||||
|
*/
|
||||||
|
public void setRawText(String rawText) {
|
||||||
|
this.rawText = rawText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取原始元数据。
|
||||||
|
*
|
||||||
|
* @return 原始元数据
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getRawMetadata() {
|
||||||
|
return rawMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置原始元数据。
|
||||||
|
*
|
||||||
|
* @param rawMetadata 原始元数据
|
||||||
|
*/
|
||||||
|
public void setRawMetadata(Map<String, Object> rawMetadata) {
|
||||||
|
this.rawMetadata = rawMetadata == null ? new LinkedHashMap<>() : new LinkedHashMap<>(rawMetadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
package com.easyagents.agent.runtime.model;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行时模型适配器使用的生成参数。
|
||||||
|
*/
|
||||||
|
public class AgentGenerationOptions {
|
||||||
|
|
||||||
|
private Double temperature;
|
||||||
|
private Double topP;
|
||||||
|
private Integer topK;
|
||||||
|
private Integer maxTokens;
|
||||||
|
private Integer maxCompletionTokens;
|
||||||
|
private Integer thinkingBudget;
|
||||||
|
private String reasoningEffort;
|
||||||
|
private Boolean thinkingEnabled;
|
||||||
|
private Boolean stream = true;
|
||||||
|
private Map<String, Object> additionalBodyParams = new LinkedHashMap<>();
|
||||||
|
private Map<String, String> additionalHeaders = new LinkedHashMap<>();
|
||||||
|
private Map<String, String> additionalQueryParams = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取temperature。
|
||||||
|
*
|
||||||
|
* @return temperature 参数
|
||||||
|
*/
|
||||||
|
public Double getTemperature() {
|
||||||
|
return temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置temperature。
|
||||||
|
*
|
||||||
|
* @param temperature temperature 参数
|
||||||
|
*/
|
||||||
|
public void setTemperature(Double temperature) {
|
||||||
|
this.temperature = temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取top-p。
|
||||||
|
*
|
||||||
|
* @return top-p 参数
|
||||||
|
*/
|
||||||
|
public Double getTopP() {
|
||||||
|
return topP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置top-p。
|
||||||
|
*
|
||||||
|
* @param topP top-p 参数
|
||||||
|
*/
|
||||||
|
public void setTopP(Double topP) {
|
||||||
|
this.topP = topP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取top-k。
|
||||||
|
*
|
||||||
|
* @return top-k 参数
|
||||||
|
*/
|
||||||
|
public Integer getTopK() {
|
||||||
|
return topK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置top-k。
|
||||||
|
*
|
||||||
|
* @param topK top-k 参数
|
||||||
|
*/
|
||||||
|
public void setTopK(Integer topK) {
|
||||||
|
this.topK = topK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最大 Token 数。
|
||||||
|
*
|
||||||
|
* @return 最大 Token 数
|
||||||
|
*/
|
||||||
|
public Integer getMaxTokens() {
|
||||||
|
return maxTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最大 Token 数。
|
||||||
|
*
|
||||||
|
* @param maxTokens 最大 Token 数
|
||||||
|
*/
|
||||||
|
public void setMaxTokens(Integer maxTokens) {
|
||||||
|
this.maxTokens = maxTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最大补全 Token 数。
|
||||||
|
*
|
||||||
|
* @return 最大补全 Token 数
|
||||||
|
*/
|
||||||
|
public Integer getMaxCompletionTokens() {
|
||||||
|
return maxCompletionTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最大补全 Token 数。
|
||||||
|
*
|
||||||
|
* @param maxCompletionTokens 最大补全 Token 数
|
||||||
|
*/
|
||||||
|
public void setMaxCompletionTokens(Integer maxCompletionTokens) {
|
||||||
|
this.maxCompletionTokens = maxCompletionTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取思考预算。
|
||||||
|
*
|
||||||
|
* @return 思考预算
|
||||||
|
*/
|
||||||
|
public Integer getThinkingBudget() {
|
||||||
|
return thinkingBudget;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置思考预算。
|
||||||
|
*
|
||||||
|
* @param thinkingBudget 思考预算
|
||||||
|
*/
|
||||||
|
public void setThinkingBudget(Integer thinkingBudget) {
|
||||||
|
this.thinkingBudget = thinkingBudget;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取推理强度。
|
||||||
|
*
|
||||||
|
* @return 推理强度
|
||||||
|
*/
|
||||||
|
public String getReasoningEffort() {
|
||||||
|
return reasoningEffort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置推理强度。
|
||||||
|
*
|
||||||
|
* @param reasoningEffort 推理强度
|
||||||
|
*/
|
||||||
|
public void setReasoningEffort(String reasoningEffort) {
|
||||||
|
this.reasoningEffort = reasoningEffort;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回是否provider thinking is enabled。
|
||||||
|
*
|
||||||
|
* @return 思考开关
|
||||||
|
*/
|
||||||
|
public Boolean getThinkingEnabled() {
|
||||||
|
return thinkingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置provider thinking flag。
|
||||||
|
*
|
||||||
|
* @param thinkingEnabled 思考开关
|
||||||
|
*/
|
||||||
|
public void setThinkingEnabled(Boolean thinkingEnabled) {
|
||||||
|
this.thinkingEnabled = thinkingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回是否streaming is requested。
|
||||||
|
*
|
||||||
|
* @return 流式开关
|
||||||
|
*/
|
||||||
|
public Boolean getStream() {
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置流式开关。
|
||||||
|
*
|
||||||
|
* @param stream 流式开关
|
||||||
|
*/
|
||||||
|
public void setStream(Boolean stream) {
|
||||||
|
this.stream = stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取额外请求体参数。
|
||||||
|
*
|
||||||
|
* @return 额外请求体参数
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getAdditionalBodyParams() {
|
||||||
|
return additionalBodyParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置额外请求体参数。
|
||||||
|
*
|
||||||
|
* @param additionalBodyParams 额外请求体参数
|
||||||
|
*/
|
||||||
|
public void setAdditionalBodyParams(Map<String, Object> additionalBodyParams) {
|
||||||
|
this.additionalBodyParams = additionalBodyParams == null ? new LinkedHashMap<>() : additionalBodyParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取额外请求头。
|
||||||
|
*
|
||||||
|
* @return 额外请求头
|
||||||
|
*/
|
||||||
|
public Map<String, String> getAdditionalHeaders() {
|
||||||
|
return additionalHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置额外请求头。
|
||||||
|
*
|
||||||
|
* @param additionalHeaders 额外请求头
|
||||||
|
*/
|
||||||
|
public void setAdditionalHeaders(Map<String, String> additionalHeaders) {
|
||||||
|
this.additionalHeaders = additionalHeaders == null ? new LinkedHashMap<>() : additionalHeaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取额外查询参数。
|
||||||
|
*
|
||||||
|
* @return 额外查询参数
|
||||||
|
*/
|
||||||
|
public Map<String, String> getAdditionalQueryParams() {
|
||||||
|
return additionalQueryParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置额外查询参数。
|
||||||
|
*
|
||||||
|
* @param additionalQueryParams 额外查询参数
|
||||||
|
*/
|
||||||
|
public void setAdditionalQueryParams(Map<String, String> additionalQueryParams) {
|
||||||
|
this.additionalQueryParams = additionalQueryParams == null ? new LinkedHashMap<>() : additionalQueryParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.easyagents.agent.runtime.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供应商无关的模型工厂标记接口。
|
||||||
|
*
|
||||||
|
* @param <T> 由适配器持有的具体模型类型
|
||||||
|
*/
|
||||||
|
public interface AgentModelFactory<T> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建a concrete model。
|
||||||
|
*
|
||||||
|
* @param modelSpec 模型声明
|
||||||
|
* @param generationOptions 生成参数
|
||||||
|
* @return 具体模型
|
||||||
|
*/
|
||||||
|
T create(AgentModelSpec modelSpec, AgentGenerationOptions generationOptions);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.easyagents.agent.runtime.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体运行时支持的模型供应商类型。
|
||||||
|
*/
|
||||||
|
public enum AgentModelProviderType {
|
||||||
|
OPENAI,
|
||||||
|
OPENAI_COMPATIBLE,
|
||||||
|
ANTHROPIC,
|
||||||
|
GEMINI,
|
||||||
|
OLLAMA,
|
||||||
|
DEEPSEEK,
|
||||||
|
GLM,
|
||||||
|
MINIMAX,
|
||||||
|
MOONSHOT,
|
||||||
|
ARK,
|
||||||
|
SILICONFLOW,
|
||||||
|
DASHSCOPE,
|
||||||
|
CUSTOM
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package com.easyagents.agent.runtime.model;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供应商无关的模型声明。
|
||||||
|
*/
|
||||||
|
public class AgentModelSpec {
|
||||||
|
|
||||||
|
private AgentModelProviderType providerType = AgentModelProviderType.OPENAI_COMPATIBLE;
|
||||||
|
private String modelName;
|
||||||
|
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<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取供应商类型。
|
||||||
|
*
|
||||||
|
* @return 供应商类型
|
||||||
|
*/
|
||||||
|
public AgentModelProviderType getProviderType() {
|
||||||
|
return providerType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置供应商类型。
|
||||||
|
*
|
||||||
|
* @param providerType 供应商类型
|
||||||
|
*/
|
||||||
|
public void setProviderType(AgentModelProviderType providerType) {
|
||||||
|
this.providerType = providerType == null ? AgentModelProviderType.OPENAI_COMPATIBLE : providerType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型名称。
|
||||||
|
*
|
||||||
|
* @return 模型名称
|
||||||
|
*/
|
||||||
|
public String getModelName() {
|
||||||
|
return modelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置模型名称。
|
||||||
|
*
|
||||||
|
* @param modelName 模型名称
|
||||||
|
*/
|
||||||
|
public void setModelName(String modelName) {
|
||||||
|
this.modelName = modelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取基础 URL。
|
||||||
|
*
|
||||||
|
* @return 基础 URL
|
||||||
|
*/
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置基础 URL。
|
||||||
|
*
|
||||||
|
* @param baseUrl 基础 URL
|
||||||
|
*/
|
||||||
|
public void setBaseUrl(String baseUrl) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取端点路径。
|
||||||
|
*
|
||||||
|
* @return 端点路径
|
||||||
|
*/
|
||||||
|
public String getEndpointPath() {
|
||||||
|
return endpointPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置端点路径。
|
||||||
|
*
|
||||||
|
* @param endpointPath 端点路径
|
||||||
|
*/
|
||||||
|
public void setEndpointPath(String endpointPath) {
|
||||||
|
this.endpointPath = endpointPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取API Key。
|
||||||
|
*
|
||||||
|
* @return API Key 值
|
||||||
|
*/
|
||||||
|
public String getApiKey() {
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置API Key。
|
||||||
|
*
|
||||||
|
* @param apiKey API Key 值
|
||||||
|
*/
|
||||||
|
public void setApiKey(String apiKey) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @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,182 @@
|
|||||||
|
package com.easyagents.agent.runtime.persistence;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 控制运行时状态持久化。
|
||||||
|
*/
|
||||||
|
public class AgentPersistencePolicy {
|
||||||
|
|
||||||
|
private boolean enabled;
|
||||||
|
private boolean memoryManaged;
|
||||||
|
private boolean toolkitManaged;
|
||||||
|
private boolean planNotebookManaged;
|
||||||
|
private boolean statefulToolsManaged;
|
||||||
|
private boolean hitlManaged;
|
||||||
|
private boolean sessionManaged;
|
||||||
|
private boolean conversationRecordingEnabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建禁用策略。
|
||||||
|
*
|
||||||
|
* @return 禁用策略
|
||||||
|
*/
|
||||||
|
public static AgentPersistencePolicy disabled() {
|
||||||
|
return new AgentPersistencePolicy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建仅记忆持久化策略。
|
||||||
|
*
|
||||||
|
* @return 仅记忆持久化策略
|
||||||
|
*/
|
||||||
|
public static AgentPersistencePolicy memoryOnly() {
|
||||||
|
AgentPersistencePolicy policy = new AgentPersistencePolicy();
|
||||||
|
policy.setEnabled(true);
|
||||||
|
policy.setMemoryManaged(true);
|
||||||
|
policy.setSessionManaged(true);
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回是否persistence is enabled。
|
||||||
|
*
|
||||||
|
* @return 启用标记
|
||||||
|
*/
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置启用标记。
|
||||||
|
*
|
||||||
|
* @param enabled 启用标记
|
||||||
|
*/
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回是否memory is managed。
|
||||||
|
*
|
||||||
|
* @return 记忆标记
|
||||||
|
*/
|
||||||
|
public boolean isMemoryManaged() {
|
||||||
|
return memoryManaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置记忆标记。
|
||||||
|
*
|
||||||
|
* @param memoryManaged 记忆标记
|
||||||
|
*/
|
||||||
|
public void setMemoryManaged(boolean memoryManaged) {
|
||||||
|
this.memoryManaged = memoryManaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回是否toolkit is managed。
|
||||||
|
*
|
||||||
|
* @return Toolkit 标记
|
||||||
|
*/
|
||||||
|
public boolean isToolkitManaged() {
|
||||||
|
return toolkitManaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置Toolkit 标记。
|
||||||
|
*
|
||||||
|
* @param toolkitManaged Toolkit 标记
|
||||||
|
*/
|
||||||
|
public void setToolkitManaged(boolean toolkitManaged) {
|
||||||
|
this.toolkitManaged = toolkitManaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回是否plan notebook is managed。
|
||||||
|
*
|
||||||
|
* @return 计划笔记标记
|
||||||
|
*/
|
||||||
|
public boolean isPlanNotebookManaged() {
|
||||||
|
return planNotebookManaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置计划笔记标记。
|
||||||
|
*
|
||||||
|
* @param planNotebookManaged 计划笔记标记
|
||||||
|
*/
|
||||||
|
public void setPlanNotebookManaged(boolean planNotebookManaged) {
|
||||||
|
this.planNotebookManaged = planNotebookManaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回是否stateful tools are managed。
|
||||||
|
*
|
||||||
|
* @return 有状态工具标记
|
||||||
|
*/
|
||||||
|
public boolean isStatefulToolsManaged() {
|
||||||
|
return statefulToolsManaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置有状态工具标记。
|
||||||
|
*
|
||||||
|
* @param statefulToolsManaged 有状态工具标记
|
||||||
|
*/
|
||||||
|
public void setStatefulToolsManaged(boolean statefulToolsManaged) {
|
||||||
|
this.statefulToolsManaged = statefulToolsManaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回是否HITL pending state is managed。
|
||||||
|
*
|
||||||
|
* @return HITL 标记
|
||||||
|
*/
|
||||||
|
public boolean isHitlManaged() {
|
||||||
|
return hitlManaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置HITL 标记。
|
||||||
|
*
|
||||||
|
* @param hitlManaged HITL 标记
|
||||||
|
*/
|
||||||
|
public void setHitlManaged(boolean hitlManaged) {
|
||||||
|
this.hitlManaged = hitlManaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回是否session storage is managed。
|
||||||
|
*
|
||||||
|
* @return 会话标记
|
||||||
|
*/
|
||||||
|
public boolean isSessionManaged() {
|
||||||
|
return sessionManaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置会话标记。
|
||||||
|
*
|
||||||
|
* @param sessionManaged 会话标记
|
||||||
|
*/
|
||||||
|
public void setSessionManaged(boolean sessionManaged) {
|
||||||
|
this.sessionManaged = sessionManaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回是否conversation events should be recorded。
|
||||||
|
*
|
||||||
|
* @return 会话记录标记
|
||||||
|
*/
|
||||||
|
public boolean isConversationRecordingEnabled() {
|
||||||
|
return conversationRecordingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置会话记录标记。
|
||||||
|
*
|
||||||
|
* @param conversationRecordingEnabled 会话记录标记
|
||||||
|
*/
|
||||||
|
public void setConversationRecordingEnabled(boolean conversationRecordingEnabled) {
|
||||||
|
this.conversationRecordingEnabled = conversationRecordingEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.easyagents.agent.runtime.persistence;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可恢复状态的运行时检查点。
|
||||||
|
*/
|
||||||
|
public class AgentRuntimeCheckpoint {
|
||||||
|
|
||||||
|
private String checkpointId;
|
||||||
|
private String sessionId;
|
||||||
|
private String agentId;
|
||||||
|
private Map<String, Object> state = new LinkedHashMap<>();
|
||||||
|
private Instant createdAt = Instant.now();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取检查点ID。
|
||||||
|
*
|
||||||
|
* @return 检查点ID
|
||||||
|
*/
|
||||||
|
public String getCheckpointId() {
|
||||||
|
return checkpointId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置检查点ID。
|
||||||
|
*
|
||||||
|
* @param checkpointId 检查点ID
|
||||||
|
*/
|
||||||
|
public void setCheckpointId(String checkpointId) {
|
||||||
|
this.checkpointId = checkpointId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取会话ID。
|
||||||
|
*
|
||||||
|
* @return 会话ID
|
||||||
|
*/
|
||||||
|
public String getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置会话ID。
|
||||||
|
*
|
||||||
|
* @param sessionId 会话ID
|
||||||
|
*/
|
||||||
|
public void setSessionId(String sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取智能体ID。
|
||||||
|
*
|
||||||
|
* @return 智能体ID
|
||||||
|
*/
|
||||||
|
public String getAgentId() {
|
||||||
|
return agentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置智能体ID。
|
||||||
|
*
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
*/
|
||||||
|
public void setAgentId(String agentId) {
|
||||||
|
this.agentId = agentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态载荷。
|
||||||
|
*
|
||||||
|
* @return 状态载荷
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置状态载荷。
|
||||||
|
*
|
||||||
|
* @param state 状态载荷
|
||||||
|
*/
|
||||||
|
public void setState(Map<String, Object> state) {
|
||||||
|
this.state = state == null ? new LinkedHashMap<>() : state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取创建时间。
|
||||||
|
*
|
||||||
|
* @return 创建时间
|
||||||
|
*/
|
||||||
|
public Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置创建时间。
|
||||||
|
*
|
||||||
|
* @param createdAt 创建时间
|
||||||
|
*/
|
||||||
|
public void setCreatedAt(Instant createdAt) {
|
||||||
|
this.createdAt = createdAt == null ? Instant.now() : createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.easyagents.agent.runtime.persistence.conversation;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.AgentRuntimeExecutionContext;
|
||||||
|
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录业务会话事件。
|
||||||
|
*/
|
||||||
|
public interface AgentConversationRecorder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录一条运行时事件。
|
||||||
|
*
|
||||||
|
* @param request 运行请求
|
||||||
|
* @param 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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user