发布 v1.10 #5
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1482,10 +1482,11 @@ public class AgentRunService {
|
||||
if (imageUploadIds == null || imageUploadIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
tech.easyflow.ai.entity.Model model = agent == null || agent.getModelId() == null
|
||||
? null
|
||||
: modelService.getModelInstance(agent.getModelId());
|
||||
if (model == null || !Boolean.TRUE.equals(model.getSupportImage())) {
|
||||
AgentModelCapabilityResolver.Resolution capabilities =
|
||||
AgentModelCapabilityResolver.resolve(agent, () ->
|
||||
agent == null || agent.getModelId() == null
|
||||
? null : modelService.getModelInstance(agent.getModelId()));
|
||||
if (!capabilities.supportImage()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "当前 Agent 模型未启用多模态图片能力");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,12 @@ public class AgentRuntimeCompiler {
|
||||
if (model == null) {
|
||||
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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,62 @@ public class AgentRuntimeCompilerModelConfigTest {
|
||||
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 压缩配置。
|
||||
*
|
||||
@@ -230,13 +286,37 @@ public class AgentRuntimeCompilerModelConfigTest {
|
||||
* @throws Exception 反射调用失败时抛出
|
||||
*/
|
||||
private AgentModelSpec invokeModelSpec(AgentRuntimeCompiler compiler) throws Exception {
|
||||
Agent agent = new Agent();
|
||||
agent.setModelId(BigInteger.TEN);
|
||||
return invokeModelSpec(compiler, agent(Map.of()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用私有模型声明编译方法。
|
||||
*
|
||||
* @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.setAccessible(true);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用私有记忆策略编译方法。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user