feat: 完善多模型配置与运行切换
将模型连接统一持久化管理,并支持 Agent 运行中切换模型以及中断后选择模型继续。补充配置校验、事务与前后端交互测试。
This commit is contained in:
@@ -1,14 +1,62 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.agentscope.core.agui.adapter.AguiAgentAdapter;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import tech.easyflow.manuagent.project.ProjectFileService;
|
||||
import tech.easyflow.manuagent.project.ProjectService;
|
||||
import tech.easyflow.manuagent.skill.SkillService;
|
||||
|
||||
/**
|
||||
* 验证 Agent 事件持久化的精简规则。
|
||||
*/
|
||||
class AgentExecutionServiceTest {
|
||||
|
||||
/**
|
||||
* Run 在创建时已经固化模型配置 ID;执行和模型重连都必须沿用该 ID,
|
||||
* 不能在工厂内部重新读取可能已经变化的全局默认模型。
|
||||
*/
|
||||
@Test
|
||||
void shouldCreateAgentWithModelBoundToRun() {
|
||||
UUID projectId = UUID.randomUUID();
|
||||
UUID runId = UUID.randomUUID();
|
||||
UUID modelId = UUID.randomUUID();
|
||||
AgentFactory factory = mock(AgentFactory.class);
|
||||
AgentFactory.AgentHandle handle = mock(AgentFactory.AgentHandle.class);
|
||||
AguiAgentAdapter adapter = mock(AguiAgentAdapter.class);
|
||||
SkillService skillService = mock(SkillService.class);
|
||||
when(handle.adapter()).thenReturn(adapter);
|
||||
when(adapter.run(any())).thenReturn(Flux.empty());
|
||||
when(skillService.enabledNames()).thenReturn(new String[] {"document"});
|
||||
when(factory.create(eq(projectId), eq(modelId), any(String[].class))).thenReturn(handle);
|
||||
|
||||
AgentExecutionService service = new AgentExecutionService(
|
||||
new ObjectMapper(),
|
||||
factory,
|
||||
mock(AgentEventService.class),
|
||||
mock(ProjectFileService.class),
|
||||
skillService);
|
||||
ProjectService.ProjectView project = mock(ProjectService.ProjectView.class);
|
||||
when(project.id()).thenReturn(projectId);
|
||||
when(project.threadId()).thenReturn("project-" + projectId);
|
||||
AgentRunService.RunView run = new AgentRunService.RunView(
|
||||
runId, projectId, modelId, "INITIAL", "RUNNING", null, null, null, null);
|
||||
|
||||
service.execute(project, run, "执行测试", Mono.never(), () -> { }, () -> false);
|
||||
|
||||
verify(factory).create(eq(projectId), eq(modelId), any(String[].class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文档视觉结果保留图片路径元数据并丢弃 Base64 正文。
|
||||
*/
|
||||
|
||||
@@ -1,20 +1,87 @@
|
||||
package tech.easyflow.manuagent.agent;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.agentscope.core.model.transport.HttpTransportException;
|
||||
import io.agentscope.core.skill.AgentSkill;
|
||||
import java.nio.file.Path;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import tech.easyflow.manuagent.artifact.ArtifactService;
|
||||
import tech.easyflow.manuagent.auth.UserService;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.project.ProjectFileService;
|
||||
import tech.easyflow.manuagent.project.ProjectService;
|
||||
|
||||
/**
|
||||
* 验证 Agent Run 的模型重连边界。
|
||||
*/
|
||||
class AgentRunServiceTest {
|
||||
|
||||
/**
|
||||
* 运行中模型配置被编辑后,用户应能选择同一模型 ID 创建新的恢复 Run,
|
||||
* 使新 Agent 客户端重新读取数据库中的最新地址、模型标识和密钥。
|
||||
*/
|
||||
@Test
|
||||
void shouldRestartRunningTaskWhenSameModelConfigurationWasUpdated() {
|
||||
UUID projectId = UUID.randomUUID();
|
||||
UUID modelId = UUID.randomUUID();
|
||||
UUID currentRunId = UUID.randomUUID();
|
||||
UUID replacementRunId = UUID.randomUUID();
|
||||
OffsetDateTime now = OffsetDateTime.now();
|
||||
ProjectService.ProjectView project = new ProjectService.ProjectView(
|
||||
projectId, "测试企业", "测试项目", "thread-1", "ADVANCED", "MATERIAL_CHECK", 0L, now, now);
|
||||
AgentRunService.RunView current = new AgentRunService.RunView(
|
||||
currentRunId, projectId, modelId, "INITIAL", "RUNNING", null, null, now, null);
|
||||
AgentRunService.RunView replacement = new AgentRunService.RunView(
|
||||
replacementRunId, projectId, modelId, "RESUME", "RUNNING", null, null, now, null);
|
||||
|
||||
AgentRunMapper runMapper = mock(AgentRunMapper.class);
|
||||
AgentRunStore runStore = mock(AgentRunStore.class);
|
||||
ProjectService projectService = mock(ProjectService.class);
|
||||
UserService userService = mock(UserService.class);
|
||||
when(projectService.require(projectId)).thenReturn(project);
|
||||
when(userService.requireUserId("admin")).thenReturn(UUID.randomUUID());
|
||||
when(runStore.latest(projectId)).thenReturn(current);
|
||||
when(runStore.interruptedPhase(current, project)).thenReturn("MATERIAL_CHECK");
|
||||
when(runMapper.interruptRunning(currentRunId)).thenReturn(1);
|
||||
when(runStore.create(projectId, "RESUME", currentRunId, modelId)).thenReturn(replacement);
|
||||
|
||||
AgentRunService service = new AgentRunService(
|
||||
runMapper,
|
||||
new ObjectMapper(),
|
||||
mock(AgentExecutionService.class),
|
||||
mock(AgentOutputService.class),
|
||||
runStore,
|
||||
mock(AgentEventService.class),
|
||||
projectService,
|
||||
mock(ProjectFileService.class),
|
||||
userService,
|
||||
mock(ArtifactService.class),
|
||||
mock(ExecutorService.class),
|
||||
mock(TransactionTemplate.class));
|
||||
|
||||
// 服务会注册“提交后取消旧流并启动新流”的回调;测试只验证注册前的事务内状态转换。
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
try {
|
||||
assertThat(service.switchModel(projectId, modelId, () -> "admin")).isEqualTo(replacement);
|
||||
verify(runMapper).interruptRunning(currentRunId);
|
||||
verify(runStore).create(projectId, "RESUME", currentRunId, modelId);
|
||||
} finally {
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络故障和服务端错误允许重连,参数错误保持原始失败。
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import tech.easyflow.manuagent.entity.AgentRunEntity;
|
||||
import tech.easyflow.manuagent.common.ApiException;
|
||||
import tech.easyflow.manuagent.entity.ModelConfigEntity;
|
||||
import tech.easyflow.manuagent.mapper.AgentEventMapper;
|
||||
import tech.easyflow.manuagent.mapper.AgentRunMapper;
|
||||
import tech.easyflow.manuagent.mapper.ModelConfigMapper;
|
||||
@@ -49,10 +50,11 @@ class AgentRunStoreQueryTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认模型缺失属于数据库配置异常,不应在 ORM 迁移中新增 409 业务错误。
|
||||
* 数据库允许在首次启动时没有模型,因此启动 Run 时应返回可操作的业务错误,
|
||||
* 不能把正常的“尚未配置”状态暴露为服务端技术异常。
|
||||
*/
|
||||
@Test
|
||||
void shouldKeepMissingDefaultModelAsUnexpectedTechnicalFailure() {
|
||||
void shouldReportMissingDefaultModelAsConfigurationConflict() {
|
||||
AgentRunMapper runMapper = mock(AgentRunMapper.class);
|
||||
when(runMapper.selectCountByQuery(any(QueryWrapper.class))).thenReturn(0L);
|
||||
AgentRunStore store = new AgentRunStore(
|
||||
@@ -62,8 +64,49 @@ class AgentRunStoreQueryTest {
|
||||
new ObjectMapper());
|
||||
|
||||
assertThatThrownBy(() -> store.create(UUID.randomUUID(), "INITIAL", null))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.isNotInstanceOf(ApiException.class);
|
||||
.isInstanceOfSatisfying(ApiException.class, exception -> {
|
||||
assertThat(exception.status().value()).isEqualTo(409);
|
||||
assertThat(exception.code()).isEqualTo("MODEL_NOT_CONFIGURED");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户恢复任务时可以显式选择替代模型;新 Run 必须保存该模型 ID,
|
||||
* 不能再次回退到随后可能变化的全局默认模型。
|
||||
*/
|
||||
@Test
|
||||
void shouldCreateRunWithExplicitEnabledModel() {
|
||||
UUID projectId = UUID.randomUUID();
|
||||
UUID modelId = UUID.randomUUID();
|
||||
AgentRunMapper runMapper = mock(AgentRunMapper.class);
|
||||
ModelConfigMapper modelMapper = mock(ModelConfigMapper.class);
|
||||
when(runMapper.selectCountByQuery(any(QueryWrapper.class))).thenReturn(0L);
|
||||
|
||||
ModelConfigEntity selectedModel = new ModelConfigEntity();
|
||||
selectedModel.setId(modelId);
|
||||
selectedModel.setEnabled(true);
|
||||
when(modelMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(selectedModel);
|
||||
|
||||
AgentRunEntity storedRun = new AgentRunEntity();
|
||||
storedRun.setId(UUID.randomUUID());
|
||||
storedRun.setProjectId(projectId);
|
||||
storedRun.setModelConfigId(modelId);
|
||||
storedRun.setTriggerType("RESUME");
|
||||
storedRun.setStatus("RUNNING");
|
||||
when(runMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(storedRun);
|
||||
|
||||
AgentRunStore store = new AgentRunStore(
|
||||
runMapper,
|
||||
mock(AgentEventMapper.class),
|
||||
modelMapper,
|
||||
new ObjectMapper());
|
||||
|
||||
AgentRunService.RunView run = store.create(projectId, "RESUME", null, modelId);
|
||||
|
||||
ArgumentCaptor<AgentRunEntity> entityCaptor = ArgumentCaptor.forClass(AgentRunEntity.class);
|
||||
verify(runMapper).insertSelective(entityCaptor.capture());
|
||||
assertThat(entityCaptor.getValue().getModelConfigId()).isEqualTo(modelId);
|
||||
assertThat(run.modelConfigId()).isEqualTo(modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user