refactor: 重构升级为有状态 Agent
- 完善 hook 对接机制 - 提供更加明确的调用方式
This commit is contained in:
@@ -1,951 +0,0 @@
|
||||
package com.easyagents.agent.runtime.agentscope;
|
||||
|
||||
import com.easyagents.agent.runtime.AgentDefinition;
|
||||
import com.easyagents.agent.runtime.AgentRunRequest;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||
import com.easyagents.agent.runtime.hitl.AgentResumeToken;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalCoordinator;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
||||
import com.easyagents.agent.runtime.hitl.AgentToolApprovalResponse;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeDocument;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetrievalResult;
|
||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec;
|
||||
import com.easyagents.agent.runtime.memory.AgentMemoryCompressionParameter;
|
||||
import com.easyagents.agent.runtime.memory.AgentMemoryPolicy;
|
||||
import com.easyagents.agent.runtime.memory.AgentMemorySnapshot;
|
||||
import com.easyagents.agent.runtime.message.*;
|
||||
import com.easyagents.agent.runtime.model.AgentModelProviderType;
|
||||
import com.easyagents.agent.runtime.model.AgentModelSpec;
|
||||
import com.easyagents.agent.runtime.persistence.AgentPersistencePolicy;
|
||||
import com.easyagents.agent.runtime.persistence.AgentRuntimeState;
|
||||
import com.easyagents.agent.runtime.persistence.json.JsonAgentSessionStore;
|
||||
import com.easyagents.agent.runtime.persistence.memory.InMemoryAgentSessionStore;
|
||||
import com.easyagents.agent.runtime.skill.AgentSkillBoxSpec;
|
||||
import com.easyagents.agent.runtime.skill.AgentSkillSpec;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolResult;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||
import io.agentscope.core.ReActAgent;
|
||||
import io.agentscope.core.agent.Event;
|
||||
import io.agentscope.core.agent.EventType;
|
||||
import io.agentscope.core.hook.ErrorEvent;
|
||||
import io.agentscope.core.hook.HookEvent;
|
||||
import io.agentscope.core.hook.ReasoningChunkEvent;
|
||||
import io.agentscope.core.memory.autocontext.AutoContextConfig;
|
||||
import io.agentscope.core.message.*;
|
||||
import io.agentscope.core.model.AnthropicChatModel;
|
||||
import io.agentscope.core.model.GeminiChatModel;
|
||||
import io.agentscope.core.model.OpenAIChatModel;
|
||||
import io.agentscope.core.rag.Knowledge;
|
||||
import io.agentscope.core.rag.model.Document;
|
||||
import io.agentscope.core.rag.model.RetrieveConfig;
|
||||
import io.agentscope.core.session.Session;
|
||||
import io.agentscope.core.skill.SkillBox;
|
||||
import io.agentscope.core.tool.AgentTool;
|
||||
import io.agentscope.core.tool.ToolCallParam;
|
||||
import io.agentscope.core.tool.Toolkit;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import reactor.core.publisher.Sinks;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 在不发起真实模型网络调用的情况下测试 AgentScope 适配行为。
|
||||
*/
|
||||
public class AgentScopeAdapterTest {
|
||||
|
||||
@Test
|
||||
public void shouldBuildReActAgentFromDefinition() {
|
||||
AgentRunRequest request = request();
|
||||
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||
|
||||
ReActAgent agent = runtime.buildAgent(request, Sinks.many().unicast().onBackpressureBuffer());
|
||||
|
||||
Assert.assertEquals("agent-name", agent.getName());
|
||||
Assert.assertEquals("system", agent.getSysPrompt());
|
||||
Assert.assertEquals(3, agent.getMaxIters());
|
||||
Assert.assertNotNull(agent.getMemory());
|
||||
Assert.assertNotNull(agent.getToolkit().getTool("echo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRegisterAndCallDynamicTool() {
|
||||
AgentRunRequest request = request();
|
||||
AgentToolSpec toolSpec = request.getAgentDefinition().getToolSpecs().get(0);
|
||||
AgentTool tool = new AgentScopeToolAdapter().adapt(toolSpec, request.getToolInvokers().get("echo"), request);
|
||||
ToolUseBlock block = ToolUseBlock.builder()
|
||||
.id("call-1")
|
||||
.name("echo")
|
||||
.input(Map.of("text", "hello"))
|
||||
.build();
|
||||
|
||||
String result = ((TextBlock) tool.callAsync(ToolCallParam.builder()
|
||||
.toolUseBlock(block)
|
||||
.input(Map.of("text", "hello"))
|
||||
.build())
|
||||
.block()
|
||||
.getOutput()
|
||||
.get(0)).getText();
|
||||
|
||||
Assert.assertTrue(result.contains("hello"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldEmitToolCallAndResultEventsFromToolkitCallback() throws Exception {
|
||||
AgentRunRequest request = request();
|
||||
Sinks.Many<AgentRuntimeEvent> sink = Sinks.many().unicast().onBackpressureBuffer();
|
||||
ReActAgent agent = fakeRuntime().buildAgent(request, sink);
|
||||
CompletableFuture<List<AgentRuntimeEvent>> eventsFuture = sink.asFlux().take(2).collectList().toFuture();
|
||||
ToolUseBlock block = ToolUseBlock.builder()
|
||||
.id("call-2")
|
||||
.name("echo")
|
||||
.input(Map.of("text", "hello"))
|
||||
.build();
|
||||
|
||||
agent.getToolkit().getTool("echo").callAsync(ToolCallParam.builder()
|
||||
.toolUseBlock(block)
|
||||
.input(Map.of("text", "hello"))
|
||||
.build())
|
||||
.block();
|
||||
List<AgentRuntimeEvent> events = eventsFuture.get(3, TimeUnit.SECONDS);
|
||||
|
||||
Assert.assertTrue(events.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.TOOL_CALL));
|
||||
Assert.assertTrue(events.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.TOOL_RESULT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldEmitNormalToolEventBeforeSkillActivated() throws Exception {
|
||||
AgentRunRequest request = requestWithSkillBoundTool();
|
||||
Sinks.Many<AgentRuntimeEvent> sink = Sinks.many().unicast().onBackpressureBuffer();
|
||||
ReActAgent agent = fakeRuntime().buildAgent(request, sink);
|
||||
CompletableFuture<List<AgentRuntimeEvent>> eventsFuture = sink.asFlux().take(2).collectList().toFuture();
|
||||
ToolUseBlock block = ToolUseBlock.builder()
|
||||
.id("call-skill-tool")
|
||||
.name("echo")
|
||||
.input(Map.of("text", "hello"))
|
||||
.build();
|
||||
|
||||
agent.getToolkit().getTool("echo").callAsync(ToolCallParam.builder()
|
||||
.toolUseBlock(block)
|
||||
.input(Map.of("text", "hello"))
|
||||
.build())
|
||||
.block();
|
||||
List<AgentRuntimeEvent> events = eventsFuture.get(3, TimeUnit.SECONDS);
|
||||
|
||||
Assert.assertEquals(AgentRuntimeEventType.TOOL_CALL, events.get(0).getEventType());
|
||||
Assert.assertEquals(AgentRuntimeEventType.TOOL_RESULT, events.get(1).getEventType());
|
||||
Assert.assertFalse(events.get(0).getPayload().containsKey("skillId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldEmitSkillStepAfterSkillActivated() throws Exception {
|
||||
AgentRunRequest request = requestWithSkillBoundTool();
|
||||
com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext skillContext =
|
||||
com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec());
|
||||
skillContext.activateSkill("skill-1");
|
||||
Sinks.Many<AgentRuntimeEvent> sink = Sinks.many().unicast().onBackpressureBuffer();
|
||||
AgentTool tool = new AgentScopeToolAdapter().adapt(request.getAgentDefinition().getToolSpecs().get(0),
|
||||
request.getToolInvokers().get("echo"), request, AgentToolApprovalCoordinator.disabled(), sink,
|
||||
skillContext, skillContext.getToolBinding("echo"));
|
||||
CompletableFuture<List<AgentRuntimeEvent>> eventsFuture = sink.asFlux().take(2).collectList().toFuture();
|
||||
|
||||
tool.callAsync(ToolCallParam.builder()
|
||||
.toolUseBlock(ToolUseBlock.builder()
|
||||
.id("call-skill-tool")
|
||||
.name("echo")
|
||||
.input(Map.of("text", "hello"))
|
||||
.build())
|
||||
.input(Map.of("text", "hello"))
|
||||
.build())
|
||||
.block();
|
||||
List<AgentRuntimeEvent> events = eventsFuture.get(3, TimeUnit.SECONDS);
|
||||
|
||||
Assert.assertTrue(events.stream().allMatch(event -> event.getEventType() == AgentRuntimeEventType.SKILL_STEP));
|
||||
Assert.assertEquals("skill-1", events.get(0).getPayload().get("skillId"));
|
||||
Assert.assertEquals("TOOL_CALL", events.get(0).getPayload().get("stepType"));
|
||||
Assert.assertEquals("TOOL_RESULT", events.get(1).getPayload().get("stepType"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldEmitToolApprovalWhenToolRequiresApproval() throws Exception {
|
||||
AgentRunRequest enabled = request();
|
||||
AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
|
||||
approvalRequest.setApprovalPrompt("确认执行?");
|
||||
enabled.getAgentDefinition().getToolSpecs().get(0).setApprovalRequired(true);
|
||||
enabled.getAgentDefinition().getToolSpecs().get(0).setApprovalRequest(approvalRequest);
|
||||
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||
Sinks.Many<AgentRuntimeEvent> sink = Sinks.many().unicast().onBackpressureBuffer();
|
||||
AgentTool tool = new AgentScopeToolAdapter().adapt(enabled.getAgentDefinition().getToolSpecs().get(0),
|
||||
enabled.getToolInvokers().get("echo"), enabled, coordinator, sink);
|
||||
java.util.concurrent.CountDownLatch approvalLatch = new java.util.concurrent.CountDownLatch(1);
|
||||
java.util.concurrent.CopyOnWriteArrayList<AgentRuntimeEvent> events = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
sink.asFlux().subscribe(event -> {
|
||||
events.add(event);
|
||||
if (event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED) {
|
||||
approvalLatch.countDown();
|
||||
}
|
||||
});
|
||||
tool.callAsync(ToolCallParam.builder()
|
||||
.toolUseBlock(ToolUseBlock.builder()
|
||||
.id("call-hitl")
|
||||
.name("echo")
|
||||
.input(Map.of("text", "hello"))
|
||||
.build())
|
||||
.input(Map.of("text", "hello"))
|
||||
.build())
|
||||
.subscribe(result -> {
|
||||
}, error -> {
|
||||
});
|
||||
Assert.assertTrue(approvalLatch.await(5, TimeUnit.SECONDS));
|
||||
AgentRuntimeEvent approval = events.stream()
|
||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED)
|
||||
.findFirst()
|
||||
.orElseThrow(AssertionError::new);
|
||||
Assert.assertEquals("确认执行?", approval.getPayload().get("approvalPrompt"));
|
||||
Assert.assertNotNull(approval.getPayload().get("resumeToken"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldContinueToolExecutionAfterApproval() throws Exception {
|
||||
AgentRunRequest request = request();
|
||||
AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
|
||||
approvalRequest.setApprovalPrompt("确认执行?");
|
||||
request.getAgentDefinition().getToolSpecs().get(0).setApprovalRequired(true);
|
||||
request.getAgentDefinition().getToolSpecs().get(0).setApprovalRequest(approvalRequest);
|
||||
|
||||
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||
Sinks.Many<AgentRuntimeEvent> sink = Sinks.many().unicast().onBackpressureBuffer();
|
||||
AgentTool tool = new AgentScopeToolAdapter().adapt(request.getAgentDefinition().getToolSpecs().get(0),
|
||||
request.getToolInvokers().get("echo"), request, coordinator, sink);
|
||||
java.util.List<AgentRuntimeEvent> events = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
java.util.concurrent.CountDownLatch approvalLatch = new java.util.concurrent.CountDownLatch(1);
|
||||
java.util.concurrent.atomic.AtomicReference<Throwable> errorRef = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
sink.asFlux().subscribe(event -> {
|
||||
events.add(event);
|
||||
if (event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED) {
|
||||
approvalLatch.countDown();
|
||||
}
|
||||
});
|
||||
java.util.concurrent.CompletableFuture<io.agentscope.core.message.ToolResultBlock> future = tool.callAsync(
|
||||
ToolCallParam.builder()
|
||||
.toolUseBlock(ToolUseBlock.builder()
|
||||
.id("call-hitl")
|
||||
.name("echo")
|
||||
.input(Map.of("text", "hello"))
|
||||
.build())
|
||||
.input(Map.of("text", "hello"))
|
||||
.build()).toFuture();
|
||||
Assert.assertTrue(approvalLatch.await(5, TimeUnit.SECONDS));
|
||||
AgentRuntimeEvent approval = events.stream()
|
||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED)
|
||||
.findFirst()
|
||||
.orElseThrow(AssertionError::new);
|
||||
|
||||
AgentToolApprovalResponse response = new AgentToolApprovalResponse();
|
||||
AgentResumeToken token = AgentResumeToken.create();
|
||||
token.setValue(String.valueOf(approval.getPayload().get("resumeToken")));
|
||||
response.setResumeToken(token);
|
||||
response.setApproved(true);
|
||||
|
||||
coordinator.submit(response);
|
||||
io.agentscope.core.message.ToolResultBlock resultBlock = future.get(5, TimeUnit.SECONDS);
|
||||
Assert.assertTrue(events.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.TOOL_RESULT));
|
||||
Assert.assertNotNull(resultBlock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCancelToolExecutionAfterRejection() throws Exception {
|
||||
AgentRunRequest request = request();
|
||||
AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
|
||||
approvalRequest.setApprovalPrompt("确认执行?");
|
||||
request.getAgentDefinition().getToolSpecs().get(0).setApprovalRequired(true);
|
||||
request.getAgentDefinition().getToolSpecs().get(0).setApprovalRequest(approvalRequest);
|
||||
|
||||
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||
Sinks.Many<AgentRuntimeEvent> sink = Sinks.many().unicast().onBackpressureBuffer();
|
||||
AgentTool tool = new AgentScopeToolAdapter().adapt(request.getAgentDefinition().getToolSpecs().get(0),
|
||||
request.getToolInvokers().get("echo"), request, coordinator, sink);
|
||||
java.util.List<AgentRuntimeEvent> events = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
sink.asFlux().subscribe(events::add);
|
||||
java.util.concurrent.CompletableFuture<io.agentscope.core.message.ToolResultBlock> future = tool.callAsync(
|
||||
ToolCallParam.builder()
|
||||
.toolUseBlock(ToolUseBlock.builder()
|
||||
.id("call-hitl")
|
||||
.name("echo")
|
||||
.input(Map.of("text", "hello"))
|
||||
.build())
|
||||
.input(Map.of("text", "hello"))
|
||||
.build()).toFuture();
|
||||
AgentRuntimeEvent approval = null;
|
||||
long deadline = System.currentTimeMillis() + 5000L;
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
approval = events.stream()
|
||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (approval != null) {
|
||||
break;
|
||||
}
|
||||
Thread.sleep(50L);
|
||||
}
|
||||
if (approval == null) {
|
||||
throw new AssertionError("未收到审批事件");
|
||||
}
|
||||
|
||||
AgentToolApprovalResponse response = new AgentToolApprovalResponse();
|
||||
AgentResumeToken token = AgentResumeToken.create();
|
||||
token.setValue(String.valueOf(approval.getPayload().get("resumeToken")));
|
||||
response.setResumeToken(token);
|
||||
response.setApproved(false);
|
||||
response.setRejectReason("拒绝执行");
|
||||
|
||||
coordinator.submit(response);
|
||||
try {
|
||||
future.get(5, TimeUnit.SECONDS);
|
||||
Assert.fail("拒绝后不应返回工具结果。");
|
||||
} catch (java.util.concurrent.ExecutionException exception) {
|
||||
Assert.assertTrue(exception.getCause() instanceof com.easyagents.agent.runtime.hitl.AgentToolApprovalRejectedException);
|
||||
}
|
||||
Assert.assertFalse(events.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.TOOL_RESULT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotDuplicateToolApprovalFromRuntimeEventMapper() throws Exception {
|
||||
AgentRunRequest request = request();
|
||||
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||
java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod(
|
||||
"mapEvent", AgentRunRequest.class, Event.class);
|
||||
method.setAccessible(true);
|
||||
ToolResultBlock resultBlock = ToolResultBlock.builder()
|
||||
.id("call-hitl")
|
||||
.name("echo")
|
||||
.output(TextBlock.builder().text("need confirm").build())
|
||||
.metadata(Map.of(ToolResultBlock.METADATA_SUSPENDED, true))
|
||||
.build();
|
||||
Msg message = Msg.builder()
|
||||
.role(MsgRole.TOOL)
|
||||
.content(resultBlock)
|
||||
.build();
|
||||
Event event = new Event(EventType.TOOL_RESULT, message, true);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<AgentRuntimeEvent> events = (List<AgentRuntimeEvent>) method.invoke(runtime, request, event);
|
||||
|
||||
Assert.assertTrue(events.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMapSkillLoadEventsFromAgentScopeToolEvents() throws Exception {
|
||||
AgentRunRequest request = requestWithSkillBoundTool();
|
||||
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||
java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod(
|
||||
"mapEvent", AgentRunRequest.class, Event.class,
|
||||
com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext.class);
|
||||
method.setAccessible(true);
|
||||
com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext skillContext =
|
||||
com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec());
|
||||
ToolUseBlock toolUseBlock = ToolUseBlock.builder()
|
||||
.id("skill-call-1")
|
||||
.name(com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext.LOAD_SKILL_TOOL_NAME)
|
||||
.input(Map.of("skillId", "skill-1", "path", "SKILL.md"))
|
||||
.build();
|
||||
Msg reasoning = Msg.builder()
|
||||
.id("reasoning-msg")
|
||||
.role(MsgRole.ASSISTANT)
|
||||
.content(toolUseBlock)
|
||||
.build();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<AgentRuntimeEvent> callEvents = (List<AgentRuntimeEvent>) method.invoke(
|
||||
runtime, request, new Event(EventType.REASONING, reasoning, false), skillContext);
|
||||
|
||||
ToolResultBlock resultBlock = ToolResultBlock.of(
|
||||
"skill-call-1",
|
||||
com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext.LOAD_SKILL_TOOL_NAME,
|
||||
TextBlock.builder().text("Successfully loaded skill: skill-1").build());
|
||||
Msg toolResult = Msg.builder()
|
||||
.id("tool-msg")
|
||||
.role(MsgRole.TOOL)
|
||||
.content(resultBlock)
|
||||
.build();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<AgentRuntimeEvent> resultEvents = (List<AgentRuntimeEvent>) method.invoke(
|
||||
runtime, request, new Event(EventType.TOOL_RESULT, toolResult, true), skillContext);
|
||||
|
||||
Assert.assertTrue(callEvents.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.SKILL_CALL));
|
||||
AgentRuntimeEvent resultEvent = resultEvents.get(0);
|
||||
Assert.assertEquals(AgentRuntimeEventType.SKILL_RESULT, resultEvent.getEventType());
|
||||
Assert.assertEquals("skill-1", resultEvent.getPayload().get("skillId"));
|
||||
Assert.assertEquals("SKILL.md", resultEvent.getPayload().get("path"));
|
||||
Assert.assertTrue(skillContext.isSkillActive("skill-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAttachStructuredMessageOnLastAgentResult() throws Exception {
|
||||
AgentRunRequest request = request();
|
||||
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||
java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod(
|
||||
"mapEvent", AgentRunRequest.class, Event.class);
|
||||
method.setAccessible(true);
|
||||
Msg message = Msg.builder()
|
||||
.id("assistant-msg")
|
||||
.role(MsgRole.ASSISTANT)
|
||||
.content(TextBlock.builder().text("final answer").build())
|
||||
.build();
|
||||
Event event = new Event(EventType.AGENT_RESULT, message, true);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<AgentRuntimeEvent> events = (List<AgentRuntimeEvent>) method.invoke(runtime, request, event);
|
||||
|
||||
AgentRuntimeEvent delta = events.get(0);
|
||||
Assert.assertEquals(AgentRuntimeEventType.MESSAGE_DELTA, delta.getEventType());
|
||||
Assert.assertNotNull(delta.getMessage());
|
||||
Assert.assertEquals("assistant-msg", delta.getMessage().getMessageId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotEmitReasoningFromHook() {
|
||||
AgentRunRequest request = request();
|
||||
AgentScopeEventHook hook = new AgentScopeEventHook(request);
|
||||
ReActAgent agent = fakeRuntime().buildAgent(request, Sinks.many().unicast().onBackpressureBuffer());
|
||||
Msg chunk = Msg.builder()
|
||||
.role(MsgRole.ASSISTANT)
|
||||
.textContent("thinking")
|
||||
.build();
|
||||
|
||||
hook.onEvent(new ReasoningChunkEvent(agent, "run-1", null, chunk, chunk)).block();
|
||||
Assert.assertNotNull(chunk);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAggregateKnowledgeAndPreserveMetadata() {
|
||||
AgentRunRequest request = request();
|
||||
AgentKnowledgeSpec second = knowledge("kb-2", "KB2");
|
||||
second.getMetadata().put("permissionScope", "team");
|
||||
request.getAgentDefinition().getKnowledgeSpecs().add(second);
|
||||
request.getKnowledgeRetrievers().put("kb-2", retrievalRequest -> {
|
||||
AgentKnowledgeDocument document = doc("doc-2", "chunk-2", "content-2", 0.95D);
|
||||
document.getMetadata().put("pageNo", 2);
|
||||
return AgentKnowledgeRetrievalResult.of(List.of(document));
|
||||
});
|
||||
Knowledge knowledge = new AgentScopeKnowledgeAdapter().createAggregateKnowledge(request);
|
||||
|
||||
List<Document> documents = knowledge.retrieve("query", RetrieveConfig.builder().limit(2).scoreThreshold(0D).build()).block();
|
||||
|
||||
Assert.assertEquals(2, documents.size());
|
||||
Map<String, Object> payload = documents.get(0).getPayload();
|
||||
Assert.assertTrue(payload.containsKey("knowledgeMetadata"));
|
||||
Assert.assertTrue(payload.containsKey("documentMetadata"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldEmitKnowledgeRetrievalEvent() throws Exception {
|
||||
AgentRunRequest request = request();
|
||||
Sinks.Many<AgentRuntimeEvent> sink = Sinks.many().unicast().onBackpressureBuffer();
|
||||
Knowledge knowledge = new AgentScopeKnowledgeAdapter().createAggregateKnowledge(request, sink);
|
||||
CompletableFuture<List<AgentRuntimeEvent>> eventsFuture = sink.asFlux().take(1).collectList().toFuture();
|
||||
|
||||
knowledge.retrieve("query", RetrieveConfig.builder().limit(1).scoreThreshold(0.2D).build()).block();
|
||||
AgentRuntimeEvent event = eventsFuture.get(3, TimeUnit.SECONDS).get(0);
|
||||
|
||||
Assert.assertEquals(AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL, event.getEventType());
|
||||
Assert.assertEquals("kb-1", event.getPayload().get("knowledgeId"));
|
||||
Assert.assertEquals(1, event.getPayload().get("documentCount"));
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> documents = (List<Map<String, Object>>) event.getPayload().get("documents");
|
||||
Assert.assertFalse(documents.get(0).containsKey("content"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFailWhenKnowledgeRetrieverMissing() {
|
||||
AgentRunRequest request = request();
|
||||
request.getKnowledgeRetrievers().clear();
|
||||
Knowledge knowledge = new AgentScopeKnowledgeAdapter().createAggregateKnowledge(request);
|
||||
|
||||
try {
|
||||
knowledge.retrieve("query", RetrieveConfig.builder().limit(2).scoreThreshold(0D).build()).block();
|
||||
Assert.fail("Expected missing knowledge retriever to fail.");
|
||||
} catch (Exception exception) {
|
||||
Assert.assertTrue(exception.getMessage().contains("Knowledge retriever is required"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldUseKnowledgeSpecLimitAsAggregateDefault() {
|
||||
AgentRunRequest request = request();
|
||||
AgentKnowledgeSpec second = knowledge("kb-2", "KB2");
|
||||
second.setLimit(3);
|
||||
request.getAgentDefinition().getKnowledgeSpecs().get(0).setLimit(2);
|
||||
request.getAgentDefinition().getKnowledgeSpecs().add(second);
|
||||
ReActAgent agent = fakeRuntime().buildAgent(request, Sinks.many().unicast().onBackpressureBuffer());
|
||||
|
||||
Assert.assertNotNull(agent);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldUseMinimumKnowledgeScoreThresholdAsAggregateDefault() throws Exception {
|
||||
AgentRunRequest request = request();
|
||||
request.getAgentDefinition().getKnowledgeSpecs().get(0).setScoreThreshold(0.4D);
|
||||
AgentKnowledgeSpec second = knowledge("kb-2", "KB2");
|
||||
second.setScoreThreshold(0.2D);
|
||||
request.getAgentDefinition().getKnowledgeSpecs().add(second);
|
||||
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||
java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod(
|
||||
"defaultRetrieveConfig", AgentDefinition.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
RetrieveConfig config = (RetrieveConfig) method.invoke(runtime, request.getAgentDefinition());
|
||||
|
||||
Assert.assertEquals(0.2D, config.getScoreThreshold(), 0.0001D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRoundTripStructuredMessageBlocks() {
|
||||
AgentMessage message = new AgentMessage();
|
||||
message.setMessageId("msg-1");
|
||||
message.setRole(AgentMessageRole.ASSISTANT);
|
||||
message.getMetadata().put("scene", "chat");
|
||||
message.getContentBlocks().add(new AgentTextBlock("hello"));
|
||||
message.getContentBlocks().add(new AgentThinkingBlock("thinking"));
|
||||
AgentToolUseBlock toolUse = new AgentToolUseBlock("call-1", "echo", Map.of("text", "hi"));
|
||||
toolUse.setContent("raw-call");
|
||||
message.getContentBlocks().add(toolUse);
|
||||
AgentToolResultBlock toolResult = new AgentToolResultBlock("call-1", "echo");
|
||||
toolResult.setSuspended(true);
|
||||
toolResult.getMetadata().put("status", "ok");
|
||||
toolResult.getOutput().add(new AgentTextBlock("result"));
|
||||
message.getContentBlocks().add(toolResult);
|
||||
AgentMediaBlock media = new AgentMediaBlock("image");
|
||||
media.setUrl("https://example.com/a.png");
|
||||
media.setMimeType("image/png");
|
||||
message.getContentBlocks().add(media);
|
||||
|
||||
AgentScopeMessageAdapter adapter = new AgentScopeMessageAdapter();
|
||||
Msg msg = adapter.toMsg(message);
|
||||
AgentMessage converted = adapter.toAgentMessage(msg);
|
||||
|
||||
Assert.assertEquals("msg-1", converted.getMessageId());
|
||||
Assert.assertEquals(5, converted.getContentBlocks().size());
|
||||
Assert.assertEquals("hello", ((AgentTextBlock) converted.getContentBlocks().get(0)).getText());
|
||||
Assert.assertEquals("thinking", ((AgentThinkingBlock) converted.getContentBlocks().get(1)).getThinking());
|
||||
Assert.assertEquals("call-1", ((AgentToolUseBlock) converted.getContentBlocks().get(2)).getId());
|
||||
Assert.assertEquals("echo", ((AgentToolResultBlock) converted.getContentBlocks().get(3)).getName());
|
||||
Assert.assertEquals("image", ((AgentMediaBlock) converted.getContentBlocks().get(4)).getMediaKind());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldPreserveInvalidTimestampAsRawMetadata() {
|
||||
Msg msg = Msg.builder()
|
||||
.id("msg-invalid-time")
|
||||
.role(MsgRole.USER)
|
||||
.textContent("hello")
|
||||
.timestamp("invalid-time")
|
||||
.build();
|
||||
|
||||
AgentMessage converted = new AgentScopeMessageAdapter().toAgentMessage(msg);
|
||||
|
||||
Assert.assertEquals("invalid-time", converted.getMetadata().get("rawTimestamp"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldConvertUnknownBlockToExplicitFallbackText() {
|
||||
AgentUnknownBlock unknownBlock = new AgentUnknownBlock();
|
||||
unknownBlock.setSourceClassName("com.example.CustomBlock");
|
||||
unknownBlock.setSourceTypeName("custom");
|
||||
|
||||
io.agentscope.core.message.ContentBlock converted = new AgentScopeMessageAdapter().toContentBlock(unknownBlock);
|
||||
|
||||
Assert.assertTrue(converted instanceof TextBlock);
|
||||
Assert.assertTrue(((TextBlock) converted).getText().contains("unsupported content block"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateAutoContextConfigFromPolicy() {
|
||||
AgentMemoryCompressionParameter parameter = new AgentMemoryCompressionParameter();
|
||||
parameter.setMsgThreshold(9);
|
||||
parameter.setLastKeep(4);
|
||||
parameter.setMaxToken(1234L);
|
||||
|
||||
AutoContextConfig config = new AgentScopeMemoryAdapter().toAutoContextConfig(parameter);
|
||||
|
||||
Assert.assertEquals(9, config.getMsgThreshold());
|
||||
Assert.assertEquals(4, config.getLastKeep());
|
||||
Assert.assertEquals(1234L, config.getMaxToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldMapOllamaGenerationOptions() throws Exception {
|
||||
AgentModelSpec modelSpec = new AgentModelSpec();
|
||||
modelSpec.setProviderType(AgentModelProviderType.OLLAMA);
|
||||
modelSpec.setModelName("llama");
|
||||
com.easyagents.agent.runtime.model.AgentGenerationOptions options = new com.easyagents.agent.runtime.model.AgentGenerationOptions();
|
||||
options.setTemperature(0.2D);
|
||||
options.setTopP(0.8D);
|
||||
options.setTopK(20);
|
||||
options.setMaxTokens(128);
|
||||
|
||||
Object model = new AgentScopeModelFactory().create(modelSpec, options);
|
||||
Field field = model.getClass().getDeclaredField("defaultOptions");
|
||||
field.setAccessible(true);
|
||||
io.agentscope.core.model.ollama.OllamaOptions ollamaOptions = (io.agentscope.core.model.ollama.OllamaOptions) field.get(model);
|
||||
|
||||
Assert.assertEquals(0.2D, ollamaOptions.getTemperature(), 0.0001D);
|
||||
Assert.assertEquals(0.8D, ollamaOptions.getTopP(), 0.0001D);
|
||||
Assert.assertEquals(Integer.valueOf(20), ollamaOptions.getTopK());
|
||||
Assert.assertEquals(Integer.valueOf(128), ollamaOptions.getMaxTokens());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateNativeProviderModels() {
|
||||
Assert.assertTrue(createModel(AgentModelProviderType.OPENAI) instanceof OpenAIChatModel);
|
||||
Assert.assertTrue(createModel(AgentModelProviderType.ANTHROPIC) instanceof AnthropicChatModel);
|
||||
Assert.assertTrue(createModel(AgentModelProviderType.GEMINI) instanceof GeminiChatModel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateDomesticProvidersAsOpenAiCompatibleModels() {
|
||||
Assert.assertTrue(createModel(AgentModelProviderType.GLM) instanceof OpenAIChatModel);
|
||||
Assert.assertTrue(createModel(AgentModelProviderType.MINIMAX) instanceof OpenAIChatModel);
|
||||
Assert.assertTrue(createModel(AgentModelProviderType.MOONSHOT) instanceof OpenAIChatModel);
|
||||
Assert.assertTrue(createModel(AgentModelProviderType.ARK) instanceof OpenAIChatModel);
|
||||
Assert.assertTrue(createModel(AgentModelProviderType.SILICONFLOW) instanceof OpenAIChatModel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldStoreSessionThroughAdapter() {
|
||||
InMemoryAgentSessionStore store = new InMemoryAgentSessionStore();
|
||||
Session session = new AgentScopeSessionAdapter(store);
|
||||
|
||||
session.save(AgentScopeSessionAdapter.sessionKey("s1"), "state", new TestState("v1"));
|
||||
|
||||
Assert.assertTrue(store.exists("s1"));
|
||||
Assert.assertEquals("v1", session.get(AgentScopeSessionAdapter.sessionKey("s1"), "state", TestState.class).get().value);
|
||||
Assert.assertTrue(AgentScopeSessionAdapter.toStatePersistence(AgentPersistencePolicy.memoryOnly()).memoryManaged());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldPersistSessionStateAsJsonFiles() throws Exception {
|
||||
Path directory = Files.createTempDirectory("easy-agents-session-");
|
||||
JsonAgentSessionStore store = new JsonAgentSessionStore(directory);
|
||||
Session session = new AgentScopeSessionAdapter(store);
|
||||
Msg first = Msg.builder()
|
||||
.role(MsgRole.USER)
|
||||
.content(TextBlock.builder().text("hello").build())
|
||||
.build();
|
||||
Msg second = Msg.builder()
|
||||
.role(MsgRole.ASSISTANT)
|
||||
.content(TextBlock.builder().text("world").build())
|
||||
.build();
|
||||
|
||||
session.save(AgentScopeSessionAdapter.sessionKey("s1"), "message", first);
|
||||
session.save(AgentScopeSessionAdapter.sessionKey("s1"), "messages", List.of(first, second));
|
||||
|
||||
JsonAgentSessionStore reloadedStore = new JsonAgentSessionStore(directory);
|
||||
Session reloadedSession = new AgentScopeSessionAdapter(reloadedStore);
|
||||
Msg reloaded = reloadedSession.get(AgentScopeSessionAdapter.sessionKey("s1"), "message", Msg.class).orElseThrow();
|
||||
List<Msg> messages = reloadedSession.getList(AgentScopeSessionAdapter.sessionKey("s1"), "messages", Msg.class);
|
||||
|
||||
Assert.assertTrue(reloadedStore.exists("s1"));
|
||||
Assert.assertEquals("hello", reloaded.getTextContent());
|
||||
Assert.assertEquals(2, messages.size());
|
||||
Assert.assertEquals("world", messages.get(1).getTextContent());
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException.class)
|
||||
public void shouldRejectUnsafeSessionKeysForFileStore() throws Exception {
|
||||
JsonAgentSessionStore store = new JsonAgentSessionStore(Files.createTempDirectory("easy-agents-session-"));
|
||||
|
||||
store.save("../unsafe", "state", AgentRuntimeState.of("state", new TestState("v1")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldCreateSkillBox() {
|
||||
AgentSkillSpec skill = new AgentSkillSpec();
|
||||
skill.setSkillId("skill-1");
|
||||
skill.setName("skill");
|
||||
skill.setDescription("desc");
|
||||
skill.setSkillContent("content");
|
||||
AgentSkillBoxSpec spec = new AgentSkillBoxSpec();
|
||||
spec.setSkillBoxId("box");
|
||||
spec.setSkills(List.of(skill));
|
||||
|
||||
SkillBox skillBox = new AgentScopeSkillAdapter().createSkillBox(spec, new Toolkit());
|
||||
|
||||
Assert.assertNotNull(skillBox);
|
||||
Assert.assertFalse(skillBox.getAllSkillIds().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldExposeSkillBoundToolThroughAgentToolkit() {
|
||||
AgentRunRequest request = requestWithSkillBoundTool();
|
||||
ReActAgent agent = fakeRuntime().buildAgent(request, Sinks.many().unicast().onBackpressureBuffer());
|
||||
|
||||
Assert.assertNotNull(agent.getToolkit().getTool("echo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAttachSkillMetadataToApprovalEvent() throws Exception {
|
||||
AgentRunRequest request = requestWithSkillBoundTool();
|
||||
request.getAgentDefinition().getToolSpecs().get(0).setApprovalRequired(true);
|
||||
AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled();
|
||||
Sinks.Many<AgentRuntimeEvent> sink = Sinks.many().unicast().onBackpressureBuffer();
|
||||
com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext skillContext = activatedSkillContext(request);
|
||||
AgentTool tool = new AgentScopeToolAdapter().adapt(request.getAgentDefinition().getToolSpecs().get(0),
|
||||
request.getToolInvokers().get("echo"), request, coordinator, sink,
|
||||
skillContext, skillContext.getToolBinding("echo"));
|
||||
java.util.List<AgentRuntimeEvent> events = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
java.util.concurrent.CountDownLatch approvalLatch = new java.util.concurrent.CountDownLatch(1);
|
||||
sink.asFlux().subscribe(event -> {
|
||||
events.add(event);
|
||||
if (event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED) {
|
||||
approvalLatch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
tool.callAsync(ToolCallParam.builder()
|
||||
.toolUseBlock(ToolUseBlock.builder()
|
||||
.id("call-skill-approval")
|
||||
.name("echo")
|
||||
.input(Map.of("text", "hello"))
|
||||
.build())
|
||||
.input(Map.of("text", "hello"))
|
||||
.build())
|
||||
.subscribe(result -> {
|
||||
}, error -> {
|
||||
});
|
||||
Assert.assertTrue(approvalLatch.await(5, TimeUnit.SECONDS));
|
||||
|
||||
AgentRuntimeEvent approval = events.stream()
|
||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED)
|
||||
.findFirst()
|
||||
.orElseThrow(AssertionError::new);
|
||||
Assert.assertEquals("skill-1", approval.getPayload().get("skillId"));
|
||||
Assert.assertEquals("skill", approval.getPayload().get("skillName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldEmitCompletedAndFailedEvents() {
|
||||
AgentRunRequest request = request();
|
||||
|
||||
Assert.assertTrue(fakeRuntime().stream(request)
|
||||
.collectList()
|
||||
.block()
|
||||
.stream()
|
||||
.anyMatch(event -> event.getEventType() == AgentRuntimeEventType.COMPLETED));
|
||||
|
||||
AgentRunRequest failed = request();
|
||||
failed.getAgentDefinition().setModelSpec(null);
|
||||
Assert.assertTrue(fakeRuntime().stream(failed)
|
||||
.collectList()
|
||||
.block()
|
||||
.stream()
|
||||
.anyMatch(event -> event.getEventType() == AgentRuntimeEventType.FAILED));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldEmitCancelledEventFromRunHandle() throws Exception {
|
||||
AgentRunRequest request = request();
|
||||
request.setCancelReason("user stopped");
|
||||
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||
java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod(
|
||||
"cancelled", AgentRunRequest.class);
|
||||
method.setAccessible(true);
|
||||
AgentRuntimeEvent cancelledEvent = (AgentRuntimeEvent) method.invoke(runtime, request);
|
||||
List<AgentRuntimeEvent> events = List.of(cancelledEvent);
|
||||
Assert.assertTrue(events.stream().anyMatch(runtimeEvent -> runtimeEvent.getEventType() == AgentRuntimeEventType.CANCELLED));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotEmitFailedFromHook() {
|
||||
AgentRunRequest request = request();
|
||||
AgentScopeEventHook hook = new AgentScopeEventHook(request);
|
||||
ReActAgent agent = fakeRuntime().buildAgent(request, Sinks.many().unicast().onBackpressureBuffer());
|
||||
ErrorEvent errorEvent = new ErrorEvent(agent, new RuntimeException("boom"));
|
||||
|
||||
HookEvent returned = hook.onEvent(errorEvent).block();
|
||||
|
||||
Assert.assertSame(errorEvent, returned);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAccumulateCompletedTextFromMessageDeltas() throws Exception {
|
||||
AgentRunRequest request = request();
|
||||
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||
java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod(
|
||||
"updateFinalText", StringBuilder.class, AgentRuntimeEvent.class);
|
||||
method.setAccessible(true);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
AgentRuntimeEvent first = AgentRuntimeEvent.of(AgentRuntimeEventType.MESSAGE_DELTA);
|
||||
first.getPayload().put("text", "hello ");
|
||||
AgentRuntimeEvent second = AgentRuntimeEvent.of(AgentRuntimeEventType.MESSAGE_DELTA);
|
||||
second.getPayload().put("text", "world");
|
||||
|
||||
method.invoke(runtime, builder, first);
|
||||
method.invoke(runtime, builder, second);
|
||||
|
||||
Assert.assertEquals("hello world", builder.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAttachStructuredMessageOnCompletedEvent() throws Exception {
|
||||
AgentRunRequest request = request();
|
||||
AgentMessage message = AgentMessage.text(AgentMessageRole.ASSISTANT, "final answer");
|
||||
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||
java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod(
|
||||
"completed", AgentRunRequest.class, String.class, AgentMessage.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
AgentRuntimeEvent event = (AgentRuntimeEvent) method.invoke(runtime, request, "final answer", message);
|
||||
|
||||
Assert.assertEquals(AgentRuntimeEventType.COMPLETED, event.getEventType());
|
||||
Assert.assertEquals("final answer", event.getPayload().get("text"));
|
||||
Assert.assertSame(message, event.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldAttachKnowledgeReferencesToCompletedMessage() throws Exception {
|
||||
AgentRunRequest request = request();
|
||||
AgentMessage message = AgentMessage.text(AgentMessageRole.ASSISTANT, "final answer");
|
||||
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||
java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod(
|
||||
"completed", AgentRunRequest.class, String.class, AgentMessage.class, Map.class);
|
||||
method.setAccessible(true);
|
||||
Map<String, AgentKnowledgeReference> refs = new java.util.LinkedHashMap<>();
|
||||
AgentKnowledgeReference ref = new AgentKnowledgeReference();
|
||||
ref.setKnowledgeId("kb-1");
|
||||
ref.setKnowledgeName("KB1");
|
||||
ref.setDocumentId("doc-1");
|
||||
ref.setDocumentName("doc-1.md");
|
||||
ref.setChunkId("chunk-1");
|
||||
refs.put("kb-1|doc-1|chunk-1", ref);
|
||||
|
||||
AgentRuntimeEvent event = (AgentRuntimeEvent) method.invoke(runtime, request, "final answer", message, refs);
|
||||
|
||||
Assert.assertEquals(AgentRuntimeEventType.COMPLETED, event.getEventType());
|
||||
List<AgentKnowledgeReference> knowledgeRefs = event.getMessage().getKnowledgeReferences();
|
||||
Assert.assertEquals(1, knowledgeRefs.size());
|
||||
Assert.assertEquals("doc-1.md", knowledgeRefs.get(0).getDocumentName());
|
||||
Assert.assertEquals("kb-1", knowledgeRefs.get(0).getKnowledgeId());
|
||||
}
|
||||
|
||||
private AgentScopeReActRuntime fakeRuntime() {
|
||||
return new AgentScopeReActRuntime(new FakeAgentScopeModelFactory(), new AgentScopeToolAdapter(),
|
||||
new AgentScopeKnowledgeAdapter(), new AgentScopeMemoryAdapter(), new AgentScopeSkillAdapter(),
|
||||
new AgentScopeMessageAdapter());
|
||||
}
|
||||
|
||||
private List<AgentRuntimeEvent> invokeToolAndCollect(AgentRunRequest request, int count) throws Exception {
|
||||
Sinks.Many<AgentRuntimeEvent> sink = Sinks.many().replay().all();
|
||||
ReActAgent agent = fakeRuntime().buildAgent(request, sink);
|
||||
CompletableFuture<List<AgentRuntimeEvent>> eventsFuture = sink.asFlux().take(count).collectList().toFuture();
|
||||
ToolUseBlock block = ToolUseBlock.builder()
|
||||
.id("call-hitl")
|
||||
.name("echo")
|
||||
.input(Map.of("text", "hello"))
|
||||
.build();
|
||||
agent.getToolkit().getTool("echo").callAsync(ToolCallParam.builder()
|
||||
.toolUseBlock(block)
|
||||
.input(Map.of("text", "hello"))
|
||||
.build())
|
||||
.block();
|
||||
return eventsFuture.get(3, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private AgentRunRequest request() {
|
||||
AgentModelSpec modelSpec = new AgentModelSpec();
|
||||
modelSpec.setModelName("fake-model");
|
||||
AgentToolSpec tool = new AgentToolSpec();
|
||||
tool.setName("echo");
|
||||
tool.setDescription("Echo text");
|
||||
tool.setParametersSchema(Map.of("type", "object", "properties", Map.of("text", Map.of("type", "string"))));
|
||||
|
||||
AgentDefinition definition = new AgentDefinition();
|
||||
definition.setAgentId("agent-1");
|
||||
definition.setAgentName("agent-name");
|
||||
definition.setSystemPrompt("system");
|
||||
definition.setModelSpec(modelSpec);
|
||||
definition.getExecutionOptions().setMaxIters(3);
|
||||
definition.setToolSpecs(List.of(tool));
|
||||
definition.setKnowledgeSpecs(List.of(knowledge("kb-1", "KB1")));
|
||||
definition.setMemoryPolicy(AgentMemoryPolicy.inMemory());
|
||||
|
||||
AgentMemorySnapshot snapshot = new AgentMemorySnapshot();
|
||||
snapshot.addMessage(AgentMessage.text(AgentMessageRole.USER, "history"));
|
||||
|
||||
AgentRunRequest request = new AgentRunRequest();
|
||||
request.setRequestId("req-1");
|
||||
request.setTraceId("trace-1");
|
||||
request.setSessionId("session-1");
|
||||
request.setAgentDefinition(definition);
|
||||
request.setMemorySnapshot(snapshot);
|
||||
request.setUserMessage(AgentMessage.text(AgentMessageRole.USER, "hello"));
|
||||
request.getToolInvokers().put("echo", (arguments, context) -> AgentToolResult.success(String.valueOf(arguments.get("text"))));
|
||||
request.getKnowledgeRetrievers().put("kb-1", retrievalRequest -> AgentKnowledgeRetrievalResult.of(
|
||||
List.of(doc("doc-1", "chunk-1", "content-1", 0.8D))));
|
||||
return request;
|
||||
}
|
||||
|
||||
private AgentRunRequest requestWithSkillBoundTool() {
|
||||
AgentRunRequest request = request();
|
||||
AgentSkillSpec skill = new AgentSkillSpec();
|
||||
skill.setSkillId("skill-1");
|
||||
skill.setName("skill");
|
||||
skill.setDescription("desc");
|
||||
skill.setSkillContent("content");
|
||||
AgentSkillBoxSpec skillBoxSpec = new AgentSkillBoxSpec();
|
||||
skillBoxSpec.setSkillBoxId("box");
|
||||
skillBoxSpec.setSkills(List.of(skill));
|
||||
skillBoxSpec.setToolBindings(Map.of("skill-1", List.of("echo")));
|
||||
request.getAgentDefinition().setSkillBoxSpec(skillBoxSpec);
|
||||
return request;
|
||||
}
|
||||
|
||||
private com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext activatedSkillContext(AgentRunRequest request) {
|
||||
com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext skillContext =
|
||||
com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec());
|
||||
skillContext.activateSkill("skill-1");
|
||||
return skillContext;
|
||||
}
|
||||
|
||||
private Object createModel(AgentModelProviderType providerType) {
|
||||
AgentModelSpec modelSpec = new AgentModelSpec();
|
||||
modelSpec.setProviderType(providerType);
|
||||
modelSpec.setModelName("test-model");
|
||||
modelSpec.setApiKey("test-key");
|
||||
return new AgentScopeModelFactory().create(modelSpec, new com.easyagents.agent.runtime.model.AgentGenerationOptions());
|
||||
}
|
||||
|
||||
private AgentKnowledgeSpec knowledge(String id, String name) {
|
||||
AgentKnowledgeSpec spec = new AgentKnowledgeSpec();
|
||||
spec.setKnowledgeId(id);
|
||||
spec.setName(name);
|
||||
spec.getMetadata().put("publishVersion", "v1");
|
||||
return spec;
|
||||
}
|
||||
|
||||
private AgentKnowledgeDocument doc(String documentId, String chunkId, String content, double score) {
|
||||
AgentKnowledgeDocument document = new AgentKnowledgeDocument();
|
||||
document.setDocumentId(documentId);
|
||||
document.setDocumentName(documentId + ".md");
|
||||
document.setChunkId(chunkId);
|
||||
document.setContent(content);
|
||||
document.setScore(score);
|
||||
document.getMetadata().put("sectionTitle", "section");
|
||||
return document;
|
||||
}
|
||||
|
||||
private static class TestState implements io.agentscope.core.state.State {
|
||||
private final String value;
|
||||
|
||||
private TestState(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
package com.easyagents.agent.runtime.knowledge.citation;
|
||||
|
||||
import com.easyagents.agent.runtime.message.AgentKnowledgeReference;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 测试启发式知识库引用匹配器。
|
||||
*/
|
||||
public class HeuristicKnowledgeCitationMatcherTest {
|
||||
|
||||
private final HeuristicKnowledgeCitationMatcher matcher = new HeuristicKnowledgeCitationMatcher();
|
||||
|
||||
/**
|
||||
* 当答案与候选片段存在明确文本证据时,应返回对应引用。
|
||||
*/
|
||||
@Test
|
||||
public void shouldMatchReferenceByAnswerEvidence() {
|
||||
AgentKnowledgeReference unrelated = reference("doc-1", "周期结算金额等于订单金额减去佣金金额。");
|
||||
AgentKnowledgeReference cited = reference("faq-1",
|
||||
"问题:暑假安排是什么?答案:2026 年暑假安排为 7 月 1 日到 8 月 15 日。");
|
||||
|
||||
List<AgentKnowledgeReference> references = matcher.match(
|
||||
"根据查询到的信息,2026 年暑假安排为:7 月 1 日至 8 月 15 日。",
|
||||
List.of(unrelated, cited));
|
||||
|
||||
Assert.assertEquals(1, references.size());
|
||||
Assert.assertEquals("faq-1", references.get(0).getDocumentId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 即使只有一个候选,如果答案缺少文本支撑,也不应强行返回引用。
|
||||
*/
|
||||
@Test
|
||||
public void shouldNotGuessOnlyCandidateWithoutEvidence() {
|
||||
AgentKnowledgeReference candidate = reference("doc-1", "唯一命中片段");
|
||||
|
||||
List<AgentKnowledgeReference> references = matcher.match("final answer", List.of(candidate));
|
||||
|
||||
Assert.assertTrue(references.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* 多候选无明确文本支撑时,应返回空引用列表。
|
||||
*/
|
||||
@Test
|
||||
public void shouldNotGuessMultipleCandidatesWithoutEvidence() {
|
||||
AgentKnowledgeReference first = reference("doc-1", "周期结算金额等于订单金额减去佣金金额。");
|
||||
AgentKnowledgeReference second = reference("doc-2", "权限配置需要绑定角色与数据范围。");
|
||||
|
||||
List<AgentKnowledgeReference> references = matcher.match("final answer", List.of(first, second));
|
||||
|
||||
Assert.assertTrue(references.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期数字相同但语义内容不相关时,不应仅凭数字覆盖返回引用。
|
||||
*/
|
||||
@Test
|
||||
public void shouldNotMatchByDateNumbersOnly() {
|
||||
AgentKnowledgeReference unrelated = reference("doc-1",
|
||||
"内部文件 胜意科技AI Infra解决方案 技术文件 武汉科技大学计算机科学与技术学院 "
|
||||
+ "2026年1月 目录 1 总体目标 4 2 总体方案 5 3 详细方案 6 3.2 流程与编排 7 "
|
||||
+ "3.3 知识库管理 8 3.4 智能文档解析 9");
|
||||
|
||||
List<AgentKnowledgeReference> references = matcher.match(
|
||||
"根据系统中的信息,2026 年暑假的安排是:2026 年 7 月 1 日到 8 月 15 日。",
|
||||
List.of(unrelated));
|
||||
|
||||
Assert.assertTrue(references.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* 空答案或空候选不应返回引用。
|
||||
*/
|
||||
@Test
|
||||
public void shouldReturnEmptyWhenAnswerOrCandidatesEmpty() {
|
||||
AgentKnowledgeReference candidate = reference("doc-1", "问题:暑假安排是什么?答案:7月1日到8月15日。");
|
||||
|
||||
Assert.assertTrue(matcher.match("", List.of(candidate)).isEmpty());
|
||||
Assert.assertTrue(matcher.match("暑假安排是什么?", List.of()).isEmpty());
|
||||
}
|
||||
|
||||
private AgentKnowledgeReference reference(String documentId, String chunkContent) {
|
||||
AgentKnowledgeReference reference = new AgentKnowledgeReference();
|
||||
reference.setKnowledgeId("kb-1");
|
||||
reference.setKnowledgeName("知识库");
|
||||
reference.setDocumentId(documentId);
|
||||
reference.setDocumentName(documentId + ".md");
|
||||
reference.setChunkId(documentId + "-chunk");
|
||||
reference.setChunkContent(chunkContent);
|
||||
return reference;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user