diff --git a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelSpecMapper.java b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelSpecMapper.java new file mode 100644 index 00000000..b6ca972b --- /dev/null +++ b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentModelSpecMapper.java @@ -0,0 +1,122 @@ +package tech.easyflow.agent.runtime; + +import com.easyagents.agent.runtime.model.AgentHttpVersionPolicy; +import com.easyagents.agent.runtime.model.AgentModelProviderType; +import com.easyagents.agent.runtime.model.AgentModelSpec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tech.easyflow.ai.entity.Model; + +import java.util.Locale; +import java.util.Map; + +/** + * 将 EasyFlow 模型配置映射为智能体运行时模型声明。 + */ +public final class AgentModelSpecMapper { + + private static final Logger LOG = LoggerFactory.getLogger(AgentModelSpecMapper.class); + + /** + * 禁止实例化工具类。 + */ + private AgentModelSpecMapper() { + } + + /** + * 按模型持久化配置创建运行时模型声明。 + * + * @param model 已补齐供应商默认配置的模型 + * @return 运行时模型声明 + * @throws IllegalArgumentException 模型为空时抛出 + */ + public static AgentModelSpec fromModel(Model model) { + return fromModel(model, Map.of()); + } + + /** + * 按模型配置和 Agent 快照覆盖项创建运行时模型声明。 + * + * @param model 已补齐供应商默认配置的模型 + * @param overrides Agent 发布快照中的模型覆盖项 + * @return 运行时模型声明 + * @throws IllegalArgumentException 模型为空时抛出 + */ + public static AgentModelSpec fromModel(Model model, Map overrides) { + if (model == null) { + throw new IllegalArgumentException("模型配置不能为空"); + } + Map safeOverrides = overrides == null ? Map.of() : overrides; + AgentModelSpec spec = new AgentModelSpec(); + String providerType = stringValue( + safeOverrides, + "providerType", + model.getModelProvider() == null ? null : model.getModelProvider().getProviderType()); + spec.setProviderType(parseProviderType(providerType)); + spec.setModelName(stringValue(safeOverrides, "modelName", model.getModelName())); + spec.setBaseUrl(stringValue(safeOverrides, "baseUrl", model.getEndpoint())); + spec.setEndpointPath(stringValue(safeOverrides, "endpointPath", model.getRequestPath())); + spec.setApiKey(stringValue(safeOverrides, "apiKey", model.getApiKey())); + spec.setSupportImage(Boolean.TRUE.equals(model.getSupportImage())); + spec.setSupportImageBase64Only(Boolean.TRUE.equals(model.getSupportImageB64Only())); + spec.setHttpVersionPolicy(parseHttpVersionPolicy(model)); + spec.getMetadata().put("modelId", model.getId()); + if (providerType != null && !providerType.isBlank()) { + spec.getMetadata().put("sourceProviderType", providerType); + } + return spec; + } + + /** + * 解析模型配置中的 Agent HTTP 版本策略。 + * + * @param model 模型配置 + * @return HTTP 版本策略,缺失或非法时返回 AUTO + */ + private static AgentHttpVersionPolicy parseHttpVersionPolicy(Model model) { + Object rawPolicy = model.getOptions() == null + ? null + : model.getOptions().get("agentHttpVersionPolicy"); + if (rawPolicy == null || String.valueOf(rawPolicy).isBlank()) { + return AgentHttpVersionPolicy.AUTO; + } + String normalizedPolicy = String.valueOf(rawPolicy).trim().toUpperCase(Locale.ROOT); + try { + return AgentHttpVersionPolicy.valueOf(normalizedPolicy); + } catch (IllegalArgumentException exception) { + LOG.warn("Invalid Agent HTTP version policy '{}' for model {}, fallback to AUTO", + rawPolicy, model.getId()); + return AgentHttpVersionPolicy.AUTO; + } + } + + /** + * 解析 AgentScope 支持的模型供应商类型。 + * + * @param providerType 供应商类型 + * @return 运行时供应商类型,未知值按 OpenAI-compatible 处理 + */ + private static AgentModelProviderType parseProviderType(String providerType) { + if (providerType == null || providerType.isBlank()) { + return AgentModelProviderType.OPENAI_COMPATIBLE; + } + try { + return AgentModelProviderType.valueOf(providerType.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ignored) { + return AgentModelProviderType.OPENAI_COMPATIBLE; + } + } + + /** + * 读取字符串覆盖项。 + * + * @param values 配置映射 + * @param key 字段名 + * @param defaultValue 默认值 + * @return 覆盖值或默认值 + */ + private static String stringValue(Map values, String key, String defaultValue) { + Object value = values.get(key); + return value == null ? defaultValue : String.valueOf(value); + } +} diff --git a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRuntimeCompiler.java b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRuntimeCompiler.java index 227b4f46..9a55758f 100644 --- a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRuntimeCompiler.java +++ b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentRuntimeCompiler.java @@ -13,7 +13,6 @@ import com.easyagents.agent.runtime.memory.AgentMemoryType; import com.easyagents.agent.runtime.mcp.McpSpec; import com.easyagents.agent.runtime.mcp.McpTransportType; import com.easyagents.agent.runtime.model.AgentGenerationOptions; -import com.easyagents.agent.runtime.model.AgentModelProviderType; import com.easyagents.agent.runtime.model.AgentModelSpec; import com.easyagents.agent.runtime.tool.AgentToolCategory; import com.easyagents.agent.runtime.tool.AgentToolResult; @@ -104,18 +103,7 @@ public class AgentRuntimeCompiler { if (model == null) { throw new BusinessException("Agent 模型不存在"); } - Map config = agent.getModelConfigJson(); - AgentModelSpec spec = new AgentModelSpec(); - String providerType = stringValue(config, "providerType", model.getModelProvider() == null ? null : model.getModelProvider().getProviderType()); - spec.setProviderType(parseProviderType(providerType)); - spec.setModelName(stringValue(config, "modelName", model.getModelName())); - spec.setBaseUrl(stringValue(config, "baseUrl", model.getEndpoint())); - spec.setEndpointPath(stringValue(config, "endpointPath", model.getRequestPath())); - spec.setApiKey(stringValue(config, "apiKey", model.getApiKey())); - spec.setSupportImage(Boolean.TRUE.equals(model.getSupportImage())); - spec.setSupportImageBase64Only(Boolean.TRUE.equals(model.getSupportImageB64Only())); - spec.getMetadata().put("modelId", model.getId()); - return spec; + return AgentModelSpecMapper.fromModel(model, agent.getModelConfigJson()); } private AgentGenerationOptions buildGenerationOptions(Map config) { @@ -756,17 +744,6 @@ public class AgentRuntimeCompiler { return description == null ? "" : description; } - private AgentModelProviderType parseProviderType(String providerType) { - if (providerType == null || providerType.isBlank()) { - return AgentModelProviderType.OPENAI_COMPATIBLE; - } - try { - return AgentModelProviderType.valueOf(providerType.trim().toUpperCase()); - } catch (IllegalArgumentException ignored) { - return AgentModelProviderType.OPENAI_COMPATIBLE; - } - } - private AgentMemoryType memoryTypeValue(Map map, String key) { String value = stringValue(map, key, AgentMemoryType.AUTO_CONTEXT.name()); try { diff --git a/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentScopeChatModelConnectivityVerifier.java b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentScopeChatModelConnectivityVerifier.java new file mode 100644 index 00000000..4fc51518 --- /dev/null +++ b/easyflow-modules/easyflow-module-agent/src/main/java/tech/easyflow/agent/runtime/AgentScopeChatModelConnectivityVerifier.java @@ -0,0 +1,275 @@ +package tech.easyflow.agent.runtime; + +import com.easyagents.agent.runtime.agentscope.AgentHttpTransportProvider; +import com.easyagents.agent.runtime.agentscope.AgentScopeMessageAdapter; +import com.easyagents.agent.runtime.agentscope.AgentScopeModelFactory; +import com.easyagents.agent.runtime.message.AgentContentBlock; +import com.easyagents.agent.runtime.message.AgentMediaBlock; +import com.easyagents.agent.runtime.message.AgentMessage; +import com.easyagents.agent.runtime.message.AgentMessageRole; +import com.easyagents.agent.runtime.message.AgentTextBlock; +import com.easyagents.agent.runtime.model.AgentGenerationOptions; +import com.easyagents.agent.runtime.model.AgentModelFactory; +import com.easyagents.agent.runtime.model.AgentModelProviderType; +import com.easyagents.agent.runtime.model.AgentModelSpec; +import io.agentscope.core.message.ContentBlock; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.TextBlock; +import io.agentscope.core.model.ChatResponse; +import io.agentscope.core.model.ExecutionConfig; +import io.agentscope.core.model.GenerateOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import tech.easyflow.ai.entity.Model; +import tech.easyflow.ai.service.support.VlmVerificationImage; +import tech.easyflow.ai.service.verification.ChatModelConnectivityVerifier; +import tech.easyflow.ai.service.verification.ChatModelVerificationResult; +import tech.easyflow.common.web.exceptions.BusinessException; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +/** + * 使用 AgentScope 真实运行链路验证 Chat Model 与 VLM 连通性。 + */ +@Component +public class AgentScopeChatModelConnectivityVerifier implements ChatModelConnectivityVerifier { + + private static final Logger LOG = LoggerFactory.getLogger(AgentScopeChatModelConnectivityVerifier.class); + /** 为未识别关闭思考扩展参数的推理模型保留足够的小额输出预算。 */ + private static final int MAX_TOKENS = 256; + private static final Duration DEFAULT_PHASE_TIMEOUT = Duration.ofSeconds(60); + + /** AgentScope 模型工厂。 */ + private final AgentModelFactory modelFactory; + /** EasyAgents 与 AgentScope 的消息适配器。 */ + private final AgentScopeMessageAdapter messageAdapter; + /** 单阶段最大等待时间。 */ + private final Duration phaseTimeout; + + /** + * 使用生产运行时组件创建验证器。 + */ + public AgentScopeChatModelConnectivityVerifier() { + this(new AgentScopeModelFactory(), new AgentScopeMessageAdapter(), DEFAULT_PHASE_TIMEOUT); + } + + /** + * 使用指定依赖创建验证器,供测试和受控运行环境使用。 + * + * @param modelFactory AgentScope 模型工厂 + * @param messageAdapter 消息适配器 + * @param phaseTimeout 单阶段最大等待时间 + */ + AgentScopeChatModelConnectivityVerifier( + AgentModelFactory modelFactory, + AgentScopeMessageAdapter messageAdapter, + Duration phaseTimeout) { + this.modelFactory = modelFactory; + this.messageAdapter = messageAdapter; + this.phaseTimeout = phaseTimeout; + } + + /** + * 依次验证非流式基础连接与流式响应能力。 + * + * @param model 已补齐供应商默认配置的模型 + * @return 双阶段验证结果 + * @throws BusinessException 非流式基础连接失败时抛出 + */ + @Override + public ChatModelVerificationResult verify(Model model) { + AgentModelSpec modelSpec = AgentModelSpecMapper.fromModel(model); + String effectiveHttpVersion = AgentHttpTransportProvider.resolveEffectivePolicy( + modelSpec.getHttpVersionPolicy(), modelSpec.getBaseUrl()).name(); + AgentMessage verificationMessage = buildVerificationMessage(modelSpec.isSupportImage()); + + try { + verifyPhase(modelSpec, verificationMessage, false); + } catch (BusinessException exception) { + LOG.error("AgentScope model base connectivity verification failed, modelId={}, httpPolicy={}", + model.getId(), effectiveHttpVersion, exception); + throw exception; + } catch (Exception exception) { + LOG.error("AgentScope model base connectivity verification failed, modelId={}, httpPolicy={}", + model.getId(), effectiveHttpVersion, exception); + throw new BusinessException(400, 1, "模型基础连接验证失败,请查看后端日志", exception); + } + + try { + verifyPhase(modelSpec, verificationMessage, true); + return ChatModelVerificationResult.passed(effectiveHttpVersion); + } catch (Exception exception) { + LOG.warn("AgentScope model streaming verification failed, modelId={}, httpPolicy={}", + model.getId(), effectiveHttpVersion, exception); + return ChatModelVerificationResult.streamingUnavailable(effectiveHttpVersion); + } + } + + /** + * 执行一次指定流式模式的模型请求并校验响应。 + * + * @param modelSpec 运行时模型声明 + * @param verificationMessage 验证消息 + * @param stream 是否启用流式响应 + * @throws BusinessException 响应为空或 VLM 图片识别错误时抛出 + */ + private void verifyPhase(AgentModelSpec modelSpec, + AgentMessage verificationMessage, + boolean stream) { + AgentGenerationOptions generationOptions = new AgentGenerationOptions(); + generationOptions.setStream(stream); + generationOptions.setThinkingEnabled(false); + disableOpenAiCompatibleThinking(modelSpec, generationOptions); + generationOptions.setMaxTokens(MAX_TOKENS); + io.agentscope.core.model.Model agentScopeModel = modelFactory.create(modelSpec, generationOptions); + + Msg message = messageAdapter.toMsg(verificationMessage); + GenerateOptions requestOptions = GenerateOptions.builder() + .stream(stream) + .maxTokens(MAX_TOKENS) + .executionConfig(ExecutionConfig.builder() + .timeout(phaseTimeout) + .maxAttempts(1) + .build()) + .build(); + List responses = agentScopeModel + .stream(List.of(message), List.of(), requestOptions) + .timeout(phaseTimeout) + .collectList() + .block(phaseTimeout.plusSeconds(1)); + String responseText = aggregateText(responses); + validateResponse(modelSpec.isSupportImage(), responseText); + } + + /** + * 为支持该扩展字段的 OpenAI-compatible 服务显式关闭思考。 + * + *

AgentScope 的通用 OpenAI Formatter 不会读取中立的 + * {@code thinkingEnabled} 字段,需通过扩展请求体传递;原生 OpenAI、 + * DeepSeek 等服务不接收该非标准字段,因此仅对已知兼容入口设置。

+ * + * @param modelSpec 运行时模型声明 + * @param generationOptions 验证生成参数 + */ + private void disableOpenAiCompatibleThinking( + AgentModelSpec modelSpec, + AgentGenerationOptions generationOptions) { + AgentModelProviderType providerType = modelSpec.getProviderType(); + if (providerType == AgentModelProviderType.OPENAI_COMPATIBLE + || providerType == AgentModelProviderType.CUSTOM + || providerType == AgentModelProviderType.SILICONFLOW) { + generationOptions.getAdditionalBodyParams().put("enable_thinking", false); + } + Object sourceProviderType = modelSpec.getMetadata().get("sourceProviderType"); + if (sourceProviderType != null + && "gpustack".equalsIgnoreCase(String.valueOf(sourceProviderType))) { + // GPUStack 托管的 vLLM 模型通过 chat_template_kwargs 控制 Qwen 思考模式。 + generationOptions.getAdditionalBodyParams().put( + "chat_template_kwargs", + Map.of("enable_thinking", false)); + } + } + + /** + * 创建文字模型或 VLM 的最小验证消息。 + * + * @param supportImage 是否验证图片理解能力 + * @return 验证消息 + */ + private AgentMessage buildVerificationMessage(boolean supportImage) { + if (!supportImage) { + return AgentMessage.text( + AgentMessageRole.USER, + "请直接回复“你好”,不要补充其他内容。"); + } + List blocks = new ArrayList<>(); + blocks.add(new AgentTextBlock("请直接输出图片中的内容,不要补充其他内容。")); + AgentMediaBlock image = new AgentMediaBlock("image"); + image.setMimeType("image/png"); + image.setData(Base64.getEncoder().encodeToString(VlmVerificationImage.pngBytes())); + blocks.add(image); + AgentMessage message = new AgentMessage(); + message.setRole(AgentMessageRole.USER); + message.setContentBlocks(blocks); + return message; + } + + /** + * 聚合流式响应中的全部文本增量。 + * + * @param responses AgentScope 响应片段 + * @return 聚合后的文本 + */ + private String aggregateText(List responses) { + StringBuilder result = new StringBuilder(); + if (responses == null) { + return result.toString(); + } + for (ChatResponse response : responses) { + if (response == null || response.getContent() == null) { + continue; + } + for (ContentBlock block : response.getContent()) { + if (block instanceof TextBlock textBlock && textBlock.getText() != null) { + result.append(textBlock.getText()); + } + } + } + return result.toString(); + } + + /** + * 校验模型返回内容。 + * + * @param supportImage 是否执行 VLM 图片识别校验 + * @param responseText 聚合后的响应文本 + * @throws BusinessException 响应为空或图片识别结果不匹配时抛出 + */ + private void validateResponse(boolean supportImage, String responseText) { + if (responseText == null || responseText.isBlank()) { + throw new BusinessException("模型未返回有效内容"); + } + if (supportImage + && !normalizeVerificationText(responseText) + .contains(VlmVerificationImage.VERIFICATION_CODE)) { + LOG.warn("VLM verification response did not contain expected code, responseSummary={}", + responseSummary(responseText)); + throw new BusinessException("多模态校验未通过,模型未正确识别验证图片"); + } + } + + /** + * 归一化 VLM 对固定验证码的常见排版输出。 + * + *

模型可能将连续数字输出为 {@code 5 8 3 9} 或 {@code 5,8,3,9}, + * 这不影响图片识别结论,因此移除非字母数字字符后再校验。

+ * + * @param responseText 模型验证回复 + * @return 仅保留字母与数字的响应文本 + */ + private String normalizeVerificationText(String responseText) { + return responseText == null + ? "" + : responseText.replaceAll("[^\\p{L}\\p{N}]", ""); + } + + /** + * 生成固定验证回复的安全日志摘要。 + * + * @param responseText 模型验证回复 + * @return 移除控制字符且最长 160 字符的摘要 + */ + private String responseSummary(String responseText) { + String normalized = responseText == null + ? "" + : responseText.replaceAll("[\\p{Cntrl}]", " ").trim(); + return normalized.length() <= 160 + ? normalized + : normalized.substring(0, 160) + "..."; + } +} diff --git a/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRuntimeCompilerModelConfigTest.java b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRuntimeCompilerModelConfigTest.java new file mode 100644 index 00000000..d780901f --- /dev/null +++ b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentRuntimeCompilerModelConfigTest.java @@ -0,0 +1,130 @@ +package tech.easyflow.agent.runtime; + +import com.easyagents.agent.runtime.model.AgentGenerationOptions; +import com.easyagents.agent.runtime.model.AgentHttpVersionPolicy; +import com.easyagents.agent.runtime.model.AgentModelSpec; +import org.junit.Assert; +import org.junit.Test; +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.Method; +import java.math.BigInteger; +import java.util.Map; + +/** + * Agent 模型生成和 HTTP 传输配置编译测试。 + */ +public class AgentRuntimeCompilerModelConfigTest { + + /** + * 验证缺少 stream 时默认开启,显式关闭时保持关闭。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void generationStreamShouldDefaultToTrueAndAllowFalse() throws Exception { + AgentRuntimeCompiler compiler = new AgentRuntimeCompiler(); + + AgentGenerationOptions defaultOptions = invokeGenerationOptions(compiler, Map.of()); + AgentGenerationOptions disabledOptions = invokeGenerationOptions(compiler, Map.of("stream", false)); + + Assert.assertTrue(defaultOptions.getStream()); + Assert.assertFalse(disabledOptions.getStream()); + } + + /** + * 验证模型 options 中的 HTTP 策略会编译到中立模型声明。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void modelHttpPolicyShouldCompileFromOptions() throws Exception { + Model model = model(Map.of("agentHttpVersionPolicy", "HTTP_1_1")); + AgentRuntimeCompiler compiler = compiler(model); + + AgentModelSpec spec = invokeModelSpec(compiler); + + Assert.assertEquals(AgentHttpVersionPolicy.HTTP_1_1, spec.getHttpVersionPolicy()); + } + + /** + * 验证未知 HTTP 策略安全回退到 AUTO。 + * + * @throws Exception 反射调用失败时抛出 + */ + @Test + public void unknownModelHttpPolicyShouldFallbackToAuto() throws Exception { + Model model = model(Map.of("agentHttpVersionPolicy", "HTTP_3")); + AgentRuntimeCompiler compiler = compiler(model); + + AgentModelSpec spec = invokeModelSpec(compiler); + + Assert.assertEquals(AgentHttpVersionPolicy.AUTO, spec.getHttpVersionPolicy()); + } + + /** + * 创建已注入模型服务的编译器。 + * + * @param model 模型 + * @return 编译器 + * @throws Exception 注入失败时抛出 + */ + private AgentRuntimeCompiler compiler(Model model) throws Exception { + AgentRuntimeCompiler compiler = new AgentRuntimeCompiler(); + ModelService modelService = (ModelService) java.lang.reflect.Proxy.newProxyInstance( + ModelService.class.getClassLoader(), + new Class[]{ModelService.class}, + (proxy, method, args) -> "getModelInstance".equals(method.getName()) ? model : null); + Field field = AgentRuntimeCompiler.class.getDeclaredField("modelService"); + field.setAccessible(true); + field.set(compiler, modelService); + return compiler; + } + + /** + * 创建测试模型。 + * + * @param options 模型扩展配置 + * @return 测试模型 + */ + private Model model(Map options) { + Model model = new Model(); + model.setId(BigInteger.TEN); + model.setModelName("test-model"); + model.setOptions(options); + return model; + } + + /** + * 调用私有生成参数编译方法。 + * + * @param compiler 编译器 + * @param config 生成配置 + * @return 生成参数 + * @throws Exception 反射调用失败时抛出 + */ + private AgentGenerationOptions invokeGenerationOptions(AgentRuntimeCompiler compiler, + Map config) throws Exception { + Method method = AgentRuntimeCompiler.class.getDeclaredMethod("buildGenerationOptions", Map.class); + method.setAccessible(true); + return (AgentGenerationOptions) method.invoke(compiler, config); + } + + /** + * 调用私有模型声明编译方法。 + * + * @param compiler 编译器 + * @return 模型声明 + * @throws Exception 反射调用失败时抛出 + */ + private AgentModelSpec invokeModelSpec(AgentRuntimeCompiler compiler) throws Exception { + Agent agent = new Agent(); + agent.setModelId(BigInteger.TEN); + Method method = AgentRuntimeCompiler.class.getDeclaredMethod("buildModelSpec", Agent.class); + method.setAccessible(true); + return (AgentModelSpec) method.invoke(compiler, agent); + } +} diff --git a/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentScopeChatModelConnectivityVerifierTest.java b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentScopeChatModelConnectivityVerifierTest.java new file mode 100644 index 00000000..961f3f7b --- /dev/null +++ b/easyflow-modules/easyflow-module-agent/src/test/java/tech/easyflow/agent/runtime/AgentScopeChatModelConnectivityVerifierTest.java @@ -0,0 +1,305 @@ +package tech.easyflow.agent.runtime; + +import com.easyagents.agent.runtime.agentscope.AgentScopeMessageAdapter; +import com.easyagents.agent.runtime.model.AgentGenerationOptions; +import com.easyagents.agent.runtime.model.AgentModelFactory; +import com.easyagents.agent.runtime.model.AgentModelSpec; +import io.agentscope.core.message.Base64Source; +import io.agentscope.core.message.ImageBlock; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.TextBlock; +import io.agentscope.core.model.ChatResponse; +import io.agentscope.core.model.GenerateOptions; +import io.agentscope.core.model.ToolSchema; +import org.junit.Assert; +import org.junit.Test; +import reactor.core.publisher.Flux; +import tech.easyflow.ai.entity.Model; +import tech.easyflow.ai.entity.ModelProvider; +import tech.easyflow.ai.service.support.VlmVerificationImage; +import tech.easyflow.ai.service.verification.ChatModelVerificationResult; +import tech.easyflow.ai.service.verification.ModelVerificationStatus; +import tech.easyflow.common.web.exceptions.BusinessException; + +import java.math.BigInteger; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +/** + * AgentScope 双阶段模型连通性验证测试。 + */ +public class AgentScopeChatModelConnectivityVerifierTest { + + /** + * 验证非流式与流式阶段按顺序执行并返回通过状态。 + */ + @Test + public void shouldPassWhenBothPhasesReturnText() { + RecordingModelFactory factory = new RecordingModelFactory( + Flux.just(response("你好")), + Flux.just(response("你"), response("好"))); + AgentScopeChatModelConnectivityVerifier verifier = verifier(factory); + + ChatModelVerificationResult result = verifier.verify(model(false)); + + Assert.assertEquals(ModelVerificationStatus.PASSED, result.getStatus()); + Assert.assertEquals("HTTP_1_1", result.getEffectiveHttpVersion()); + Assert.assertEquals(List.of(false, true), factory.getFactoryStreams()); + Assert.assertEquals(List.of(false, true), factory.getRequestStreams()); + Assert.assertEquals(List.of(false, false), factory.getEnableThinkingValues()); + Assert.assertEquals(List.of(false, false), factory.getChatTemplateThinkingValues()); + } + + /** + * 验证基础连接通过而流式阶段失败时返回部分通过结果。 + */ + @Test + public void shouldReturnPartialWhenStreamingPhaseFails() { + RecordingModelFactory factory = new RecordingModelFactory( + Flux.just(response("你好")), + Flux.error(new IllegalStateException("stream failed"))); + + ChatModelVerificationResult result = verifier(factory).verify(model(false)); + + Assert.assertEquals(ModelVerificationStatus.PARTIAL, result.getStatus()); + Assert.assertEquals(ModelVerificationStatus.PASSED, result.getNonStreaming()); + Assert.assertEquals(ModelVerificationStatus.FAILED, result.getStreaming()); + Assert.assertTrue(result.getMessage().contains("流式响应不可用")); + } + + /** + * 验证基础连接失败时立即终止且返回业务失败。 + */ + @Test + public void shouldStopWhenNonStreamingPhaseFails() { + RecordingModelFactory factory = new RecordingModelFactory( + Flux.error(new IllegalStateException("connection failed"))); + + try { + verifier(factory).verify(model(false)); + Assert.fail("Expected base connectivity verification failure"); + } catch (BusinessException exception) { + Assert.assertTrue(exception.getMessage().contains("基础连接验证失败")); + Assert.assertFalse(exception.getMessage().contains("connection failed")); + } + Assert.assertEquals(List.of(false), factory.getFactoryStreams()); + } + + /** + * 验证流式阶段超时被归类为部分可用且不暴露底层异常。 + */ + @Test + public void shouldReturnPartialWhenStreamingPhaseTimesOut() { + RecordingModelFactory factory = new RecordingModelFactory( + Flux.just(response("你好")), + Flux.never()); + AgentScopeChatModelConnectivityVerifier verifier = new AgentScopeChatModelConnectivityVerifier( + factory, + new AgentScopeMessageAdapter(), + Duration.ofMillis(20)); + + ChatModelVerificationResult result = verifier.verify(model(false)); + + Assert.assertEquals(ModelVerificationStatus.PARTIAL, result.getStatus()); + Assert.assertEquals("连接成功,流式响应不可用,可关闭智能体的模型流式响应。", + result.getMessage()); + } + + /** + * 验证 VLM 使用 Base64 图片,并能聚合流式文本增量。 + */ + @Test + public void shouldVerifyVlmImageAndAggregateStreamingChunks() { + RecordingModelFactory factory = new RecordingModelFactory( + Flux.just(response("图片中的内容是“" + VlmVerificationImage.VERIFICATION_CODE + "”。")), + Flux.just(response("识别结果:58"), response("39"))); + + ChatModelVerificationResult result = verifier(factory).verify(model(true)); + + Assert.assertEquals(ModelVerificationStatus.PASSED, result.getStatus()); + Msg message = factory.getMessages().get(0); + ImageBlock image = message.getContent().stream() + .filter(ImageBlock.class::isInstance) + .map(ImageBlock.class::cast) + .findFirst() + .orElseThrow(); + Assert.assertTrue(image.getSource() instanceof Base64Source); + byte[] imageBytes = Base64.getDecoder().decode(((Base64Source) image.getSource()).getData()); + Assert.assertEquals((byte) 0x89, imageBytes[0]); + Assert.assertEquals((byte) 0x50, imageBytes[1]); + } + + /** + * 创建待验证模型。 + * + * @param supportImage 是否支持图片 + * @return 测试模型 + */ + private Model model(boolean supportImage) { + Model model = new Model(); + model.setId(BigInteger.TEN); + model.setModelName("test-model"); + model.setEndpoint("http://model.example.com"); + model.setRequestPath("/v1/chat/completions"); + model.setApiKey("test-key"); + model.setSupportImage(supportImage); + ModelProvider provider = new ModelProvider(); + provider.setProviderType("gpustack"); + model.setModelProvider(provider); + return model; + } + + /** + * 创建使用测试工厂的验证器。 + * + * @param factory 记录型模型工厂 + * @return 验证器 + */ + private AgentScopeChatModelConnectivityVerifier verifier(RecordingModelFactory factory) { + return new AgentScopeChatModelConnectivityVerifier( + factory, + new AgentScopeMessageAdapter(), + Duration.ofSeconds(2)); + } + + /** + * 创建单个文本响应片段。 + * + * @param text 文本内容 + * @return AgentScope 响应 + */ + private ChatResponse response(String text) { + return ChatResponse.builder() + .content(List.of(TextBlock.builder().text(text).build())) + .build(); + } + + /** + * 按阶段返回预设响应并记录调用参数的模型工厂。 + */ + private static final class RecordingModelFactory + implements AgentModelFactory { + + /** 每个阶段的预设响应。 */ + private final List> phaseResponses; + /** 模型工厂收到的流式参数。 */ + private final List factoryStreams = new ArrayList<>(); + /** 模型请求收到的流式参数。 */ + private final List requestStreams = new ArrayList<>(); + /** 模型请求收到的消息。 */ + private final List messages = new ArrayList<>(); + /** OpenAI-compatible 请求中的思考开关。 */ + private final List enableThinkingValues = new ArrayList<>(); + /** GPUStack 模板参数中的思考开关。 */ + private final List chatTemplateThinkingValues = new ArrayList<>(); + + /** + * 创建记录型模型工厂。 + * + * @param phaseResponses 每个阶段的预设响应 + */ + @SafeVarargs + private RecordingModelFactory(Flux... phaseResponses) { + this.phaseResponses = List.of(phaseResponses); + } + + /** + * 创建当前验证阶段的测试模型。 + * + * @param modelSpec 模型声明 + * @param generationOptions 生成参数 + * @return 测试模型 + */ + @Override + public io.agentscope.core.model.Model create( + AgentModelSpec modelSpec, + AgentGenerationOptions generationOptions) { + int phaseIndex = factoryStreams.size(); + factoryStreams.add(Boolean.TRUE.equals(generationOptions.getStream())); + enableThinkingValues.add( + generationOptions.getAdditionalBodyParams().get("enable_thinking")); + Object templateOptions = generationOptions.getAdditionalBodyParams() + .get("chat_template_kwargs"); + chatTemplateThinkingValues.add(templateOptions instanceof java.util.Map map + ? map.get("enable_thinking") + : null); + Flux responses = phaseResponses.get(phaseIndex); + return new io.agentscope.core.model.Model() { + /** + * 返回预设响应并记录真实请求参数。 + * + * @param inputMessages 模型消息 + * @param tools 工具声明 + * @param options 生成参数 + * @return 预设响应 + */ + @Override + public Flux stream( + List inputMessages, + List tools, + GenerateOptions options) { + requestStreams.add(Boolean.TRUE.equals(options.getStream())); + messages.add(inputMessages.get(0)); + return responses; + } + + /** + * 返回测试模型名称。 + * + * @return 测试模型名称 + */ + @Override + public String getModelName() { + return modelSpec.getModelName(); + } + }; + } + + /** + * 获取工厂流式参数记录。 + * + * @return 流式参数列表 + */ + private List getFactoryStreams() { + return factoryStreams; + } + + /** + * 获取请求流式参数记录。 + * + * @return 流式参数列表 + */ + private List getRequestStreams() { + return requestStreams; + } + + /** + * 获取请求消息记录。 + * + * @return 消息列表 + */ + private List getMessages() { + return messages; + } + + /** + * 获取 OpenAI-compatible 请求中的思考开关。 + * + * @return 各阶段思考开关 + */ + private List getEnableThinkingValues() { + return enableThinkingValues; + } + + /** + * 获取 GPUStack 模板参数中的思考开关。 + * + * @return 各阶段模板思考开关 + */ + private List getChatTemplateThinkingValues() { + return chatTemplateThinkingValues; + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java index 60a4a8ca..0bf5ff9d 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/ModelServiceImpl.java @@ -4,8 +4,6 @@ package tech.easyflow.ai.service.impl; import cn.hutool.core.util.StrUtil; import com.alicp.jetcache.Cache; import com.easyagents.core.document.Document; -import com.easyagents.core.model.chat.ChatModel; -import com.easyagents.core.model.chat.ChatOptions; import com.easyagents.core.model.embedding.EmbeddingModel; import com.easyagents.core.model.rerank.RerankModel; import com.easyagents.core.store.VectorData; @@ -23,6 +21,7 @@ import tech.easyflow.ai.entity.ModelProvider; import tech.easyflow.ai.mapper.ModelMapper; import tech.easyflow.ai.service.ModelProviderService; import tech.easyflow.ai.service.ModelService; +import tech.easyflow.ai.service.verification.ChatModelConnectivityVerifier; import tech.easyflow.common.tree.Tree; import tech.easyflow.common.util.SqlOperatorsUtil; import tech.easyflow.common.util.SqlUtil; @@ -51,6 +50,10 @@ public class ModelServiceImpl extends ServiceImpl implements @Resource private Cache cache; + /** 与智能体运行时同链路的 Chat Model 连通性验证器。 */ + @Autowired(required = false) + private ChatModelConnectivityVerifier chatModelConnectivityVerifier; + @Override public boolean addAiLlm(Model entity) { @@ -69,8 +72,7 @@ public class ModelServiceImpl extends ServiceImpl implements Map resMap = new HashMap<>(); // 走聊天验证逻辑 if (Model.MODEL_TYPES[0].equals(modelType)) { - verifyChatLlm(model); - return null; + return verifyChatLlm(model); } // 走向量化验证逻辑 if (Model.MODEL_TYPES[1].equals(modelType)) { @@ -154,25 +156,17 @@ public class ModelServiceImpl extends ServiceImpl implements } } - private void verifyChatLlm(Model llm) { - - ChatModel chatModel = llm.toChatModel(); - if (chatModel == null) { - throw new BusinessException("chatModel为空"); + /** + * 使用智能体同链路验证 Chat Model 或 VLM。 + * + * @param model 已补齐供应商默认配置的模型 + * @return 结构化双阶段验证结果 + */ + private Map verifyChatLlm(Model model) { + if (chatModelConnectivityVerifier == null) { + throw new BusinessException("Agent 模型连通性验证组件未加载"); } - try { - ChatOptions options=new ChatOptions(); - options.setThinkingEnabled(false); - String response = chatModel.chat("我在对模型配置进行校验,你收到这条消息无需做任何思考,直接回复一个“你好”即可!",options); - if (response == null) { - throw new BusinessException("校验未通过,请前往后端日志查看详情!"); - } - log.info("校验结果:{}", response); - } catch (Exception e) { - log.error("校验失败:{}", e.getMessage()); - throw new BusinessException(e.getMessage()); - } - + return chatModelConnectivityVerifier.verify(model).toMap(); } @Override diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/support/VlmVerificationImage.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/support/VlmVerificationImage.java new file mode 100644 index 00000000..5d2f0ebc --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/support/VlmVerificationImage.java @@ -0,0 +1,90 @@ +package tech.easyflow.ai.service.support; + +import tech.easyflow.common.web.exceptions.BusinessException; + +import java.io.IOException; +import java.io.InputStream; + +/** + * 提供模型连接验证专用的固定 PNG 图片。 + */ +public final class VlmVerificationImage { + + /** 模型需要识别并返回的固定数字。 */ + public static final String VERIFICATION_CODE = "5839"; + + /** 固定验证图片的 classpath 路径。 */ + private static final String RESOURCE_PATH = "/images/vlm-verification.png"; + /** PNG 文件签名字节。 */ + private static final byte[] PNG_SIGNATURE = { + (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A + }; + /** 启动后复用的固定 PNG 字节。 */ + private static final byte[] PNG_BYTES = loadPng(); + + /** + * 禁止实例化工具类。 + */ + private VlmVerificationImage() { + } + + /** + * 返回固定连接验证 PNG 的字节副本。 + * + * @return PNG 字节副本 + */ + public static byte[] pngBytes() { + return PNG_BYTES.clone(); + } + + /** + * 从 classpath 加载固定验证图片。 + * + * @return PNG 文件字节 + * @throws BusinessException 资源缺失、读取失败或文件格式非法时抛出 + */ + private static byte[] loadPng() { + try (InputStream input = VlmVerificationImage.class.getResourceAsStream(RESOURCE_PATH)) { + if (input == null) { + throw new BusinessException("VLM 校验图片资源不存在:" + RESOURCE_PATH); + } + byte[] bytes = input.readAllBytes(); + if (!hasPngSignature(bytes)) { + throw new BusinessException("VLM 校验图片资源不是有效的 PNG 文件"); + } + return bytes; + } catch (IOException error) { + throw new BusinessException("VLM 校验图片读取失败:" + safeMessage(error)); + } + } + + /** + * 检查文件是否包含标准 PNG 签名。 + * + * @param bytes 待检查文件字节 + * @return 包含完整 PNG 签名时返回 true + */ + private static boolean hasPngSignature(byte[] bytes) { + if (bytes == null || bytes.length < PNG_SIGNATURE.length) { + return false; + } + for (int index = 0; index < PNG_SIGNATURE.length; index++) { + if (bytes[index] != PNG_SIGNATURE[index]) { + return false; + } + } + return true; + } + + /** + * 获取非空异常描述。 + * + * @param error 原始异常 + * @return 可展示的异常描述 + */ + private static String safeMessage(Exception error) { + return error.getMessage() == null || error.getMessage().isBlank() + ? "未知错误" + : error.getMessage(); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ChatModelConnectivityVerifier.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ChatModelConnectivityVerifier.java new file mode 100644 index 00000000..aed7b850 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ChatModelConnectivityVerifier.java @@ -0,0 +1,17 @@ +package tech.easyflow.ai.service.verification; + +import tech.easyflow.ai.entity.Model; + +/** + * Chat Model 与 VLM 的运行时同链路连通性验证器。 + */ +public interface ChatModelConnectivityVerifier { + + /** + * 验证模型的非流式基础连接与流式响应能力。 + * + * @param model 已补齐供应商默认配置的模型 + * @return 双阶段验证结果 + */ + ChatModelVerificationResult verify(Model model); +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ChatModelVerificationResult.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ChatModelVerificationResult.java new file mode 100644 index 00000000..16e4b324 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ChatModelVerificationResult.java @@ -0,0 +1,132 @@ +package tech.easyflow.ai.service.verification; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Chat Model 与 VLM 的双阶段连通性验证结果。 + */ +public final class ChatModelVerificationResult { + + /** 整体验证状态。 */ + private final ModelVerificationStatus status; + /** 非流式基础连接验证状态。 */ + private final ModelVerificationStatus nonStreaming; + /** 流式响应验证状态。 */ + private final ModelVerificationStatus streaming; + /** 实际生效的 HTTP 版本策略。 */ + private final String effectiveHttpVersion; + /** 用户可见的简洁结果说明。 */ + private final String message; + + /** + * 创建验证结果。 + * + * @param status 整体验证状态 + * @param nonStreaming 非流式验证状态 + * @param streaming 流式验证状态 + * @param effectiveHttpVersion 实际生效的 HTTP 版本策略 + * @param message 用户可见结果说明 + */ + private ChatModelVerificationResult(ModelVerificationStatus status, + ModelVerificationStatus nonStreaming, + ModelVerificationStatus streaming, + String effectiveHttpVersion, + String message) { + this.status = status; + this.nonStreaming = nonStreaming; + this.streaming = streaming; + this.effectiveHttpVersion = effectiveHttpVersion; + this.message = message; + } + + /** + * 创建双阶段全部通过的结果。 + * + * @param effectiveHttpVersion 实际生效的 HTTP 版本策略 + * @return 全部通过结果 + */ + public static ChatModelVerificationResult passed(String effectiveHttpVersion) { + return new ChatModelVerificationResult( + ModelVerificationStatus.PASSED, + ModelVerificationStatus.PASSED, + ModelVerificationStatus.PASSED, + effectiveHttpVersion, + "验证成功"); + } + + /** + * 创建基础连接通过但流式阶段失败的结果。 + * + * @param effectiveHttpVersion 实际生效的 HTTP 版本策略 + * @return 部分通过结果 + */ + public static ChatModelVerificationResult streamingUnavailable(String effectiveHttpVersion) { + return new ChatModelVerificationResult( + ModelVerificationStatus.PARTIAL, + ModelVerificationStatus.PASSED, + ModelVerificationStatus.FAILED, + effectiveHttpVersion, + "连接成功,流式响应不可用,可关闭智能体的模型流式响应。"); + } + + /** + * 获取整体验证状态。 + * + * @return 整体验证状态 + */ + public ModelVerificationStatus getStatus() { + return status; + } + + /** + * 获取非流式基础连接验证状态。 + * + * @return 非流式验证状态 + */ + public ModelVerificationStatus getNonStreaming() { + return nonStreaming; + } + + /** + * 获取流式响应验证状态。 + * + * @return 流式验证状态 + */ + public ModelVerificationStatus getStreaming() { + return streaming; + } + + /** + * 获取实际生效的 HTTP 版本策略。 + * + * @return HTTP 版本策略名称 + */ + public String getEffectiveHttpVersion() { + return effectiveHttpVersion; + } + + /** + * 获取用户可见结果说明。 + * + * @return 结果说明 + */ + public String getMessage() { + return message; + } + + /** + * 转换为现有模型验证接口使用的响应结构。 + * + * @return 有序响应字段 + */ + public Map toMap() { + Map result = new LinkedHashMap<>(); + result.put("status", status.name()); + result.put("nonStreaming", nonStreaming.name()); + result.put("streaming", streaming.name()); + result.put("effectiveHttpVersion", effectiveHttpVersion); + result.put("message", message); + return result; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ModelVerificationStatus.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ModelVerificationStatus.java new file mode 100644 index 00000000..801b96fd --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/verification/ModelVerificationStatus.java @@ -0,0 +1,15 @@ +package tech.easyflow.ai.service.verification; + +/** + * 模型连通性验证状态。 + */ +public enum ModelVerificationStatus { + /** 所有要求的验证阶段均通过。 */ + PASSED, + /** 基础连接通过,但增强能力验证未通过。 */ + PARTIAL, + /** 验证失败。 */ + FAILED, + /** 前置阶段失败,当前阶段未执行。 */ + SKIPPED +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/resources/images/vlm-verification.png b/easyflow-modules/easyflow-module-ai/src/main/resources/images/vlm-verification.png new file mode 100644 index 00000000..b3aeb69b Binary files /dev/null and b/easyflow-modules/easyflow-module-ai/src/main/resources/images/vlm-verification.png differ diff --git a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/service/support/VlmVerificationImageTest.java b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/service/support/VlmVerificationImageTest.java new file mode 100644 index 00000000..cf0d0425 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/service/support/VlmVerificationImageTest.java @@ -0,0 +1,32 @@ +package tech.easyflow.ai.service.support; + +import org.junit.Assert; +import org.junit.Test; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; + +/** + * VLM 连接验证专用 PNG 测试。 + */ +public class VlmVerificationImageTest { + + /** + * 验证固定资源图片可重复读取并能被标准 PNG 解码器解析。 + * + * @throws Exception 图片解码失败时抛出 + */ + @Test + public void pngBytesShouldReturnDeterministicReadableImage() throws Exception { + byte[] first = VlmVerificationImage.pngBytes(); + byte[] second = VlmVerificationImage.pngBytes(); + BufferedImage image = ImageIO.read(new ByteArrayInputStream(first)); + + Assert.assertArrayEquals(first, second); + Assert.assertNotNull(image); + Assert.assertEquals(480, image.getWidth()); + Assert.assertEquals(180, image.getHeight()); + Assert.assertEquals("5839", VlmVerificationImage.VERIFICATION_CODE); + } +} diff --git a/easyflow-ui-admin/app/src/api/ai/llm.ts b/easyflow-ui-admin/app/src/api/ai/llm.ts index 90675338..f73d5298 100644 --- a/easyflow-ui-admin/app/src/api/ai/llm.ts +++ b/easyflow-ui-admin/app/src/api/ai/llm.ts @@ -37,6 +37,21 @@ export async function verifyModelConfig(id: string) { return api.get('/api/v1/model/verifyLlmConfig', { params: { id } }); } +export type ModelVerificationStageStatus = + | 'FAILED' + | 'PARTIAL' + | 'PASSED' + | 'SKIPPED'; + +export interface ModelVerificationData { + dimension?: number; + effectiveHttpVersion?: string; + message?: string; + nonStreaming?: ModelVerificationStageStatus; + status?: ModelVerificationStageStatus; + streaming?: ModelVerificationStageStatus; +} + export interface ModelInvokeConfigPayload { id: string; invokeCode?: string; diff --git a/easyflow-ui-admin/app/src/views/ai/agents/components/AgentBaseForm.vue b/easyflow-ui-admin/app/src/views/ai/agents/components/AgentBaseForm.vue index 3bc4cdb4..9e0f5c93 100644 --- a/easyflow-ui-admin/app/src/views/ai/agents/components/AgentBaseForm.vue +++ b/easyflow-ui-admin/app/src/views/ai/agents/components/AgentBaseForm.vue @@ -1,8 +1,8 @@