发布 v1.10 #5

Merged
czm merged 147 commits from develop into main 2026-08-20 11:36:27 +08:00
5 changed files with 340 additions and 7 deletions
Showing only changes of commit ff5f90121b - Show all commits

View File

@@ -0,0 +1,80 @@
package tech.easyflow.agent.runtime;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.ai.entity.Model;
import java.util.Map;
import java.util.function.Supplier;
/**
* 解析 Agent 运行时应使用的模型输入能力。
*
* <p>正式运行严格使用发布快照;草稿试运行使用当前模型配置。</p>
*/
public final class AgentModelCapabilityResolver {
private static final String MODEL_SUMMARY = "modelSummary";
private static final String SUPPORT_IMAGE = "supportImage";
private static final String SUPPORT_IMAGE_BASE64_ONLY = "supportImageB64Only";
/**
* 禁止实例化能力解析工具。
*/
private AgentModelCapabilityResolver() {
}
/**
* 解析 Agent 当前运行模式对应的模型能力。
*
* <p>只要 Agent 携带发布快照,就不会读取实时模型能力。旧快照缺少能力字段时按不支持处理。</p>
*
* @param agent Agent 运行视图
* @param liveModelSupplier 草稿态实时模型提供器
* @return 规范化模型能力
*/
public static Resolution resolve(Agent agent, Supplier<Model> liveModelSupplier) {
Map<String, Object> publishedSnapshot = agent == null
? null : agent.getPublishedSnapshotJson();
if (publishedSnapshot != null && !publishedSnapshot.isEmpty()) {
return fromPublishedSnapshot(publishedSnapshot);
}
Model liveModel = liveModelSupplier == null ? null : liveModelSupplier.get();
return new Resolution(
liveModel != null && Boolean.TRUE.equals(liveModel.getSupportImage()),
liveModel != null && Boolean.TRUE.equals(liveModel.getSupportImageB64Only()));
}
/**
* 从发布快照读取模型能力。
*
* @param publishedSnapshot Agent 发布快照
* @return 快照中的模型能力
*/
private static Resolution fromPublishedSnapshot(Map<String, Object> publishedSnapshot) {
Object summaryValue = publishedSnapshot.get(MODEL_SUMMARY);
if (!(summaryValue instanceof Map<?, ?> modelSummary)) {
return Resolution.disabled();
}
return new Resolution(
Boolean.TRUE.equals(modelSummary.get(SUPPORT_IMAGE)),
Boolean.TRUE.equals(modelSummary.get(SUPPORT_IMAGE_BASE64_ONLY)));
}
/**
* 模型输入能力解析结果。
*
* @param supportImage 是否支持图片输入
* @param supportImageBase64Only 是否仅支持 Base64 图片输入
*/
public record Resolution(boolean supportImage, boolean supportImageBase64Only) {
/**
* 创建关闭全部图片能力的结果。
*
* @return 关闭图片能力的结果
*/
private static Resolution disabled() {
return new Resolution(false, false);
}
}
}

View File

@@ -1482,10 +1482,11 @@ public class AgentRunService {
if (imageUploadIds == null || imageUploadIds.isEmpty()) { if (imageUploadIds == null || imageUploadIds.isEmpty()) {
return; return;
} }
tech.easyflow.ai.entity.Model model = agent == null || agent.getModelId() == null AgentModelCapabilityResolver.Resolution capabilities =
? null AgentModelCapabilityResolver.resolve(agent, () ->
: modelService.getModelInstance(agent.getModelId()); agent == null || agent.getModelId() == null
if (model == null || !Boolean.TRUE.equals(model.getSupportImage())) { ? null : modelService.getModelInstance(agent.getModelId()));
if (!capabilities.supportImage()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "当前 Agent 模型未启用多模态图片能力"); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "当前 Agent 模型未启用多模态图片能力");
} }
} }

View File

@@ -108,7 +108,12 @@ public class AgentRuntimeCompiler {
if (model == null) { if (model == null) {
throw new BusinessException("Agent 模型不存在"); throw new BusinessException("Agent 模型不存在");
} }
return AgentModelSpecMapper.fromModel(model, agent.getModelConfigJson()); AgentModelSpec spec = AgentModelSpecMapper.fromModel(model, agent.getModelConfigJson());
AgentModelCapabilityResolver.Resolution capabilities =
AgentModelCapabilityResolver.resolve(agent, () -> model);
spec.setSupportImage(capabilities.supportImage());
spec.setSupportImageBase64Only(capabilities.supportImageBase64Only());
return spec;
} }
private AgentGenerationOptions buildGenerationOptions(Map<String, Object> config) { private AgentGenerationOptions buildGenerationOptions(Map<String, Object> config) {

View File

@@ -0,0 +1,167 @@
package tech.easyflow.agent.runtime;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.web.server.ResponseStatusException;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.ai.entity.Model;
import tech.easyflow.ai.service.ModelService;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;
/**
* Agent 正式与草稿聊天图片能力边界测试。
*/
public class AgentRunServiceImageCapabilityTest {
/**
* 验证正式聊天允许快照已启用的图片能力,且不读取实时模型能力。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void formalChatShouldUseEnabledPublishedCapability() throws Exception {
ModelService modelService = Mockito.mock(ModelService.class);
AgentRunService service = service(modelService);
Agent agent = publishedAgent(true);
invokeAssertImageCapability(service, agent);
Mockito.verifyNoInteractions(modelService);
}
/**
* 验证正式聊天拒绝快照未启用的图片能力,且不读取实时模型能力。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void formalChatShouldRejectDisabledPublishedCapability() throws Exception {
ModelService modelService = Mockito.mock(ModelService.class);
AgentRunService service = service(modelService);
Agent agent = publishedAgent(false);
ResponseStatusException error = expectImageCapabilityError(service, agent);
Assert.assertEquals(400, error.getStatusCode().value());
Mockito.verifyNoInteractions(modelService);
}
/**
* 验证旧快照缺少图片能力字段时安全拒绝图片。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void formalChatShouldFailClosedForLegacySnapshot() throws Exception {
ModelService modelService = Mockito.mock(ModelService.class);
AgentRunService service = service(modelService);
Agent agent = new Agent();
agent.setModelId(BigInteger.TEN);
agent.setPublishedSnapshotJson(Map.of(
"modelSummary", Map.of("modelName", "legacy-model")));
ResponseStatusException error = expectImageCapabilityError(service, agent);
Assert.assertEquals(400, error.getStatusCode().value());
Mockito.verifyNoInteractions(modelService);
}
/**
* 验证草稿聊天继续读取实时模型图片能力。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void draftChatShouldUseLiveModelCapability() throws Exception {
Model model = new Model();
model.setSupportImage(true);
ModelService modelService = Mockito.mock(ModelService.class);
Mockito.when(modelService.getModelInstance(BigInteger.TEN)).thenReturn(model);
AgentRunService service = service(modelService);
Agent agent = new Agent();
agent.setModelId(BigInteger.TEN);
invokeAssertImageCapability(service, agent);
Mockito.verify(modelService).getModelInstance(BigInteger.TEN);
}
/**
* 创建已注入模型服务的运行服务。
*
* @param modelService 模型服务
* @return Agent 运行服务
* @throws Exception 字段注入失败时抛出
*/
private AgentRunService service(ModelService modelService) throws Exception {
AgentRunService service = new AgentRunService();
Field field = AgentRunService.class.getDeclaredField("modelService");
field.setAccessible(true);
field.set(service, modelService);
return service;
}
/**
* 创建携带图片能力的正式运行 Agent。
*
* @param supportImage 是否支持图片
* @return 正式运行 Agent
*/
private Agent publishedAgent(boolean supportImage) {
Agent agent = new Agent();
agent.setModelId(BigInteger.TEN);
agent.setPublishedSnapshotJson(Map.of(
"modelSummary", Map.of(
"supportImage", supportImage,
"supportImageB64Only", false)));
return agent;
}
/**
* 调用图片能力校验。
*
* @param service Agent 运行服务
* @param agent Agent 运行视图
* @throws Exception 反射调用失败或能力校验失败时抛出
*/
private void invokeAssertImageCapability(AgentRunService service,
Agent agent) throws Exception {
Method method = AgentRunService.class.getDeclaredMethod(
"assertImageCapability", Agent.class, List.class);
method.setAccessible(true);
try {
method.invoke(service, agent, List.of("image-upload-id"));
} catch (InvocationTargetException error) {
if (error.getCause() instanceof Exception cause) {
throw cause;
}
throw error;
}
}
/**
* 调用图片能力校验并返回预期的业务错误。
*
* @param service Agent 运行服务
* @param agent Agent 运行视图
* @return 图片能力错误
* @throws Exception 反射调用失败时抛出
*/
private ResponseStatusException expectImageCapabilityError(AgentRunService service,
Agent agent) throws Exception {
try {
invokeAssertImageCapability(service, agent);
Assert.fail("expected ResponseStatusException");
return null;
} catch (ResponseStatusException error) {
return error;
}
}
}

View File

@@ -144,6 +144,62 @@ public class AgentRuntimeCompilerModelConfigTest {
Assert.assertEquals(AgentMessageContentFormat.STANDARD, spec.getMessageContentFormat()); Assert.assertEquals(AgentMessageContentFormat.STANDARD, spec.getMessageContentFormat());
} }
/**
* 验证正式运行的图片能力严格使用发布快照。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void publishedImageCapabilityShouldOverrideLiveModel() throws Exception {
Model model = model(Map.of());
model.setSupportImage(false);
model.setSupportImageB64Only(false);
Agent agent = agent(Map.of(
"modelSummary", Map.of(
"supportImage", true,
"supportImageB64Only", true)));
AgentModelSpec spec = invokeModelSpec(compiler(model), agent);
Assert.assertTrue(spec.isSupportImage());
Assert.assertTrue(spec.isSupportImageBase64Only());
}
/**
* 验证旧发布快照缺少能力字段时关闭图片能力。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void missingPublishedImageCapabilityShouldFailClosed() throws Exception {
Model model = model(Map.of());
model.setSupportImage(true);
model.setSupportImageB64Only(true);
Agent agent = agent(Map.of("modelSummary", Map.of("modelName", "legacy-model")));
AgentModelSpec spec = invokeModelSpec(compiler(model), agent);
Assert.assertFalse(spec.isSupportImage());
Assert.assertFalse(spec.isSupportImageBase64Only());
}
/**
* 验证草稿运行继续使用实时模型图片能力。
*
* @throws Exception 反射调用失败时抛出
*/
@Test
public void draftImageCapabilityShouldUseLiveModel() throws Exception {
Model model = model(Map.of());
model.setSupportImage(true);
model.setSupportImageB64Only(false);
AgentModelSpec spec = invokeModelSpec(compiler(model));
Assert.assertTrue(spec.isSupportImage());
Assert.assertFalse(spec.isSupportImageBase64Only());
}
/** /**
* 验证 EasyFlow 忽略新旧消息数阈值,仅保留 Token 压缩配置。 * 验证 EasyFlow 忽略新旧消息数阈值,仅保留 Token 压缩配置。
* *
@@ -230,13 +286,37 @@ public class AgentRuntimeCompilerModelConfigTest {
* @throws Exception 反射调用失败时抛出 * @throws Exception 反射调用失败时抛出
*/ */
private AgentModelSpec invokeModelSpec(AgentRuntimeCompiler compiler) throws Exception { private AgentModelSpec invokeModelSpec(AgentRuntimeCompiler compiler) throws Exception {
Agent agent = new Agent(); return invokeModelSpec(compiler, agent(Map.of()));
agent.setModelId(BigInteger.TEN); }
/**
* 调用私有模型声明编译方法。
*
* @param compiler 编译器
* @param agent Agent 运行视图
* @return 模型声明
* @throws Exception 反射调用失败时抛出
*/
private AgentModelSpec invokeModelSpec(AgentRuntimeCompiler compiler,
Agent agent) throws Exception {
Method method = AgentRuntimeCompiler.class.getDeclaredMethod("buildModelSpec", Agent.class); Method method = AgentRuntimeCompiler.class.getDeclaredMethod("buildModelSpec", Agent.class);
method.setAccessible(true); method.setAccessible(true);
return (AgentModelSpec) method.invoke(compiler, agent); return (AgentModelSpec) method.invoke(compiler, agent);
} }
/**
* 创建测试 Agent。
*
* @param publishedSnapshot 发布快照;空映射表示草稿态
* @return 测试 Agent
*/
private Agent agent(Map<String, Object> publishedSnapshot) {
Agent agent = new Agent();
agent.setModelId(BigInteger.TEN);
agent.setPublishedSnapshotJson(publishedSnapshot);
return agent;
}
/** /**
* 调用私有记忆策略编译方法。 * 调用私有记忆策略编译方法。
* *