perf: 优化中断续聊表现,被中断的回复仍可以进入上下文中,保证记忆连续性
- 续聊逻辑优化
This commit is contained in:
@@ -24,6 +24,9 @@ import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext;
|
||||
import com.easyagents.agent.runtime.skill.AgentSkillSpec;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolResult;
|
||||
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||
import com.easyagents.agent.runtime.tool.operate.AgentOperateToolAdapter;
|
||||
import com.easyagents.agent.runtime.tool.operate.AgentOperateToolSpec;
|
||||
import com.easyagents.agent.runtime.tool.operate.AgentOperateToolType;
|
||||
import io.agentscope.core.ReActAgent;
|
||||
import io.agentscope.core.hook.*;
|
||||
import io.agentscope.core.memory.autocontext.AutoContextHook;
|
||||
@@ -45,9 +48,11 @@ import java.lang.reflect.Field;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
/**
|
||||
* 测试有状态 AgentScope 运行时。
|
||||
@@ -181,6 +186,65 @@ public class AgentScopeStatefulRuntimeTest {
|
||||
Assert.assertTrue(interceptors.stream().anyMatch(ToolHitlInterceptor.class::isInstance));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRegisterOperateToolsIntoToolkit() {
|
||||
AgentInitRequest request = initRequest();
|
||||
request.getAgentDefinition().setOperateToolSpecs(List.of(
|
||||
operateToolSpec(AgentOperateToolType.READ_FILE),
|
||||
operateToolSpec(AgentOperateToolType.WRITE_FILE),
|
||||
operateToolSpec(AgentOperateToolType.SHELL)));
|
||||
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||
|
||||
runtime.init(request);
|
||||
|
||||
Toolkit toolkit = runtime.getAgent().getToolkit();
|
||||
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.VIEW_TEXT_FILE_TOOL));
|
||||
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.LIST_DIRECTORY_TOOL));
|
||||
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.WRITE_TEXT_FILE_TOOL));
|
||||
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.INSERT_TEXT_FILE_TOOL));
|
||||
Assert.assertNotNull(toolkit.getTool(AgentOperateToolAdapter.EXECUTE_SHELL_COMMAND_TOOL));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSuspendShellOperateToolWithToolHitlInterceptor() {
|
||||
AgentInitRequest request = initRequest();
|
||||
AgentOperateToolSpec shell = operateToolSpec(AgentOperateToolType.SHELL);
|
||||
shell.setShellAllowedCommands(Set.of());
|
||||
request.getAgentDefinition().setOperateToolSpecs(List.of(shell));
|
||||
AgentScopeReActRuntime runtime = runtimeWithModel(List.of(ChatResponse.builder()
|
||||
.id("shell-call-message")
|
||||
.content(List.of(ToolUseBlock.builder()
|
||||
.id("call-shell")
|
||||
.name(AgentOperateToolAdapter.EXECUTE_SHELL_COMMAND_TOOL)
|
||||
.input(Map.of("command", "echo hello"))
|
||||
.build()))
|
||||
.finishReason("tool_calls")
|
||||
.build()));
|
||||
|
||||
runtime.init(request);
|
||||
List<AgentRuntimeEvent> events = runtime.stream(AgentMessage.text(AgentMessageRole.USER, "run shell"))
|
||||
.collectList()
|
||||
.block(Duration.ofSeconds(5));
|
||||
|
||||
Assert.assertNotNull(events);
|
||||
Assert.assertTrue(events.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED));
|
||||
Assert.assertTrue(events.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.SUSPENDED));
|
||||
}
|
||||
|
||||
@Test(expected = AgentRuntimeException.class)
|
||||
public void shouldRejectOperateToolNameConflictWithBusinessTool() {
|
||||
AgentInitRequest request = initRequest();
|
||||
AgentToolSpec toolSpec = new AgentToolSpec();
|
||||
toolSpec.setName(AgentOperateToolAdapter.EXECUTE_SHELL_COMMAND_TOOL);
|
||||
toolSpec.setDescription("conflict");
|
||||
request.getAgentDefinition().setToolSpecs(List.of(toolSpec));
|
||||
request.setToolInvokers(Map.of(AgentOperateToolAdapter.EXECUTE_SHELL_COMMAND_TOOL,
|
||||
(arguments, context) -> AgentToolResult.success("ok")));
|
||||
request.getAgentDefinition().setOperateToolSpecs(List.of(operateToolSpec(AgentOperateToolType.SHELL)));
|
||||
|
||||
fakeRuntime().init(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldEnablePendingToolRecoveryForRejectedHitlContinuation() {
|
||||
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||
@@ -558,6 +622,40 @@ public class AgentScopeStatefulRuntimeTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldPersistPartialAssistantMessageWhenStreamIsCancelled() throws Exception {
|
||||
InMemoryAgentSessionStore sessionStore = new InMemoryAgentSessionStore();
|
||||
AgentInitRequest request = initRequest();
|
||||
request.setSessionStore(sessionStore);
|
||||
AgentScopeReActRuntime runtime = runtimeWithModel(List.of(
|
||||
ChatResponse.builder()
|
||||
.id("partial-message")
|
||||
.content(List.of(TextBlock.builder().text("partial answer").build()))
|
||||
.finishReason("stop")
|
||||
.build()),
|
||||
Duration.ofMillis(500));
|
||||
runtime.init(request);
|
||||
|
||||
CompletableFuture<AgentRuntimeEvent> firstDelta = new CompletableFuture<>();
|
||||
reactor.core.Disposable disposable = runtime.stream(AgentMessage.text(AgentMessageRole.USER, "first"))
|
||||
.subscribe(event -> {
|
||||
if (event.getEventType() == AgentRuntimeEventType.MESSAGE_DELTA) {
|
||||
firstDelta.complete(event);
|
||||
}
|
||||
}, firstDelta::completeExceptionally);
|
||||
AgentRuntimeEvent delta = firstDelta.get(3, TimeUnit.SECONDS);
|
||||
Assert.assertEquals("partial answer", delta.getPayload().get("text"));
|
||||
|
||||
disposable.dispose();
|
||||
awaitCondition(() -> sessionStore.exists("session-1"));
|
||||
AgentScopeReActRuntime restoredRuntime = fakeRuntime();
|
||||
restoredRuntime.init(request);
|
||||
|
||||
Assert.assertTrue(restoredRuntime.getAgent().getMemory().getMessages().stream()
|
||||
.anyMatch(message -> message.getRole() == MsgRole.ASSISTANT
|
||||
&& "partial answer".equals(message.getTextContent())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotDuplicateNormalToolEventsFromMainStream() {
|
||||
AgentInitRequest request = initRequest();
|
||||
@@ -886,11 +984,15 @@ public class AgentScopeStatefulRuntimeTest {
|
||||
}
|
||||
|
||||
private AgentScopeReActRuntime runtimeWithModel(List<ChatResponse> responses) {
|
||||
return runtimeWithModel(responses, Duration.ZERO);
|
||||
}
|
||||
|
||||
private AgentScopeReActRuntime runtimeWithModel(List<ChatResponse> responses, Duration completionDelay) {
|
||||
AgentScopeModelFactory modelFactory = new AgentScopeModelFactory() {
|
||||
@Override
|
||||
public Model create(AgentModelSpec modelSpec,
|
||||
com.easyagents.agent.runtime.model.AgentGenerationOptions generationOptions) {
|
||||
return new ScriptedModel(modelSpec == null ? "fake-model" : modelSpec.getModelName(), responses);
|
||||
return new ScriptedModel(modelSpec == null ? "fake-model" : modelSpec.getModelName(), responses, completionDelay);
|
||||
}
|
||||
};
|
||||
return new AgentScopeReActRuntime(modelFactory, new AgentScopeToolAdapter(),
|
||||
@@ -902,10 +1004,12 @@ public class AgentScopeStatefulRuntimeTest {
|
||||
|
||||
private final String modelName;
|
||||
private final List<ChatResponse> responses;
|
||||
private final Duration completionDelay;
|
||||
|
||||
private ScriptedModel(String modelName, List<ChatResponse> responses) {
|
||||
private ScriptedModel(String modelName, List<ChatResponse> responses, Duration completionDelay) {
|
||||
this.modelName = modelName;
|
||||
this.responses = responses;
|
||||
this.completionDelay = completionDelay == null ? Duration.ZERO : completionDelay;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -916,7 +1020,11 @@ public class AgentScopeStatefulRuntimeTest {
|
||||
List<ChatResponse> selectedResponses = hasToolResult && responses.size() > 1
|
||||
? responses.subList(1, responses.size())
|
||||
: responses.subList(0, 1);
|
||||
return Flux.fromIterable(selectedResponses);
|
||||
Flux<ChatResponse> responseFlux = Flux.fromIterable(selectedResponses);
|
||||
if (completionDelay.isZero() || completionDelay.isNegative()) {
|
||||
return responseFlux;
|
||||
}
|
||||
return responseFlux.concatWith(Flux.never()).timeout(completionDelay, Flux.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -968,6 +1076,13 @@ public class AgentScopeStatefulRuntimeTest {
|
||||
return spec;
|
||||
}
|
||||
|
||||
private AgentOperateToolSpec operateToolSpec(AgentOperateToolType type) {
|
||||
AgentOperateToolSpec spec = new AgentOperateToolSpec();
|
||||
spec.setType(type);
|
||||
spec.setBaseDir(System.getProperty("java.io.tmpdir"));
|
||||
return spec;
|
||||
}
|
||||
|
||||
private SkillBox skillBox(Toolkit toolkit) {
|
||||
return new AgentScopeSkillAdapter().createSkillBox(skillBoxSpec(), toolkit,
|
||||
Map.of("skill-1", List.of(new NoopAgentTool("search"))));
|
||||
@@ -1022,4 +1137,15 @@ public class AgentScopeStatefulRuntimeTest {
|
||||
private boolean isRuntimeHook(Hook hook) {
|
||||
return hook instanceof AgentScopeRuntimeHook;
|
||||
}
|
||||
|
||||
private void awaitCondition(BooleanSupplier condition) throws Exception {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3);
|
||||
while (System.nanoTime() < deadline) {
|
||||
if (condition.getAsBoolean()) {
|
||||
return;
|
||||
}
|
||||
Thread.sleep(20L);
|
||||
}
|
||||
Assert.fail("Condition was not met in time.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user