feat: 增加内置read、write、shell 工具支持
This commit is contained in:
@@ -0,0 +1,184 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.operate;
|
||||||
|
|
||||||
|
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.coding.ShellCommandTool;
|
||||||
|
import io.agentscope.core.tool.file.ReadFileTool;
|
||||||
|
import io.agentscope.core.tool.file.WriteFileTool;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AgentScope 内置操作工具适配器。
|
||||||
|
*
|
||||||
|
* <p>该适配器只负责将 Easy-Agents 的操作工具声明转换为 AgentScope Toolkit 中的原生工具。
|
||||||
|
* Shell 工具的人工审批不使用 AgentScope {@code ShellCommandTool} 的同步 callback,而是通过
|
||||||
|
* Easy-Agents 现有 {@code ToolHitlInterceptor} 统一处理,以保持 SSE 暂停、恢复和审计语义一致。
|
||||||
|
*/
|
||||||
|
public class AgentOperateToolAdapter {
|
||||||
|
|
||||||
|
public static final String VIEW_TEXT_FILE_TOOL = "view_text_file";
|
||||||
|
public static final String LIST_DIRECTORY_TOOL = "list_directory";
|
||||||
|
public static final String WRITE_TEXT_FILE_TOOL = "write_text_file";
|
||||||
|
public static final String INSERT_TEXT_FILE_TOOL = "insert_text_file";
|
||||||
|
public static final String EXECUTE_SHELL_COMMAND_TOOL = "execute_shell_command";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将操作工具声明注册到 Toolkit,并返回供 HITL 拦截器使用的工具声明。
|
||||||
|
*
|
||||||
|
* @param specs 操作工具声明
|
||||||
|
* @param toolkit AgentScope Toolkit
|
||||||
|
* @return 已启用操作工具对应的 AgentToolSpec
|
||||||
|
*/
|
||||||
|
public List<AgentToolSpec> register(List<AgentOperateToolSpec> specs, Toolkit toolkit) {
|
||||||
|
List<AgentToolSpec> toolSpecs = new ArrayList<>();
|
||||||
|
if (specs == null || specs.isEmpty()) {
|
||||||
|
return toolSpecs;
|
||||||
|
}
|
||||||
|
for (AgentOperateToolSpec spec : specs) {
|
||||||
|
if (spec == null || !spec.isEnabled()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
registerOne(spec, toolkit, toolSpecs);
|
||||||
|
}
|
||||||
|
return toolSpecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析已启用操作工具会注册到 AgentScope 的工具名称。
|
||||||
|
*
|
||||||
|
* @param specs 操作工具声明
|
||||||
|
* @return 工具名称集合
|
||||||
|
*/
|
||||||
|
public Set<String> enabledToolNames(List<AgentOperateToolSpec> specs) {
|
||||||
|
Set<String> names = new LinkedHashSet<>();
|
||||||
|
if (specs == null || specs.isEmpty()) {
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
for (AgentOperateToolSpec spec : specs) {
|
||||||
|
if (spec == null || !spec.isEnabled() || spec.getType() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (spec.getType()) {
|
||||||
|
case READ_FILE -> {
|
||||||
|
names.add(VIEW_TEXT_FILE_TOOL);
|
||||||
|
names.add(LIST_DIRECTORY_TOOL);
|
||||||
|
}
|
||||||
|
case WRITE_FILE -> {
|
||||||
|
names.add(WRITE_TEXT_FILE_TOOL);
|
||||||
|
names.add(INSERT_TEXT_FILE_TOOL);
|
||||||
|
}
|
||||||
|
case SHELL -> names.add(EXECUTE_SHELL_COMMAND_TOOL);
|
||||||
|
default -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerOne(AgentOperateToolSpec spec, Toolkit toolkit, List<AgentToolSpec> toolSpecs) {
|
||||||
|
AgentOperateToolType type = spec.getType();
|
||||||
|
if (type == null) {
|
||||||
|
throw new AgentRuntimeException("Agent operate tool type is required.");
|
||||||
|
}
|
||||||
|
Path baseDir = validateBaseDir(spec);
|
||||||
|
switch (type) {
|
||||||
|
case READ_FILE -> {
|
||||||
|
assertNoToolConflict(toolkit, VIEW_TEXT_FILE_TOOL);
|
||||||
|
assertNoToolConflict(toolkit, LIST_DIRECTORY_TOOL);
|
||||||
|
toolkit.registerTool(new ReadFileTool(baseDir.toString()));
|
||||||
|
toolSpecs.add(toolSpec(spec, VIEW_TEXT_FILE_TOOL, "View text file content.", false));
|
||||||
|
toolSpecs.add(toolSpec(spec, LIST_DIRECTORY_TOOL, "List files and directories.", false));
|
||||||
|
}
|
||||||
|
case WRITE_FILE -> {
|
||||||
|
assertNoToolConflict(toolkit, WRITE_TEXT_FILE_TOOL);
|
||||||
|
assertNoToolConflict(toolkit, INSERT_TEXT_FILE_TOOL);
|
||||||
|
toolkit.registerTool(new WriteFileTool(baseDir.toString()));
|
||||||
|
toolSpecs.add(toolSpec(spec, WRITE_TEXT_FILE_TOOL, "Write or replace text file content.", true));
|
||||||
|
toolSpecs.add(toolSpec(spec, INSERT_TEXT_FILE_TOOL, "Insert text into a file.", true));
|
||||||
|
}
|
||||||
|
case SHELL -> {
|
||||||
|
assertNoToolConflict(toolkit, EXECUTE_SHELL_COMMAND_TOOL);
|
||||||
|
Charset charset = parseCharset(spec);
|
||||||
|
toolkit.registerAgentTool(new ShellCommandTool(baseDir.toString(), spec.getShellAllowedCommands(), null,
|
||||||
|
null, charset));
|
||||||
|
toolSpecs.add(toolSpec(spec, EXECUTE_SHELL_COMMAND_TOOL, "Execute shell command.", true));
|
||||||
|
}
|
||||||
|
default -> throw new AgentRuntimeException("Unsupported agent operate tool type: " + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path validateBaseDir(AgentOperateToolSpec spec) {
|
||||||
|
String baseDir = spec.getBaseDir();
|
||||||
|
if (baseDir == null || baseDir.isBlank()) {
|
||||||
|
throw new AgentRuntimeException("Agent operate tool baseDir is required.");
|
||||||
|
}
|
||||||
|
Path path = Path.of(baseDir).toAbsolutePath().normalize();
|
||||||
|
if (!Path.of(baseDir).isAbsolute()) {
|
||||||
|
throw new AgentRuntimeException("Agent operate tool baseDir must be an absolute path: " + baseDir);
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Charset parseCharset(AgentOperateToolSpec spec) {
|
||||||
|
String charsetName = spec.getShellCharset();
|
||||||
|
if (charsetName == null || charsetName.isBlank()) {
|
||||||
|
return StandardCharsets.UTF_8;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Charset.forName(charsetName.trim());
|
||||||
|
} catch (Exception error) {
|
||||||
|
throw new AgentRuntimeException("Invalid shell charset: " + charsetName, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolSpec toolSpec(AgentOperateToolSpec operateSpec,
|
||||||
|
String toolName,
|
||||||
|
String description,
|
||||||
|
boolean defaultApprovalRequired) {
|
||||||
|
boolean approvalRequired = operateSpec.getApprovalRequired() == null
|
||||||
|
? defaultApprovalRequired : operateSpec.getApprovalRequired();
|
||||||
|
AgentToolSpec toolSpec = new AgentToolSpec();
|
||||||
|
toolSpec.setName(toolName);
|
||||||
|
toolSpec.setDescription(description);
|
||||||
|
toolSpec.setCategory(AgentToolCategory.CUSTOM);
|
||||||
|
toolSpec.setVisibility(AgentToolVisibility.VISIBLE);
|
||||||
|
toolSpec.setApprovalRequired(approvalRequired);
|
||||||
|
toolSpec.setApprovalRequest(approvalRequest(operateSpec, approvalRequired));
|
||||||
|
toolSpec.setMetadata(metadata(operateSpec));
|
||||||
|
return toolSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolApprovalRequest approvalRequest(AgentOperateToolSpec operateSpec, boolean approvalRequired) {
|
||||||
|
AgentToolApprovalRequest request = operateSpec.getApprovalRequest();
|
||||||
|
if (request != null) {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
AgentToolApprovalRequest defaultRequest = new AgentToolApprovalRequest();
|
||||||
|
if (approvalRequired) {
|
||||||
|
defaultRequest.setApprovalPrompt("请确认是否允许智能体执行该操作工具。");
|
||||||
|
}
|
||||||
|
return defaultRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> metadata(AgentOperateToolSpec spec) {
|
||||||
|
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
metadata.put("operateTool", true);
|
||||||
|
metadata.put("operateToolType", spec.getType().name());
|
||||||
|
metadata.put("baseDir", spec.getBaseDir());
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertNoToolConflict(Toolkit toolkit, String toolName) {
|
||||||
|
if (toolkit.getTool(toolName) != null) {
|
||||||
|
throw new AgentRuntimeException("Agent operate tool conflicts with existing tool: " + toolName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.operate;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
||||||
|
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 操作类工具声明。
|
||||||
|
*
|
||||||
|
* <p>操作类工具是 runtime 直接适配的 AgentScope 内置工具,用于读文件、写文件和执行 Shell。
|
||||||
|
* 这些工具直接作用于后端 JVM 所在宿主环境,调用方必须按 agent、session 或 user 维度传入受控
|
||||||
|
* 的绝对工作目录。
|
||||||
|
*/
|
||||||
|
public class AgentOperateToolSpec {
|
||||||
|
|
||||||
|
private AgentOperateToolType type;
|
||||||
|
private boolean enabled = true;
|
||||||
|
private String baseDir;
|
||||||
|
private Boolean approvalRequired;
|
||||||
|
private AgentToolApprovalRequest approvalRequest;
|
||||||
|
private Set<String> shellAllowedCommands = new LinkedHashSet<>();
|
||||||
|
private String shellCharset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取操作工具类型。
|
||||||
|
*
|
||||||
|
* @return 操作工具类型
|
||||||
|
*/
|
||||||
|
public AgentOperateToolType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置操作工具类型。
|
||||||
|
*
|
||||||
|
* @param type 操作工具类型
|
||||||
|
*/
|
||||||
|
public void setType(AgentOperateToolType type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回是否启用该操作工具。
|
||||||
|
*
|
||||||
|
* @return 启用时为 true
|
||||||
|
*/
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否启用该操作工具。
|
||||||
|
*
|
||||||
|
* @param enabled 启用标记
|
||||||
|
*/
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取操作工具工作目录。
|
||||||
|
*
|
||||||
|
* @return 绝对工作目录
|
||||||
|
*/
|
||||||
|
public String getBaseDir() {
|
||||||
|
return baseDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置操作工具工作目录。
|
||||||
|
*
|
||||||
|
* @param baseDir 绝对工作目录
|
||||||
|
*/
|
||||||
|
public void setBaseDir(String baseDir) {
|
||||||
|
this.baseDir = baseDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审批开关覆盖值。
|
||||||
|
*
|
||||||
|
* @return 审批覆盖值,null 表示使用工具类型默认值
|
||||||
|
*/
|
||||||
|
public Boolean getApprovalRequired() {
|
||||||
|
return approvalRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置审批开关覆盖值。
|
||||||
|
*
|
||||||
|
* @param approvalRequired 审批覆盖值
|
||||||
|
*/
|
||||||
|
public void setApprovalRequired(Boolean approvalRequired) {
|
||||||
|
this.approvalRequired = approvalRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审批请求配置。
|
||||||
|
*
|
||||||
|
* @return 审批请求配置
|
||||||
|
*/
|
||||||
|
public AgentToolApprovalRequest getApprovalRequest() {
|
||||||
|
return approvalRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置审批请求配置。
|
||||||
|
*
|
||||||
|
* @param approvalRequest 审批请求配置
|
||||||
|
*/
|
||||||
|
public void setApprovalRequest(AgentToolApprovalRequest approvalRequest) {
|
||||||
|
this.approvalRequest = approvalRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Shell 命令白名单。
|
||||||
|
*
|
||||||
|
* @return Shell 命令白名单
|
||||||
|
*/
|
||||||
|
public Set<String> getShellAllowedCommands() {
|
||||||
|
return shellAllowedCommands;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Shell 命令白名单。
|
||||||
|
*
|
||||||
|
* @param shellAllowedCommands Shell 命令白名单
|
||||||
|
*/
|
||||||
|
public void setShellAllowedCommands(Set<String> shellAllowedCommands) {
|
||||||
|
this.shellAllowedCommands = shellAllowedCommands == null ? new LinkedHashSet<>() : new LinkedHashSet<>(shellAllowedCommands);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Shell 输出字符集名称。
|
||||||
|
*
|
||||||
|
* @return 字符集名称
|
||||||
|
*/
|
||||||
|
public String getShellCharset() {
|
||||||
|
return shellCharset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Shell 输出字符集名称。
|
||||||
|
*
|
||||||
|
* @param shellCharset 字符集名称
|
||||||
|
*/
|
||||||
|
public void setShellCharset(String shellCharset) {
|
||||||
|
this.shellCharset = shellCharset;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.operate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 操作类工具类型。
|
||||||
|
*/
|
||||||
|
public enum AgentOperateToolType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取文本文件与列出目录。
|
||||||
|
*/
|
||||||
|
READ_FILE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入、覆盖或插入文本文件。
|
||||||
|
*/
|
||||||
|
WRITE_FILE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在服务进程所在宿主环境执行 Shell 命令。
|
||||||
|
*/
|
||||||
|
SHELL
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.operate;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||||
|
import io.agentscope.core.tool.Toolkit;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试 Agent 操作类工具适配器。
|
||||||
|
*/
|
||||||
|
public class AgentOperateToolAdapterTest {
|
||||||
|
|
||||||
|
private final AgentOperateToolAdapter adapter = new AgentOperateToolAdapter();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRegisterReadFileToolsWithDefaultHitlDisabled() {
|
||||||
|
Toolkit toolkit = new Toolkit();
|
||||||
|
AgentOperateToolSpec spec = spec(AgentOperateToolType.READ_FILE);
|
||||||
|
|
||||||
|
List<AgentToolSpec> toolSpecs = adapter.register(List.of(spec), toolkit);
|
||||||
|
|
||||||
|
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.VIEW_TEXT_FILE_TOOL));
|
||||||
|
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.LIST_DIRECTORY_TOOL));
|
||||||
|
Assert.assertEquals(2, toolSpecs.size());
|
||||||
|
Assert.assertTrue(toolSpecs.stream().noneMatch(AgentToolSpec::isApprovalRequired));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRegisterWriteFileToolsWithDefaultHitlEnabled() {
|
||||||
|
Toolkit toolkit = new Toolkit();
|
||||||
|
AgentOperateToolSpec spec = spec(AgentOperateToolType.WRITE_FILE);
|
||||||
|
|
||||||
|
List<AgentToolSpec> toolSpecs = adapter.register(List.of(spec), toolkit);
|
||||||
|
|
||||||
|
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.WRITE_TEXT_FILE_TOOL));
|
||||||
|
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.INSERT_TEXT_FILE_TOOL));
|
||||||
|
Assert.assertEquals(2, toolSpecs.size());
|
||||||
|
Assert.assertTrue(toolSpecs.stream().allMatch(AgentToolSpec::isApprovalRequired));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRegisterShellToolWithEmptyWhitelist() {
|
||||||
|
Toolkit toolkit = new Toolkit();
|
||||||
|
AgentOperateToolSpec spec = spec(AgentOperateToolType.SHELL);
|
||||||
|
spec.setShellAllowedCommands(Set.of());
|
||||||
|
|
||||||
|
List<AgentToolSpec> toolSpecs = adapter.register(List.of(spec), toolkit);
|
||||||
|
|
||||||
|
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.EXECUTE_SHELL_COMMAND_TOOL));
|
||||||
|
Assert.assertEquals(1, toolSpecs.size());
|
||||||
|
Assert.assertTrue(toolSpecs.get(0).isApprovalRequired());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldSkipDisabledOperateTool() {
|
||||||
|
Toolkit toolkit = new Toolkit();
|
||||||
|
AgentOperateToolSpec spec = spec(AgentOperateToolType.SHELL);
|
||||||
|
spec.setEnabled(false);
|
||||||
|
|
||||||
|
List<AgentToolSpec> toolSpecs = adapter.register(List.of(spec), toolkit);
|
||||||
|
|
||||||
|
Assert.assertNull(toolkit.getTool(AgentOperateToolAdapter.EXECUTE_SHELL_COMMAND_TOOL));
|
||||||
|
Assert.assertTrue(toolSpecs.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = AgentRuntimeException.class)
|
||||||
|
public void shouldRejectMissingBaseDir() {
|
||||||
|
AgentOperateToolSpec spec = spec(AgentOperateToolType.READ_FILE);
|
||||||
|
spec.setBaseDir(null);
|
||||||
|
|
||||||
|
adapter.register(List.of(spec), new Toolkit());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = AgentRuntimeException.class)
|
||||||
|
public void shouldRejectRelativeBaseDir() {
|
||||||
|
AgentOperateToolSpec spec = spec(AgentOperateToolType.READ_FILE);
|
||||||
|
spec.setBaseDir("relative/workspace");
|
||||||
|
|
||||||
|
adapter.register(List.of(spec), new Toolkit());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = AgentRuntimeException.class)
|
||||||
|
public void shouldRejectToolNameConflict() {
|
||||||
|
Toolkit toolkit = new Toolkit();
|
||||||
|
AgentOperateToolSpec first = spec(AgentOperateToolType.READ_FILE);
|
||||||
|
AgentOperateToolSpec second = spec(AgentOperateToolType.READ_FILE);
|
||||||
|
|
||||||
|
adapter.register(List.of(first, second), toolkit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentOperateToolSpec spec(AgentOperateToolType type) {
|
||||||
|
AgentOperateToolSpec spec = new AgentOperateToolSpec();
|
||||||
|
spec.setType(type);
|
||||||
|
spec.setBaseDir(System.getProperty("java.io.tmpdir"));
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user