feat: 归档 XL10 异步工具业务编译层
- 将 AgentDefinitionCompiler 升级为 AgentRuntimeCompiler - 接入 Workflow 和 Plugin 的同步/异步工具编译与 Redis 任务态 - 增加异步执行配置开关、聊天时间线聚合和后端测试
This commit is contained in:
@@ -8,6 +8,7 @@ import org.junit.Test;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.enums.AgentToolType;
|
||||
import tech.easyflow.agent.runtime.tool.AgentToolRuntimeCompiler;
|
||||
import tech.easyflow.ai.entity.Mcp;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.entity.ModelProvider;
|
||||
@@ -36,10 +37,13 @@ public class AgentDefinitionCompilerMcpTest {
|
||||
BigInteger mcpId = BigInteger.valueOf(20L);
|
||||
Model model = model(modelId);
|
||||
Mcp mcp = mcp(mcpId);
|
||||
AgentDefinitionCompiler compiler = new AgentDefinitionCompiler();
|
||||
AgentRuntimeCompiler compiler = new AgentRuntimeCompiler();
|
||||
AgentToolRuntimeCompiler toolCompiler = new AgentToolRuntimeCompiler();
|
||||
setField(compiler, "objectMapper", new com.fasterxml.jackson.databind.ObjectMapper());
|
||||
setField(compiler, "modelService", modelService(model));
|
||||
setField(compiler, "mcpService", mcpService(mcp));
|
||||
setField(toolCompiler, "objectMapper", new com.fasterxml.jackson.databind.ObjectMapper());
|
||||
setField(toolCompiler, "mcpService", mcpService(mcp));
|
||||
setField(compiler, "agentToolRuntimeCompiler", toolCompiler);
|
||||
|
||||
Agent agent = agent(modelId, mcpId);
|
||||
|
||||
|
||||
@@ -429,11 +429,11 @@ public class AgentRunServiceDraftAndHitlTest {
|
||||
@Test
|
||||
public void startRuntimeShouldUseDraftSessionStoreWithoutBindingMysqlSession() throws Exception {
|
||||
AgentRunService service = new AgentRunService();
|
||||
RecordingAgentDefinitionCompiler compiler = new RecordingAgentDefinitionCompiler();
|
||||
RecordingAgentRuntimeCompiler compiler = new RecordingAgentRuntimeCompiler();
|
||||
RecordingAgentRuntime runtime = new RecordingAgentRuntime();
|
||||
RecordingAgentRuntimeFactory runtimeFactory = new RecordingAgentRuntimeFactory(runtime);
|
||||
AgentSessionStore draftStore = new InMemoryAgentSessionStore();
|
||||
setField(service, "agentDefinitionCompiler", compiler);
|
||||
setField(service, "agentRuntimeCompiler", compiler);
|
||||
setField(service, "agentRuntimeFactory", runtimeFactory);
|
||||
setField(service, "agentRunRegistry", new AgentRunRegistry());
|
||||
|
||||
@@ -1001,7 +1001,7 @@ public class AgentRunServiceDraftAndHitlTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static class RecordingAgentDefinitionCompiler extends AgentDefinitionCompiler {
|
||||
private static class RecordingAgentRuntimeCompiler extends AgentRuntimeCompiler {
|
||||
|
||||
@Override
|
||||
public AgentRuntimeBundle compile(Agent agent) {
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
package tech.easyflow.agent.runtime.asynctool;
|
||||
|
||||
import com.easyagents.agent.runtime.tool.AgentToolContext;
|
||||
import com.easyagents.agent.runtime.tool.asynctool.AsyncToolCancelRequest;
|
||||
import com.easyagents.agent.runtime.tool.asynctool.AsyncToolObserveRequest;
|
||||
import com.easyagents.agent.runtime.tool.asynctool.AsyncToolResultRequest;
|
||||
import com.easyagents.agent.runtime.tool.asynctool.AsyncToolSubmitResult;
|
||||
import com.easyagents.agent.runtime.tool.asynctool.AsyncToolTaskStatus;
|
||||
import com.easyagents.agent.runtime.tool.asynctool.AsyncToolTaskView;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import tech.easyflow.agent.runtime.tool.AgentToolExecutionResult;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* EasyFlow 异步业务工具基类测试。
|
||||
*/
|
||||
public class AbstractAgentAsyncSubToolsTest {
|
||||
|
||||
/**
|
||||
* 验证 submit、observe、result 与 list 的基础任务生命周期。
|
||||
*
|
||||
* @throws Exception 等待后台执行超时时抛出
|
||||
*/
|
||||
@Test
|
||||
public void asyncSubToolsShouldSubmitObserveResultAndListCurrentSessionTasks() throws Exception {
|
||||
ThreadPoolTaskExecutor executor = executor();
|
||||
try {
|
||||
InMemoryTaskStore store = new InMemoryTaskStore();
|
||||
TestAsyncSubTools subTools = new TestAsyncSubTools(store, executor);
|
||||
AgentToolContext context = context("session-a");
|
||||
|
||||
AsyncToolSubmitResult submitted = subTools.submit(Map.of("keyword", "hello"), context);
|
||||
Assert.assertEquals(AsyncToolTaskStatus.PENDING, submitted.getStatus());
|
||||
Assert.assertTrue(submitted.getTaskId().startsWith("async_"));
|
||||
|
||||
AsyncToolTaskView completed = waitTerminal(subTools, submitted.getTaskId(), context);
|
||||
Assert.assertEquals(AsyncToolTaskStatus.SUCCEEDED, completed.getStatus());
|
||||
Assert.assertEquals(Map.of("echo", "hello"), completed.getResult());
|
||||
Assert.assertTrue(completed.getNextCursor() >= 2L);
|
||||
|
||||
AsyncToolResultRequest resultRequest = new AsyncToolResultRequest();
|
||||
resultRequest.setTaskId(submitted.getTaskId());
|
||||
resultRequest.setCursor(1L);
|
||||
AsyncToolTaskView result = subTools.result(resultRequest, context);
|
||||
Assert.assertEquals(AsyncToolTaskStatus.SUCCEEDED, result.getStatus());
|
||||
Assert.assertEquals(Map.of("echo", "hello"), result.getResult());
|
||||
Assert.assertFalse(result.getEvents().isEmpty());
|
||||
|
||||
Assert.assertEquals(1, subTools.list(null, context).getTasks().size());
|
||||
Assert.assertTrue(subTools.list(null, context("session-b")).getTasks().isEmpty());
|
||||
|
||||
AsyncToolTaskView crossedSessionView = observe(subTools, submitted.getTaskId(), context("session-b"));
|
||||
Assert.assertEquals(AsyncToolTaskStatus.FAILED, crossedSessionView.getStatus());
|
||||
Assert.assertEquals("TASK_NOT_FOUND", crossedSessionView.getErrorType());
|
||||
} finally {
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证首版取消语义返回明确失败结果。
|
||||
*/
|
||||
@Test
|
||||
public void cancelShouldReturnUnsupportedFailure() {
|
||||
TestAsyncSubTools subTools = new TestAsyncSubTools(new InMemoryTaskStore(), executor());
|
||||
AsyncToolCancelRequest request = new AsyncToolCancelRequest();
|
||||
request.setTaskId("task-1");
|
||||
|
||||
var result = subTools.cancel(request, context("session-a"));
|
||||
|
||||
Assert.assertEquals(AsyncToolTaskStatus.FAILED, result.getStatus());
|
||||
Assert.assertEquals("不支持取消", result.getMessage());
|
||||
}
|
||||
|
||||
private AsyncToolTaskView waitTerminal(TestAsyncSubTools subTools, String taskId, AgentToolContext context) throws Exception {
|
||||
long deadline = System.currentTimeMillis() + 3000L;
|
||||
AsyncToolTaskView view = observe(subTools, taskId, context);
|
||||
while (!Boolean.TRUE.equals(view.getTerminal()) && System.currentTimeMillis() < deadline) {
|
||||
Thread.sleep(20L);
|
||||
view = observe(subTools, taskId, context);
|
||||
}
|
||||
Assert.assertTrue("异步任务应在测试超时前完成", Boolean.TRUE.equals(view.getTerminal()));
|
||||
return view;
|
||||
}
|
||||
|
||||
private AsyncToolTaskView observe(TestAsyncSubTools subTools, String taskId, AgentToolContext context) {
|
||||
AsyncToolObserveRequest request = new AsyncToolObserveRequest();
|
||||
request.setTaskId(taskId);
|
||||
request.setCursor(0L);
|
||||
return subTools.observe(request, context);
|
||||
}
|
||||
|
||||
private AgentToolContext context(String sessionId) {
|
||||
AgentToolContext context = new AgentToolContext();
|
||||
context.setRequestId("request-1");
|
||||
context.setTraceId("trace-1");
|
||||
context.setSessionId(sessionId);
|
||||
context.setAgentId("agent-1");
|
||||
context.setToolCallId("tool-call-1");
|
||||
return context;
|
||||
}
|
||||
|
||||
private ThreadPoolTaskExecutor executor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(1);
|
||||
executor.setMaxPoolSize(1);
|
||||
executor.setQueueCapacity(4);
|
||||
executor.setThreadNamePrefix("async-sub-tools-test-");
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
|
||||
private static final class TestAsyncSubTools extends AbstractAgentAsyncSubTools {
|
||||
|
||||
private TestAsyncSubTools(AgentAsyncToolTaskStore taskStore, ThreadPoolTaskExecutor taskExecutor) {
|
||||
super(taskStore, taskExecutor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String toolType() {
|
||||
return "PLUGIN";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String toolName() {
|
||||
return "test_tool";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String displayName() {
|
||||
return "测试工具";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String businessId() {
|
||||
return "business-1";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AgentToolExecutionResult executeBusiness(Map<String, Object> arguments) {
|
||||
return new AgentToolExecutionResult(Map.of("echo", arguments.get("keyword")), "business-run-1");
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InMemoryTaskStore implements AgentAsyncToolTaskStore {
|
||||
|
||||
private final Map<String, AgentAsyncToolTaskRecord> records = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void create(AgentAsyncToolTaskRecord record) {
|
||||
record.setSessionScopedKey(key(record.getSessionId(), record.getTaskId()));
|
||||
records.put(record.getSessionScopedKey(), record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AgentAsyncToolTaskRecord> get(String sessionId, String taskId) {
|
||||
return Optional.ofNullable(records.get(key(sessionId, taskId)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AgentAsyncToolTaskRecord> update(String sessionId,
|
||||
String taskId,
|
||||
UnaryOperator<AgentAsyncToolTaskRecord> updater) {
|
||||
String key = key(sessionId, taskId);
|
||||
AgentAsyncToolTaskRecord updated = records.computeIfPresent(key,
|
||||
(ignored, existing) -> updater == null ? existing : updater.apply(existing));
|
||||
return Optional.ofNullable(updated);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentAsyncToolTaskRecord> list(String sessionId, AsyncToolTaskStatus status) {
|
||||
List<AgentAsyncToolTaskRecord> result = new ArrayList<>();
|
||||
for (AgentAsyncToolTaskRecord record : records.values()) {
|
||||
if (sessionId.equals(record.getSessionId()) && (status == null || status == record.getStatus())) {
|
||||
result.add(record);
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparing(AgentAsyncToolTaskRecord::getCreatedAt).reversed());
|
||||
return result;
|
||||
}
|
||||
|
||||
private String key(String sessionId, String taskId) {
|
||||
return sessionId + ":" + taskId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package tech.easyflow.agent.runtime.asynctool;
|
||||
|
||||
import com.easyagents.agent.runtime.tool.AgentToolContext;
|
||||
import com.easyagents.agent.runtime.tool.asynctool.AsyncToolObserveRequest;
|
||||
import com.easyagents.agent.runtime.tool.asynctool.AsyncToolResultRequest;
|
||||
import com.easyagents.agent.runtime.tool.asynctool.AsyncToolSubmitResult;
|
||||
import com.easyagents.agent.runtime.tool.asynctool.AsyncToolTaskStatus;
|
||||
import com.easyagents.agent.runtime.tool.asynctool.AsyncToolTaskView;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import tech.easyflow.agent.runtime.tool.AgentToolExecutionResult;
|
||||
import tech.easyflow.agent.runtime.tool.PluginToolExecutor;
|
||||
import tech.easyflow.agent.runtime.tool.WorkflowToolExecutor;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
* Workflow 与 Plugin 异步子工具测试。
|
||||
*/
|
||||
public class WorkflowPluginAsyncSubToolsTest {
|
||||
|
||||
/**
|
||||
* 验证 Workflow 异步子工具会把业务执行结果保留到任务视图。
|
||||
*
|
||||
* @throws Exception 等待后台执行超时时抛出
|
||||
*/
|
||||
@Test
|
||||
public void workflowAsyncSubToolsShouldKeepBusinessResultInTaskView() throws Exception {
|
||||
ThreadPoolTaskExecutor executor = executor();
|
||||
try {
|
||||
Map<String, Object> businessResult = Map.of("workflowOutput", "ok");
|
||||
WorkflowAsyncSubTools subTools = new WorkflowAsyncSubTools(workflow(),
|
||||
"workflow_demo",
|
||||
"测试工作流",
|
||||
new StubWorkflowToolExecutor(businessResult),
|
||||
new InMemoryTaskStore(),
|
||||
executor);
|
||||
|
||||
AsyncToolTaskView view = submitAndResult(subTools);
|
||||
|
||||
Assert.assertEquals(AsyncToolTaskStatus.SUCCEEDED, view.getStatus());
|
||||
Assert.assertEquals(businessResult, view.getResult());
|
||||
Assert.assertEquals("workflow-run-1", view.getPayload().get("businessExecutionId"));
|
||||
} finally {
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Plugin 异步子工具会把业务执行结果保留到任务视图。
|
||||
*
|
||||
* @throws Exception 等待后台执行超时时抛出
|
||||
*/
|
||||
@Test
|
||||
public void pluginAsyncSubToolsShouldKeepBusinessResultInTaskView() throws Exception {
|
||||
ThreadPoolTaskExecutor executor = executor();
|
||||
try {
|
||||
Map<String, Object> businessResult = Map.of("pluginOutput", List.of("a", "b"));
|
||||
PluginAsyncSubTools subTools = new PluginAsyncSubTools(pluginItem(),
|
||||
"plugin_demo",
|
||||
"测试插件",
|
||||
new StubPluginToolExecutor(businessResult),
|
||||
new InMemoryTaskStore(),
|
||||
executor);
|
||||
|
||||
AsyncToolTaskView view = submitAndResult(subTools);
|
||||
|
||||
Assert.assertEquals(AsyncToolTaskStatus.SUCCEEDED, view.getStatus());
|
||||
Assert.assertEquals(businessResult, view.getResult());
|
||||
} finally {
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private AsyncToolTaskView submitAndResult(AbstractAgentAsyncSubTools subTools) throws Exception {
|
||||
AgentToolContext context = context();
|
||||
AsyncToolSubmitResult submitted = subTools.submit(Map.of("keyword", "hello"), context);
|
||||
waitTerminal(subTools, submitted.getTaskId(), context);
|
||||
AsyncToolResultRequest request = new AsyncToolResultRequest();
|
||||
request.setTaskId(submitted.getTaskId());
|
||||
return subTools.result(request, context);
|
||||
}
|
||||
|
||||
private void waitTerminal(AbstractAgentAsyncSubTools subTools, String taskId, AgentToolContext context) throws Exception {
|
||||
long deadline = System.currentTimeMillis() + 3000L;
|
||||
AsyncToolTaskView view = observe(subTools, taskId, context);
|
||||
while (!Boolean.TRUE.equals(view.getTerminal()) && System.currentTimeMillis() < deadline) {
|
||||
Thread.sleep(20L);
|
||||
view = observe(subTools, taskId, context);
|
||||
}
|
||||
Assert.assertTrue("异步任务应在测试超时前完成", Boolean.TRUE.equals(view.getTerminal()));
|
||||
}
|
||||
|
||||
private AsyncToolTaskView observe(AbstractAgentAsyncSubTools subTools, String taskId, AgentToolContext context) {
|
||||
AsyncToolObserveRequest request = new AsyncToolObserveRequest();
|
||||
request.setTaskId(taskId);
|
||||
return subTools.observe(request, context);
|
||||
}
|
||||
|
||||
private AgentToolContext context() {
|
||||
AgentToolContext context = new AgentToolContext();
|
||||
context.setRequestId("request-1");
|
||||
context.setTraceId("trace-1");
|
||||
context.setSessionId("session-1");
|
||||
context.setAgentId("agent-1");
|
||||
context.setToolCallId("tool-call-1");
|
||||
return context;
|
||||
}
|
||||
|
||||
private Workflow workflow() {
|
||||
Workflow workflow = new Workflow();
|
||||
workflow.setId(BigInteger.valueOf(101L));
|
||||
workflow.setTitle("测试工作流");
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private PluginItem pluginItem() {
|
||||
PluginItem pluginItem = new PluginItem();
|
||||
pluginItem.setId(BigInteger.valueOf(102L));
|
||||
pluginItem.setName("测试插件");
|
||||
return pluginItem;
|
||||
}
|
||||
|
||||
private ThreadPoolTaskExecutor executor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(1);
|
||||
executor.setMaxPoolSize(1);
|
||||
executor.setQueueCapacity(4);
|
||||
executor.setThreadNamePrefix("workflow-plugin-async-test-");
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
|
||||
private static final class StubWorkflowToolExecutor extends WorkflowToolExecutor {
|
||||
|
||||
private final Map<String, Object> businessResult;
|
||||
|
||||
private StubWorkflowToolExecutor(Map<String, Object> businessResult) {
|
||||
super(null);
|
||||
this.businessResult = businessResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentToolExecutionResult execute(Workflow workflow, Map<String, Object> arguments) {
|
||||
return new AgentToolExecutionResult(businessResult, "workflow-run-1");
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StubPluginToolExecutor extends PluginToolExecutor {
|
||||
|
||||
private final Map<String, Object> businessResult;
|
||||
|
||||
private StubPluginToolExecutor(Map<String, Object> businessResult) {
|
||||
this.businessResult = businessResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentToolExecutionResult execute(PluginItem pluginItem, Map<String, Object> arguments) {
|
||||
return new AgentToolExecutionResult(businessResult, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InMemoryTaskStore implements AgentAsyncToolTaskStore {
|
||||
|
||||
private final Map<String, AgentAsyncToolTaskRecord> records = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void create(AgentAsyncToolTaskRecord record) {
|
||||
record.setSessionScopedKey(key(record.getSessionId(), record.getTaskId()));
|
||||
records.put(record.getSessionScopedKey(), record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AgentAsyncToolTaskRecord> get(String sessionId, String taskId) {
|
||||
return Optional.ofNullable(records.get(key(sessionId, taskId)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AgentAsyncToolTaskRecord> update(String sessionId,
|
||||
String taskId,
|
||||
UnaryOperator<AgentAsyncToolTaskRecord> updater) {
|
||||
AgentAsyncToolTaskRecord updated = records.computeIfPresent(key(sessionId, taskId),
|
||||
(ignored, existing) -> updater == null ? existing : updater.apply(existing));
|
||||
return Optional.ofNullable(updated);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentAsyncToolTaskRecord> list(String sessionId, AsyncToolTaskStatus status) {
|
||||
List<AgentAsyncToolTaskRecord> result = new ArrayList<>();
|
||||
for (AgentAsyncToolTaskRecord record : records.values()) {
|
||||
if (sessionId.equals(record.getSessionId()) && (status == null || status == record.getStatus())) {
|
||||
result.add(record);
|
||||
}
|
||||
}
|
||||
result.sort(Comparator.comparing(AgentAsyncToolTaskRecord::getCreatedAt).reversed());
|
||||
return result;
|
||||
}
|
||||
|
||||
private String key(String sessionId, String taskId) {
|
||||
return sessionId + ":" + taskId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package tech.easyflow.agent.runtime.tool;
|
||||
|
||||
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||
import com.easyagents.core.model.chat.tool.Parameter;
|
||||
import com.easyagents.core.model.chat.tool.Tool;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.agent.entity.Agent;
|
||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||
import tech.easyflow.agent.enums.AgentToolType;
|
||||
import tech.easyflow.ai.entity.PluginItem;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Agent 工具运行时编译测试。
|
||||
*/
|
||||
public class AgentToolRuntimeCompilerTest {
|
||||
|
||||
/**
|
||||
* 验证 Workflow 默认按同步工具编译。
|
||||
*
|
||||
* @throws Exception 反射注入依赖失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void compileShouldUseSyncModeByDefault() throws Exception {
|
||||
AgentToolRuntimeCompiler compiler = compiler();
|
||||
|
||||
AgentToolRuntimeCompilation compilation = compiler.compile(agent(workflowBinding(null, false, "flow-sync")));
|
||||
|
||||
Assert.assertEquals(List.of("flow-sync"), toolNames(compilation));
|
||||
Assert.assertEquals(1, compilation.getToolInvokers().size());
|
||||
Assert.assertFalse(compilation.getToolSpecs().get(0).isApprovalRequired());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证非法执行模式会回退为同步工具。
|
||||
*
|
||||
* @throws Exception 反射注入依赖失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void compileShouldFallbackToSyncWhenExecutionModeInvalid() throws Exception {
|
||||
AgentToolRuntimeCompiler compiler = compiler();
|
||||
|
||||
AgentToolRuntimeCompilation compilation = compiler.compile(agent(workflowBinding("BAD", false, "flow-sync")));
|
||||
|
||||
Assert.assertEquals(List.of("flow-sync"), toolNames(compilation));
|
||||
Assert.assertEquals(1, compilation.getToolInvokers().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Workflow 异步模式会展开为五个固定子工具。
|
||||
*
|
||||
* @throws Exception 反射注入依赖失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void compileShouldExpandWorkflowAsyncSubToolsAndNormalizeName() throws Exception {
|
||||
AgentToolRuntimeCompiler compiler = compiler();
|
||||
|
||||
AgentToolRuntimeCompilation compilation = compiler.compile(agent(workflowBinding("ASYNC", true, "flow-alpha")));
|
||||
|
||||
Assert.assertEquals(List.of(
|
||||
"flow_alpha_submit",
|
||||
"flow_alpha_observe",
|
||||
"flow_alpha_result",
|
||||
"flow_alpha_cancel",
|
||||
"flow_alpha_list"
|
||||
), toolNames(compilation));
|
||||
Assert.assertEquals(5, compilation.getToolInvokers().size());
|
||||
Assert.assertEquals(List.of("keyword"), compilation.getToolSpecs().get(0).getParametersSchema().get("required"));
|
||||
Assert.assertTrue(compilation.getToolSpecs().get(0).isApprovalRequired());
|
||||
Assert.assertEquals("确认执行?", compilation.getToolSpecs().get(0).getApprovalRequest().getApprovalPrompt());
|
||||
Assert.assertFalse(compilation.getToolSpecs().get(1).isApprovalRequired());
|
||||
Assert.assertEquals("flow_alpha", compilation.getToolSpecs().get(0).getMetadata().get("asyncToolName"));
|
||||
Assert.assertEquals("submit", compilation.getToolSpecs().get(0).getMetadata().get("asyncToolPhase"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Plugin 异步模式同样展开为五个固定子工具。
|
||||
*
|
||||
* @throws Exception 反射注入依赖失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void compileShouldExpandPluginAsyncSubTools() throws Exception {
|
||||
AgentToolRuntimeCompiler compiler = compiler();
|
||||
|
||||
AgentToolRuntimeCompilation compilation = compiler.compile(agent(pluginBinding("ASYNC", "plugin-tool")));
|
||||
|
||||
Assert.assertEquals(List.of(
|
||||
"plugin_tool_submit",
|
||||
"plugin_tool_observe",
|
||||
"plugin_tool_result",
|
||||
"plugin_tool_cancel",
|
||||
"plugin_tool_list"
|
||||
), toolNames(compilation));
|
||||
Assert.assertEquals(5, compilation.getToolInvokers().size());
|
||||
for (AgentToolSpec spec : compilation.getToolSpecs()) {
|
||||
Assert.assertEquals(Boolean.TRUE, spec.getMetadata().get("asyncTool"));
|
||||
Assert.assertEquals("插件工具", spec.getMetadata().get("toolDisplayName"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证异步工具名归一化后发生冲突时会在编译阶段失败。
|
||||
*
|
||||
* @throws Exception 反射注入依赖失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void compileShouldRejectNormalizedAsyncToolNameCollision() throws Exception {
|
||||
AgentToolRuntimeCompiler compiler = compiler();
|
||||
AgentToolBinding first = workflowBinding("ASYNC", false, "flow-alpha");
|
||||
AgentToolBinding second = workflowBinding("ASYNC", false, "flow_alpha");
|
||||
second.setId(BigInteger.valueOf(13L));
|
||||
second.setTargetId(BigInteger.valueOf(103L));
|
||||
|
||||
try {
|
||||
compiler.compile(agent(List.of(first, second)));
|
||||
Assert.fail("异步工具名冲突时应编译失败");
|
||||
} catch (BusinessException e) {
|
||||
Assert.assertTrue(e.getMessage().contains("flow_alpha_submit"));
|
||||
}
|
||||
}
|
||||
|
||||
private AgentToolRuntimeCompiler compiler() throws Exception {
|
||||
AgentToolRuntimeCompiler compiler = new AgentToolRuntimeCompiler();
|
||||
setField(compiler, "objectMapper", new ObjectMapper());
|
||||
setField(compiler, "workflowToolExecutor", new StubWorkflowToolExecutor());
|
||||
setField(compiler, "pluginToolExecutor", new StubPluginToolExecutor());
|
||||
return compiler;
|
||||
}
|
||||
|
||||
private Agent agent(AgentToolBinding binding) {
|
||||
return agent(List.of(binding));
|
||||
}
|
||||
|
||||
private Agent agent(List<AgentToolBinding> bindings) {
|
||||
Agent agent = new Agent();
|
||||
agent.setId(BigInteger.ONE);
|
||||
agent.setToolBindings(bindings);
|
||||
return agent;
|
||||
}
|
||||
|
||||
private AgentToolBinding workflowBinding(String executionMode, boolean hitlEnabled, String toolName) {
|
||||
AgentToolBinding binding = new AgentToolBinding();
|
||||
binding.setId(BigInteger.valueOf(11L));
|
||||
binding.setToolType(AgentToolType.WORKFLOW.name());
|
||||
binding.setTargetId(BigInteger.valueOf(101L));
|
||||
binding.setToolName(toolName);
|
||||
binding.setEnabled(true);
|
||||
binding.setHitlEnabled(hitlEnabled);
|
||||
binding.setHitlConfigJson(Map.of("prompt", "确认执行?"));
|
||||
binding.setOptionsJson(executionMode == null ? Map.of() : Map.of("executionMode", executionMode));
|
||||
binding.setResourceSnapshot(Map.of(
|
||||
"id", BigInteger.valueOf(101L),
|
||||
"title", "客户检索工作流",
|
||||
"description", "按关键词检索客户",
|
||||
"englishName", "flow-alpha"
|
||||
));
|
||||
return binding;
|
||||
}
|
||||
|
||||
private AgentToolBinding pluginBinding(String executionMode, String toolName) {
|
||||
AgentToolBinding binding = new AgentToolBinding();
|
||||
binding.setId(BigInteger.valueOf(12L));
|
||||
binding.setToolType(AgentToolType.PLUGIN.name());
|
||||
binding.setTargetId(BigInteger.valueOf(102L));
|
||||
binding.setToolName(toolName);
|
||||
binding.setEnabled(true);
|
||||
binding.setOptionsJson(Map.of("executionMode", executionMode));
|
||||
binding.setResourceSnapshot(Map.of(
|
||||
"id", BigInteger.valueOf(102L),
|
||||
"name", "插件工具",
|
||||
"description", "调用插件",
|
||||
"englishName", "plugin-tool"
|
||||
));
|
||||
return binding;
|
||||
}
|
||||
|
||||
private List<String> toolNames(AgentToolRuntimeCompilation compilation) {
|
||||
return compilation.getToolSpecs().stream().map(AgentToolSpec::getName).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void setField(Object target, String fieldName, Object value) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
private Tool testTool(String name, String description) {
|
||||
Parameter parameter = new Parameter();
|
||||
parameter.setName("keyword");
|
||||
parameter.setDescription("关键词");
|
||||
parameter.setType("string");
|
||||
parameter.setRequired(true);
|
||||
return Tool.builder()
|
||||
.name(name)
|
||||
.description(description)
|
||||
.addParameter(parameter)
|
||||
.function(arguments -> Map.of("ok", true))
|
||||
.build();
|
||||
}
|
||||
|
||||
private final class StubWorkflowToolExecutor extends WorkflowToolExecutor {
|
||||
|
||||
private StubWorkflowToolExecutor() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Tool buildTool(Workflow workflow) {
|
||||
return testTool(workflow.getEnglishName(), workflow.getDescription());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentToolExecutionResult execute(Workflow workflow, Map<String, Object> arguments) {
|
||||
return new AgentToolExecutionResult(Map.of("ok", true), "wf-run-1");
|
||||
}
|
||||
}
|
||||
|
||||
private final class StubPluginToolExecutor extends PluginToolExecutor {
|
||||
|
||||
@Override
|
||||
public Tool buildTool(PluginItem pluginItem) {
|
||||
return testTool(pluginItem.getEnglishName(), pluginItem.getDescription());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentToolExecutionResult execute(PluginItem pluginItem, Map<String, Object> arguments) {
|
||||
return new AgentToolExecutionResult(Map.of("ok", true), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user