feat: 增强智能体模型调用兼容能力
- 增加模型流式开关和 HTTP 传输策略配置 - 使用 AgentScope 执行基础连接、流式与 VLM 双阶段验证 - 固定多模态校验图片并统一验证状态展示
This commit is contained in:
@@ -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<String, Object> overrides) {
|
||||||
|
if (model == null) {
|
||||||
|
throw new IllegalArgumentException("模型配置不能为空");
|
||||||
|
}
|
||||||
|
Map<String, Object> 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<String, Object> values, String key, String defaultValue) {
|
||||||
|
Object value = values.get(key);
|
||||||
|
return value == null ? defaultValue : String.valueOf(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@ import com.easyagents.agent.runtime.memory.AgentMemoryType;
|
|||||||
import com.easyagents.agent.runtime.mcp.McpSpec;
|
import com.easyagents.agent.runtime.mcp.McpSpec;
|
||||||
import com.easyagents.agent.runtime.mcp.McpTransportType;
|
import com.easyagents.agent.runtime.mcp.McpTransportType;
|
||||||
import com.easyagents.agent.runtime.model.AgentGenerationOptions;
|
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.model.AgentModelSpec;
|
||||||
import com.easyagents.agent.runtime.tool.AgentToolCategory;
|
import com.easyagents.agent.runtime.tool.AgentToolCategory;
|
||||||
import com.easyagents.agent.runtime.tool.AgentToolResult;
|
import com.easyagents.agent.runtime.tool.AgentToolResult;
|
||||||
@@ -104,18 +103,7 @@ public class AgentRuntimeCompiler {
|
|||||||
if (model == null) {
|
if (model == null) {
|
||||||
throw new BusinessException("Agent 模型不存在");
|
throw new BusinessException("Agent 模型不存在");
|
||||||
}
|
}
|
||||||
Map<String, Object> config = agent.getModelConfigJson();
|
return AgentModelSpecMapper.fromModel(model, 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private AgentGenerationOptions buildGenerationOptions(Map<String, Object> config) {
|
private AgentGenerationOptions buildGenerationOptions(Map<String, Object> config) {
|
||||||
@@ -756,17 +744,6 @@ public class AgentRuntimeCompiler {
|
|||||||
return description == null ? "" : description;
|
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<String, Object> map, String key) {
|
private AgentMemoryType memoryTypeValue(Map<String, Object> map, String key) {
|
||||||
String value = stringValue(map, key, AgentMemoryType.AUTO_CONTEXT.name());
|
String value = stringValue(map, key, AgentMemoryType.AUTO_CONTEXT.name());
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -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<io.agentscope.core.model.Model> 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<io.agentscope.core.model.Model> 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<ChatResponse> 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 服务显式关闭思考。
|
||||||
|
*
|
||||||
|
* <p>AgentScope 的通用 OpenAI Formatter 不会读取中立的
|
||||||
|
* {@code thinkingEnabled} 字段,需通过扩展请求体传递;原生 OpenAI、
|
||||||
|
* DeepSeek 等服务不接收该非标准字段,因此仅对已知兼容入口设置。</p>
|
||||||
|
*
|
||||||
|
* @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<AgentContentBlock> 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<ChatResponse> 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 对固定验证码的常见排版输出。
|
||||||
|
*
|
||||||
|
* <p>模型可能将连续数字输出为 {@code 5 8 3 9} 或 {@code 5,8,3,9},
|
||||||
|
* 这不影响图片识别结论,因此移除非字母数字字符后再校验。</p>
|
||||||
|
*
|
||||||
|
* @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) + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, Object> 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<String, Object> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<io.agentscope.core.model.Model> {
|
||||||
|
|
||||||
|
/** 每个阶段的预设响应。 */
|
||||||
|
private final List<Flux<ChatResponse>> phaseResponses;
|
||||||
|
/** 模型工厂收到的流式参数。 */
|
||||||
|
private final List<Boolean> factoryStreams = new ArrayList<>();
|
||||||
|
/** 模型请求收到的流式参数。 */
|
||||||
|
private final List<Boolean> requestStreams = new ArrayList<>();
|
||||||
|
/** 模型请求收到的消息。 */
|
||||||
|
private final List<Msg> messages = new ArrayList<>();
|
||||||
|
/** OpenAI-compatible 请求中的思考开关。 */
|
||||||
|
private final List<Object> enableThinkingValues = new ArrayList<>();
|
||||||
|
/** GPUStack 模板参数中的思考开关。 */
|
||||||
|
private final List<Object> chatTemplateThinkingValues = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建记录型模型工厂。
|
||||||
|
*
|
||||||
|
* @param phaseResponses 每个阶段的预设响应
|
||||||
|
*/
|
||||||
|
@SafeVarargs
|
||||||
|
private RecordingModelFactory(Flux<ChatResponse>... 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<ChatResponse> responses = phaseResponses.get(phaseIndex);
|
||||||
|
return new io.agentscope.core.model.Model() {
|
||||||
|
/**
|
||||||
|
* 返回预设响应并记录真实请求参数。
|
||||||
|
*
|
||||||
|
* @param inputMessages 模型消息
|
||||||
|
* @param tools 工具声明
|
||||||
|
* @param options 生成参数
|
||||||
|
* @return 预设响应
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Flux<ChatResponse> stream(
|
||||||
|
List<Msg> inputMessages,
|
||||||
|
List<ToolSchema> 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<Boolean> getFactoryStreams() {
|
||||||
|
return factoryStreams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求流式参数记录。
|
||||||
|
*
|
||||||
|
* @return 流式参数列表
|
||||||
|
*/
|
||||||
|
private List<Boolean> getRequestStreams() {
|
||||||
|
return requestStreams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求消息记录。
|
||||||
|
*
|
||||||
|
* @return 消息列表
|
||||||
|
*/
|
||||||
|
private List<Msg> getMessages() {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 OpenAI-compatible 请求中的思考开关。
|
||||||
|
*
|
||||||
|
* @return 各阶段思考开关
|
||||||
|
*/
|
||||||
|
private List<Object> getEnableThinkingValues() {
|
||||||
|
return enableThinkingValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 GPUStack 模板参数中的思考开关。
|
||||||
|
*
|
||||||
|
* @return 各阶段模板思考开关
|
||||||
|
*/
|
||||||
|
private List<Object> getChatTemplateThinkingValues() {
|
||||||
|
return chatTemplateThinkingValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,6 @@ package tech.easyflow.ai.service.impl;
|
|||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.alicp.jetcache.Cache;
|
import com.alicp.jetcache.Cache;
|
||||||
import com.easyagents.core.document.Document;
|
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.embedding.EmbeddingModel;
|
||||||
import com.easyagents.core.model.rerank.RerankModel;
|
import com.easyagents.core.model.rerank.RerankModel;
|
||||||
import com.easyagents.core.store.VectorData;
|
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.mapper.ModelMapper;
|
||||||
import tech.easyflow.ai.service.ModelProviderService;
|
import tech.easyflow.ai.service.ModelProviderService;
|
||||||
import tech.easyflow.ai.service.ModelService;
|
import tech.easyflow.ai.service.ModelService;
|
||||||
|
import tech.easyflow.ai.service.verification.ChatModelConnectivityVerifier;
|
||||||
import tech.easyflow.common.tree.Tree;
|
import tech.easyflow.common.tree.Tree;
|
||||||
import tech.easyflow.common.util.SqlOperatorsUtil;
|
import tech.easyflow.common.util.SqlOperatorsUtil;
|
||||||
import tech.easyflow.common.util.SqlUtil;
|
import tech.easyflow.common.util.SqlUtil;
|
||||||
@@ -51,6 +50,10 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
|
|||||||
@Resource
|
@Resource
|
||||||
private Cache<String, Object> cache;
|
private Cache<String, Object> cache;
|
||||||
|
|
||||||
|
/** 与智能体运行时同链路的 Chat Model 连通性验证器。 */
|
||||||
|
@Autowired(required = false)
|
||||||
|
private ChatModelConnectivityVerifier chatModelConnectivityVerifier;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean addAiLlm(Model entity) {
|
public boolean addAiLlm(Model entity) {
|
||||||
@@ -69,8 +72,7 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
|
|||||||
Map<String, Object> resMap = new HashMap<>();
|
Map<String, Object> resMap = new HashMap<>();
|
||||||
// 走聊天验证逻辑
|
// 走聊天验证逻辑
|
||||||
if (Model.MODEL_TYPES[0].equals(modelType)) {
|
if (Model.MODEL_TYPES[0].equals(modelType)) {
|
||||||
verifyChatLlm(model);
|
return verifyChatLlm(model);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
// 走向量化验证逻辑
|
// 走向量化验证逻辑
|
||||||
if (Model.MODEL_TYPES[1].equals(modelType)) {
|
if (Model.MODEL_TYPES[1].equals(modelType)) {
|
||||||
@@ -154,25 +156,17 @@ public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verifyChatLlm(Model llm) {
|
/**
|
||||||
|
* 使用智能体同链路验证 Chat Model 或 VLM。
|
||||||
ChatModel chatModel = llm.toChatModel();
|
*
|
||||||
if (chatModel == null) {
|
* @param model 已补齐供应商默认配置的模型
|
||||||
throw new BusinessException("chatModel为空");
|
* @return 结构化双阶段验证结果
|
||||||
|
*/
|
||||||
|
private Map<String, Object> verifyChatLlm(Model model) {
|
||||||
|
if (chatModelConnectivityVerifier == null) {
|
||||||
|
throw new BusinessException("Agent 模型连通性验证组件未加载");
|
||||||
}
|
}
|
||||||
try {
|
return chatModelConnectivityVerifier.verify(model).toMap();
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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<String, Object> toMap() {
|
||||||
|
Map<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package tech.easyflow.ai.service.verification;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模型连通性验证状态。
|
||||||
|
*/
|
||||||
|
public enum ModelVerificationStatus {
|
||||||
|
/** 所有要求的验证阶段均通过。 */
|
||||||
|
PASSED,
|
||||||
|
/** 基础连接通过,但增强能力验证未通过。 */
|
||||||
|
PARTIAL,
|
||||||
|
/** 验证失败。 */
|
||||||
|
FAILED,
|
||||||
|
/** 前置阶段失败,当前阶段未执行。 */
|
||||||
|
SKIPPED
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,21 @@ export async function verifyModelConfig(id: string) {
|
|||||||
return api.get('/api/v1/model/verifyLlmConfig', { params: { id } });
|
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 {
|
export interface ModelInvokeConfigPayload {
|
||||||
id: string;
|
id: string;
|
||||||
invokeCode?: string;
|
invokeCode?: string;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/* eslint-disable vue/no-mutating-props */
|
/* eslint-disable vue/no-mutating-props */
|
||||||
import type {AgentInfo, AgentOption} from '../types';
|
import type { AgentInfo, AgentOption } from '../types';
|
||||||
|
|
||||||
import {InfoFilled} from '@element-plus/icons-vue';
|
import { InfoFilled } from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElForm,
|
ElForm,
|
||||||
ElFormItem,
|
ElFormItem,
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
ElInputNumber,
|
ElInputNumber,
|
||||||
ElOption,
|
ElOption,
|
||||||
ElSelect,
|
ElSelect,
|
||||||
|
ElSwitch,
|
||||||
ElTooltip,
|
ElTooltip,
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
@@ -75,6 +76,27 @@ const emit = defineEmits<{ change: [] }>();
|
|||||||
/>
|
/>
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
<ElFormItem>
|
||||||
|
<template #label>
|
||||||
|
<span class="agent-form__label">
|
||||||
|
模型流式响应
|
||||||
|
<ElTooltip
|
||||||
|
content="关闭后,模型会在生成完成后一次性返回回答。"
|
||||||
|
effect="light"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<ElIcon class="agent-form__info" aria-label="模型流式响应说明">
|
||||||
|
<InfoFilled />
|
||||||
|
</ElIcon>
|
||||||
|
</ElTooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<ElSwitch
|
||||||
|
v-model="agent.generationConfigJson!.stream"
|
||||||
|
aria-label="模型流式响应"
|
||||||
|
@change="emit('change')"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
<ElFormItem label="系统提示词">
|
<ElFormItem label="系统提示词">
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model="agent.promptConfigJson!.systemPrompt"
|
v-model="agent.promptConfigJson!.systemPrompt"
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createEmptyAgent,
|
||||||
|
useAgentDesignerState,
|
||||||
|
} from './useAgentDesignerState';
|
||||||
|
|
||||||
|
describe('useAgentDesignerState generation stream', () => {
|
||||||
|
it('defaults new and legacy agents to streaming', () => {
|
||||||
|
expect(createEmptyAgent().generationConfigJson?.stream).toBe(true);
|
||||||
|
|
||||||
|
const designer = useAgentDesignerState();
|
||||||
|
designer.reset({ name: '旧智能体' });
|
||||||
|
|
||||||
|
expect(designer.state.agent.generationConfigJson?.stream).toBe(true);
|
||||||
|
expect(designer.buildPayloadAgent().generationConfigJson?.stream).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves an explicitly disabled stream value in the payload', () => {
|
||||||
|
const designer = useAgentDesignerState();
|
||||||
|
designer.reset({
|
||||||
|
name: '非流式智能体',
|
||||||
|
generationConfigJson: { stream: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(designer.state.agent.generationConfigJson?.stream).toBe(false);
|
||||||
|
expect(designer.buildPayloadAgent().generationConfigJson?.stream).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -84,6 +84,7 @@ export function createEmptyAgent(): AgentInfo {
|
|||||||
categoryId: '',
|
categoryId: '',
|
||||||
modelId: '',
|
modelId: '',
|
||||||
promptConfigJson: { systemPrompt: '' },
|
promptConfigJson: { systemPrompt: '' },
|
||||||
|
generationConfigJson: { stream: true },
|
||||||
memoryConfigJson: {
|
memoryConfigJson: {
|
||||||
compressionParameter: {
|
compressionParameter: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -111,6 +112,10 @@ function normalizeAgent(agent?: AgentInfo): AgentInfo {
|
|||||||
systemPrompt: '',
|
systemPrompt: '',
|
||||||
...source.promptConfigJson,
|
...source.promptConfigJson,
|
||||||
},
|
},
|
||||||
|
generationConfigJson: {
|
||||||
|
...source.generationConfigJson,
|
||||||
|
stream: source.generationConfigJson?.stream !== false,
|
||||||
|
},
|
||||||
memoryConfigJson: {
|
memoryConfigJson: {
|
||||||
...memoryConfig,
|
...memoryConfig,
|
||||||
compressionParameter: {
|
compressionParameter: {
|
||||||
@@ -356,6 +361,10 @@ export function useAgentDesignerState() {
|
|||||||
interactionConfigJson: buildInteractionConfigPayload(
|
interactionConfigJson: buildInteractionConfigPayload(
|
||||||
state.agent.interactionConfigJson,
|
state.agent.interactionConfigJson,
|
||||||
),
|
),
|
||||||
|
generationConfigJson: {
|
||||||
|
...state.agent.generationConfigJson,
|
||||||
|
stream: state.agent.generationConfigJson?.stream !== false,
|
||||||
|
},
|
||||||
memoryConfigJson: {
|
memoryConfigJson: {
|
||||||
...restMemoryConfigJson,
|
...restMemoryConfigJson,
|
||||||
compressionParameter: {
|
compressionParameter: {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { llmType } from '#/api';
|
import type { llmType } from '#/api';
|
||||||
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
|
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
|
||||||
|
import type { VerifyButtonStatus } from '#/views/ai/model/modelUtils/model-verification';
|
||||||
|
|
||||||
import { computed, onMounted, reactive, ref } from 'vue';
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
Edit,
|
Edit,
|
||||||
Loading,
|
Loading,
|
||||||
Select,
|
Select,
|
||||||
|
Warning,
|
||||||
} from '@element-plus/icons-vue';
|
} from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
ElButton,
|
||||||
@@ -31,6 +33,10 @@ import { $t } from '#/locales';
|
|||||||
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
|
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
|
||||||
import { getDefaultModelAbility } from '#/views/ai/model/modelUtils/model-ability';
|
import { getDefaultModelAbility } from '#/views/ai/model/modelUtils/model-ability';
|
||||||
import { mapLlmToModelAbility } from '#/views/ai/model/modelUtils/model-ability-utils';
|
import { mapLlmToModelAbility } from '#/views/ai/model/modelUtils/model-ability-utils';
|
||||||
|
import {
|
||||||
|
getVerifyButtonText as getStatusButtonText,
|
||||||
|
resolveModelVerificationFeedback,
|
||||||
|
} from '#/views/ai/model/modelUtils/model-verification';
|
||||||
|
|
||||||
interface ProviderOption {
|
interface ProviderOption {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -63,7 +69,6 @@ const isActionLoading = ref(false);
|
|||||||
const modelRows = ref<llmType[]>([]);
|
const modelRows = ref<llmType[]>([]);
|
||||||
const selectedRows = ref<llmType[]>([]);
|
const selectedRows = ref<llmType[]>([]);
|
||||||
const lastErrorMessage = ref('');
|
const lastErrorMessage = ref('');
|
||||||
type VerifyButtonStatus = 'error' | 'idle' | 'loading' | 'success';
|
|
||||||
const verifyStatusMap = ref<Record<string, VerifyButtonStatus>>({});
|
const verifyStatusMap = ref<Record<string, VerifyButtonStatus>>({});
|
||||||
|
|
||||||
const filterState = reactive<FilterState>({
|
const filterState = reactive<FilterState>({
|
||||||
@@ -121,17 +126,7 @@ const setVerifyStatus = (id: string, status: VerifyButtonStatus) => {
|
|||||||
};
|
};
|
||||||
const isVerifying = (row: llmType) => getVerifyStatus(row) === 'loading';
|
const isVerifying = (row: llmType) => getVerifyStatus(row) === 'loading';
|
||||||
const getVerifyButtonText = (row: llmType) => {
|
const getVerifyButtonText = (row: llmType) => {
|
||||||
const status = getVerifyStatus(row);
|
return getStatusButtonText(getVerifyStatus(row));
|
||||||
if (status === 'loading') {
|
|
||||||
return '验证中';
|
|
||||||
}
|
|
||||||
if (status === 'success') {
|
|
||||||
return '验证成功';
|
|
||||||
}
|
|
||||||
if (status === 'error') {
|
|
||||||
return '验证失败';
|
|
||||||
}
|
|
||||||
return '验证配置';
|
|
||||||
};
|
};
|
||||||
const getVerifyButtonIcon = (row: llmType) => {
|
const getVerifyButtonIcon = (row: llmType) => {
|
||||||
const status = getVerifyStatus(row);
|
const status = getVerifyStatus(row);
|
||||||
@@ -141,6 +136,9 @@ const getVerifyButtonIcon = (row: llmType) => {
|
|||||||
if (status === 'success') {
|
if (status === 'success') {
|
||||||
return CircleCheck;
|
return CircleCheck;
|
||||||
}
|
}
|
||||||
|
if (status === 'warning') {
|
||||||
|
return Warning;
|
||||||
|
}
|
||||||
if (status === 'error') {
|
if (status === 'error') {
|
||||||
return CircleClose;
|
return CircleClose;
|
||||||
}
|
}
|
||||||
@@ -285,17 +283,16 @@ const handleVerify = async (row: llmType) => {
|
|||||||
try {
|
try {
|
||||||
const res = await verifyModelConfig(modelId);
|
const res = await verifyModelConfig(modelId);
|
||||||
|
|
||||||
if (res.errorCode === 0) {
|
const feedback = resolveModelVerificationFeedback(res, row.modelType);
|
||||||
setVerifyStatus(modelId, 'success');
|
setVerifyStatus(modelId, feedback.status);
|
||||||
if (row.modelType === 'embeddingModel' && res?.data?.dimension) {
|
|
||||||
ElMessage.success(`验证成功,向量维度:${res.data.dimension}`);
|
if (feedback.status === 'success') {
|
||||||
} else {
|
ElMessage.success(feedback.message);
|
||||||
ElMessage.success('验证成功');
|
} else if (feedback.status === 'warning') {
|
||||||
}
|
ElMessage.warning(feedback.message);
|
||||||
} else {
|
} else {
|
||||||
setVerifyStatus(modelId, 'error');
|
|
||||||
if (!res.message) {
|
if (!res.message) {
|
||||||
ElMessage.error($t('ui.actionMessage.operationFailed'));
|
ElMessage.error(feedback.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -717,6 +714,10 @@ defineExpose({
|
|||||||
color: hsl(var(--success));
|
color: hsl(var(--success));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.active-workspace__verify-btn.is-warning {
|
||||||
|
color: hsl(var(--warning));
|
||||||
|
}
|
||||||
|
|
||||||
.active-workspace__verify-btn.is-error {
|
.active-workspace__verify-btn.is-error {
|
||||||
color: hsl(var(--destructive));
|
color: hsl(var(--destructive));
|
||||||
}
|
}
|
||||||
@@ -732,6 +733,7 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.active-workspace__verify-icon.is-success,
|
.active-workspace__verify-icon.is-success,
|
||||||
|
.active-workspace__verify-icon.is-warning,
|
||||||
.active-workspace__verify-icon.is-error {
|
.active-workspace__verify-icon.is-error {
|
||||||
animation: active-workspace-verify-pop 0.32s ease;
|
animation: active-workspace-verify-pop 0.32s ease;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ import { computed, reactive, ref, watch } from 'vue';
|
|||||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
import { IconifyIcon } from '@easyflow/icons';
|
import { IconifyIcon } from '@easyflow/icons';
|
||||||
|
|
||||||
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
import {
|
||||||
|
ElForm,
|
||||||
|
ElFormItem,
|
||||||
|
ElInput,
|
||||||
|
ElMessage,
|
||||||
|
ElOption,
|
||||||
|
ElSelect,
|
||||||
|
} from 'element-plus';
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
@@ -19,6 +26,13 @@ import {
|
|||||||
resetModelAbility,
|
resetModelAbility,
|
||||||
} from '#/views/ai/model/modelUtils/model-ability-utils';
|
} from '#/views/ai/model/modelUtils/model-ability-utils';
|
||||||
|
|
||||||
|
type AgentHttpVersionPolicy = 'AUTO' | 'HTTP_1_1' | 'HTTP_2_PREFERRED';
|
||||||
|
|
||||||
|
interface ModelOptions {
|
||||||
|
agentHttpVersionPolicy: AgentHttpVersionPolicy;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
id?: string;
|
id?: string;
|
||||||
modelType: string;
|
modelType: string;
|
||||||
@@ -39,6 +53,7 @@ interface FormData {
|
|||||||
supportVideo: boolean;
|
supportVideo: boolean;
|
||||||
supportImageB64Only: boolean;
|
supportImageB64Only: boolean;
|
||||||
supportToolMessage: boolean;
|
supportToolMessage: boolean;
|
||||||
|
options: ModelOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -85,8 +100,39 @@ const formData = reactive<FormData>({
|
|||||||
supportVideo: false,
|
supportVideo: false,
|
||||||
supportImageB64Only: false,
|
supportImageB64Only: false,
|
||||||
supportToolMessage: true,
|
supportToolMessage: true,
|
||||||
|
options: {
|
||||||
|
agentHttpVersionPolicy: 'AUTO',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const agentHttpVersionOptions = [
|
||||||
|
{ label: '自动', value: 'AUTO' },
|
||||||
|
{ label: 'HTTP/1.1 兼容', value: 'HTTP_1_1' },
|
||||||
|
{ label: 'HTTP/2 优先', value: 'HTTP_2_PREFERRED' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const normalizeAgentHttpVersionPolicy = (
|
||||||
|
value?: unknown,
|
||||||
|
): AgentHttpVersionPolicy => {
|
||||||
|
if (value === 'HTTP_1_1' || value === 'HTTP_2_PREFERRED') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return 'AUTO';
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeModelOptions = (options?: unknown): ModelOptions => {
|
||||||
|
const source =
|
||||||
|
options && typeof options === 'object' && !Array.isArray(options)
|
||||||
|
? (options as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
return {
|
||||||
|
...source,
|
||||||
|
agentHttpVersionPolicy: normalizeAgentHttpVersionPolicy(
|
||||||
|
source.agentHttpVersionPolicy,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const modelAbility = ref<ModelAbilityItem[]>(getDefaultModelAbility());
|
const modelAbility = ref<ModelAbilityItem[]>(getDefaultModelAbility());
|
||||||
const visibleModelAbility = computed(() =>
|
const visibleModelAbility = computed(() =>
|
||||||
modelAbility.value.filter(
|
modelAbility.value.filter(
|
||||||
@@ -189,6 +235,7 @@ const resetFormData = () => {
|
|||||||
supportImageB64Only: false,
|
supportImageB64Only: false,
|
||||||
supportFree: false,
|
supportFree: false,
|
||||||
supportToolMessage: true,
|
supportToolMessage: true,
|
||||||
|
options: normalizeModelOptions(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -231,6 +278,7 @@ defineExpose({
|
|||||||
supportFree: item.supportFree || false,
|
supportFree: item.supportFree || false,
|
||||||
supportToolMessage:
|
supportToolMessage:
|
||||||
item.supportToolMessage === undefined ? true : item.supportToolMessage,
|
item.supportToolMessage === undefined ? true : item.supportToolMessage,
|
||||||
|
options: normalizeModelOptions(item.options),
|
||||||
});
|
});
|
||||||
selectedModelType.value = normalizeSelectableModelType(item.modelType);
|
selectedModelType.value = normalizeSelectableModelType(item.modelType);
|
||||||
if (selectedModelType.value) {
|
if (selectedModelType.value) {
|
||||||
@@ -386,6 +434,20 @@ const save = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|
||||||
|
<ElFormItem v-if="!hasSpecialModelType" label="Agent HTTP 传输">
|
||||||
|
<ElSelect
|
||||||
|
v-model="formData.options.agentHttpVersionPolicy"
|
||||||
|
aria-label="Agent HTTP 传输"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="item in agentHttpVersionOptions"
|
||||||
|
:key="item.value"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
</div>
|
</div>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import {
|
|||||||
ElSelect,
|
ElSelect,
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
import { api } from '#/api/request';
|
import { getModelList, verifyModelConfig } from '#/api/ai/llm';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
import { resolveModelVerificationFeedback } from '#/views/ai/model/modelUtils/model-verification';
|
||||||
|
|
||||||
type VerifyStatus = 'error' | 'idle' | 'success';
|
type VerifyStatus = 'error' | 'idle' | 'success' | 'warning';
|
||||||
|
|
||||||
const options = ref<any[]>([]);
|
const options = ref<any[]>([]);
|
||||||
const modelType = ref('');
|
const modelType = ref('');
|
||||||
@@ -39,11 +40,15 @@ const resultTitle = computed(() => {
|
|||||||
return '验证失败';
|
return '验证失败';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (verifyStatus.value === 'warning') {
|
||||||
|
return '流式不可用';
|
||||||
|
}
|
||||||
|
|
||||||
return '等待验证';
|
return '等待验证';
|
||||||
});
|
});
|
||||||
|
|
||||||
const getLlmList = async (providerId: string) => {
|
const getLlmList = async (providerId: string) => {
|
||||||
const res = await api.get(`/api/v1/model/list?providerId=${providerId}`, {});
|
const res = await getModelList({ providerId });
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
options.value = res.data;
|
options.value = res.data;
|
||||||
}
|
}
|
||||||
@@ -87,17 +92,21 @@ const save = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await formDataRef.value.validate();
|
await formDataRef.value.validate();
|
||||||
const res = await api.get(
|
const res = await verifyModelConfig(formData.llmId);
|
||||||
`/api/v1/model/verifyLlmConfig?id=${formData.llmId}`,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
verifyStatus.value = 'success';
|
const feedback = resolveModelVerificationFeedback(res, modelType.value);
|
||||||
verifyMessage.value = $t('llm.testSuccess');
|
verifyStatus.value = feedback.status;
|
||||||
ElMessage.success($t('llm.testSuccess'));
|
verifyMessage.value = feedback.message;
|
||||||
if (modelType.value === 'embeddingModel' && res?.data?.dimension) {
|
if (feedback.status === 'warning') {
|
||||||
vectorDimension.value = res.data.dimension;
|
ElMessage.warning(feedback.message);
|
||||||
|
} else if (feedback.status === 'success') {
|
||||||
|
ElMessage.success(feedback.message);
|
||||||
|
} else {
|
||||||
|
ElMessage.error(feedback.message);
|
||||||
|
}
|
||||||
|
if (modelType.value === 'embeddingModel' && feedback.dimension) {
|
||||||
|
vectorDimension.value = String(feedback.dimension);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
verifyStatus.value = 'error';
|
verifyStatus.value = 'error';
|
||||||
@@ -136,9 +145,7 @@ const save = async () => {
|
|||||||
<section class="verify-modal__section">
|
<section class="verify-modal__section">
|
||||||
<div class="verify-modal__section-head">
|
<div class="verify-modal__section-head">
|
||||||
<h3>1. 选择待验证模型</h3>
|
<h3>1. 选择待验证模型</h3>
|
||||||
<p>
|
<p>会用当前保存的配置检查基础连接和流式响应。</p>
|
||||||
会用当前保存的服务商配置发起一次真实请求,帮助你确认密钥和路径是否正确。
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ElForm
|
<ElForm
|
||||||
@@ -170,13 +177,14 @@ const save = async () => {
|
|||||||
<section class="verify-modal__section verify-modal__section--result">
|
<section class="verify-modal__section verify-modal__section--result">
|
||||||
<div class="verify-modal__section-head">
|
<div class="verify-modal__section-head">
|
||||||
<h3>2. 查看验证结果</h3>
|
<h3>2. 查看验证结果</h3>
|
||||||
<p>成功后会返回可用状态;如果是向量模型,还会展示向量维度。</p>
|
<p>会展示可用状态;向量模型还会展示向量维度。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="verify-result-card"
|
class="verify-result-card"
|
||||||
:class="{
|
:class="{
|
||||||
'is-success': verifyStatus === 'success',
|
'is-success': verifyStatus === 'success',
|
||||||
|
'is-warning': verifyStatus === 'warning',
|
||||||
'is-error': verifyStatus === 'error',
|
'is-error': verifyStatus === 'error',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
@@ -253,6 +261,11 @@ const save = async () => {
|
|||||||
border-color: hsl(var(--success) / 36%);
|
border-color: hsl(var(--success) / 36%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.verify-result-card.is-warning {
|
||||||
|
background: hsl(var(--warning) / 6%);
|
||||||
|
border-color: hsl(var(--warning) / 36%);
|
||||||
|
}
|
||||||
|
|
||||||
.verify-result-card.is-error {
|
.verify-result-card.is-error {
|
||||||
background: hsl(var(--destructive) / 5%);
|
background: hsl(var(--destructive) / 5%);
|
||||||
border-color: hsl(var(--destructive) / 36%);
|
border-color: hsl(var(--destructive) / 36%);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { PropType } from 'vue';
|
|||||||
|
|
||||||
import type { llmType } from '#/api';
|
import type { llmType } from '#/api';
|
||||||
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
|
import type { ModelAbilityItem } from '#/views/ai/model/modelUtils/model-ability';
|
||||||
|
import type { VerifyButtonStatus } from '#/views/ai/model/modelUtils/model-verification';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
Edit,
|
Edit,
|
||||||
Loading,
|
Loading,
|
||||||
Select,
|
Select,
|
||||||
|
Warning,
|
||||||
} from '@element-plus/icons-vue';
|
} from '@element-plus/icons-vue';
|
||||||
import { ElButton, ElIcon, ElMessage, ElTag } from 'element-plus';
|
import { ElButton, ElIcon, ElMessage, ElTag } from 'element-plus';
|
||||||
|
|
||||||
@@ -20,6 +22,10 @@ import { verifyModelConfig } from '#/api/ai/llm';
|
|||||||
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
|
import ModelProviderBadge from '#/views/ai/model/ModelProviderBadge.vue';
|
||||||
import { getDefaultModelAbility } from '#/views/ai/model/modelUtils/model-ability';
|
import { getDefaultModelAbility } from '#/views/ai/model/modelUtils/model-ability';
|
||||||
import { mapLlmToModelAbility } from '#/views/ai/model/modelUtils/model-ability-utils';
|
import { mapLlmToModelAbility } from '#/views/ai/model/modelUtils/model-ability-utils';
|
||||||
|
import {
|
||||||
|
getVerifyButtonText as getStatusButtonText,
|
||||||
|
resolveModelVerificationFeedback,
|
||||||
|
} from '#/views/ai/model/modelUtils/model-verification';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
llmList: {
|
llmList: {
|
||||||
@@ -33,7 +39,6 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['deleteLlm', 'editLlm']);
|
const emit = defineEmits(['deleteLlm', 'editLlm']);
|
||||||
type VerifyButtonStatus = 'error' | 'idle' | 'loading' | 'success';
|
|
||||||
const verifyStatusMap = ref<Record<string, VerifyButtonStatus>>({});
|
const verifyStatusMap = ref<Record<string, VerifyButtonStatus>>({});
|
||||||
const getModelId = (llm: llmType) =>
|
const getModelId = (llm: llmType) =>
|
||||||
String((llm as any).id || (llm as any).llmId || (llm as any).modelId || '');
|
String((llm as any).id || (llm as any).llmId || (llm as any).modelId || '');
|
||||||
@@ -47,17 +52,7 @@ const setVerifyStatus = (id: string, status: VerifyButtonStatus) => {
|
|||||||
verifyStatusMap.value[id] = status;
|
verifyStatusMap.value[id] = status;
|
||||||
};
|
};
|
||||||
const getVerifyButtonText = (llm: llmType) => {
|
const getVerifyButtonText = (llm: llmType) => {
|
||||||
const status = getVerifyStatus(llm);
|
return getStatusButtonText(getVerifyStatus(llm));
|
||||||
if (status === 'loading') {
|
|
||||||
return '验证中';
|
|
||||||
}
|
|
||||||
if (status === 'success') {
|
|
||||||
return '验证成功';
|
|
||||||
}
|
|
||||||
if (status === 'error') {
|
|
||||||
return '验证失败';
|
|
||||||
}
|
|
||||||
return '验证配置';
|
|
||||||
};
|
};
|
||||||
const getVerifyButtonIcon = (llm: llmType) => {
|
const getVerifyButtonIcon = (llm: llmType) => {
|
||||||
const status = getVerifyStatus(llm);
|
const status = getVerifyStatus(llm);
|
||||||
@@ -67,6 +62,9 @@ const getVerifyButtonIcon = (llm: llmType) => {
|
|||||||
if (status === 'success') {
|
if (status === 'success') {
|
||||||
return CircleCheck;
|
return CircleCheck;
|
||||||
}
|
}
|
||||||
|
if (status === 'warning') {
|
||||||
|
return Warning;
|
||||||
|
}
|
||||||
if (status === 'error') {
|
if (status === 'error') {
|
||||||
return CircleClose;
|
return CircleClose;
|
||||||
}
|
}
|
||||||
@@ -97,17 +95,16 @@ const handleVerifyLlm = async (llm: llmType) => {
|
|||||||
try {
|
try {
|
||||||
const res = await verifyModelConfig(modelId);
|
const res = await verifyModelConfig(modelId);
|
||||||
|
|
||||||
if (res.errorCode === 0) {
|
const feedback = resolveModelVerificationFeedback(res, llm.modelType);
|
||||||
setVerifyStatus(modelId, 'success');
|
setVerifyStatus(modelId, feedback.status);
|
||||||
if (llm.modelType === 'embeddingModel' && res?.data?.dimension) {
|
|
||||||
ElMessage.success(`验证成功,向量维度:${res.data.dimension}`);
|
if (feedback.status === 'success') {
|
||||||
} else {
|
ElMessage.success(feedback.message);
|
||||||
ElMessage.success('验证成功');
|
} else if (feedback.status === 'warning') {
|
||||||
}
|
ElMessage.warning(feedback.message);
|
||||||
} else {
|
} else {
|
||||||
setVerifyStatus(modelId, 'error');
|
|
||||||
if (!res.message) {
|
if (!res.message) {
|
||||||
ElMessage.error('验证失败');
|
ElMessage.error(feedback.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -309,6 +306,10 @@ const getSelectedAbilityTagsForLlm = (llm: llmType): ModelAbilityItem[] => {
|
|||||||
color: hsl(var(--success));
|
color: hsl(var(--success));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.llm-item__verify-btn.is-warning {
|
||||||
|
color: hsl(var(--warning));
|
||||||
|
}
|
||||||
|
|
||||||
.llm-item__verify-btn.is-error {
|
.llm-item__verify-btn.is-error {
|
||||||
color: hsl(var(--destructive));
|
color: hsl(var(--destructive));
|
||||||
}
|
}
|
||||||
@@ -324,6 +325,7 @@ const getSelectedAbilityTagsForLlm = (llm: llmType): ModelAbilityItem[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.llm-item__verify-icon.is-success,
|
.llm-item__verify-icon.is-success,
|
||||||
|
.llm-item__verify-icon.is-warning,
|
||||||
.llm-item__verify-icon.is-error {
|
.llm-item__verify-icon.is-error {
|
||||||
animation: llm-item-verify-pop 0.32s ease;
|
animation: llm-item-verify-pop 0.32s ease;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getVerifyButtonText,
|
||||||
|
resolveModelVerificationFeedback,
|
||||||
|
} from '../model-verification';
|
||||||
|
|
||||||
|
describe('model verification helpers', () => {
|
||||||
|
it('双阶段通过时返回成功状态', () => {
|
||||||
|
expect(
|
||||||
|
resolveModelVerificationFeedback(
|
||||||
|
{
|
||||||
|
data: { status: 'PASSED' },
|
||||||
|
errorCode: 0,
|
||||||
|
},
|
||||||
|
'chatModel',
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
dimension: undefined,
|
||||||
|
message: '验证成功',
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('基础连接通过但流式失败时返回警告状态', () => {
|
||||||
|
const feedback = resolveModelVerificationFeedback(
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
message: '连接成功,流式响应不可用,可关闭智能体的模型流式响应。',
|
||||||
|
status: 'PARTIAL',
|
||||||
|
},
|
||||||
|
errorCode: 0,
|
||||||
|
},
|
||||||
|
'chatModel',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(feedback.status).toBe('warning');
|
||||||
|
expect(feedback.message).toContain('流式响应不可用');
|
||||||
|
expect(getVerifyButtonText('warning')).toBe('流式不可用');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('向量模型验证保留维度结果', () => {
|
||||||
|
expect(
|
||||||
|
resolveModelVerificationFeedback(
|
||||||
|
{ data: { dimension: 1024 }, errorCode: 0 },
|
||||||
|
'embeddingModel',
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
dimension: 1024,
|
||||||
|
message: '验证成功,向量维度:1024',
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('接口失败时返回错误状态', () => {
|
||||||
|
expect(
|
||||||
|
resolveModelVerificationFeedback(
|
||||||
|
{ errorCode: 1, message: '密钥无效' },
|
||||||
|
'chatModel',
|
||||||
|
),
|
||||||
|
).toEqual({ message: '密钥无效', status: 'error' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import type { ModelVerificationData } from '#/api/ai/llm';
|
||||||
|
|
||||||
|
export type VerifyButtonStatus =
|
||||||
|
| 'error'
|
||||||
|
| 'idle'
|
||||||
|
| 'loading'
|
||||||
|
| 'success'
|
||||||
|
| 'warning';
|
||||||
|
|
||||||
|
export interface ModelVerificationFeedback {
|
||||||
|
dimension?: number;
|
||||||
|
message: string;
|
||||||
|
status: 'error' | 'success' | 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelVerificationResponse {
|
||||||
|
data?: ModelVerificationData;
|
||||||
|
errorCode: number;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STREAMING_UNAVAILABLE_MESSAGE =
|
||||||
|
'连接成功,流式响应不可用,可关闭智能体的模型流式响应。';
|
||||||
|
|
||||||
|
export function resolveModelVerificationFeedback(
|
||||||
|
response: ModelVerificationResponse,
|
||||||
|
modelType: string,
|
||||||
|
): ModelVerificationFeedback {
|
||||||
|
if (response.errorCode !== 0) {
|
||||||
|
return {
|
||||||
|
message: response.message || '验证失败',
|
||||||
|
status: 'error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data?.status === 'PARTIAL') {
|
||||||
|
return {
|
||||||
|
message: response.data.message || STREAMING_UNAVAILABLE_MESSAGE,
|
||||||
|
status: 'warning',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data?.status === 'FAILED') {
|
||||||
|
return {
|
||||||
|
message: response.data.message || '验证失败',
|
||||||
|
status: 'error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dimension = response.data?.dimension;
|
||||||
|
return {
|
||||||
|
dimension,
|
||||||
|
message:
|
||||||
|
modelType === 'embeddingModel' && dimension
|
||||||
|
? `验证成功,向量维度:${dimension}`
|
||||||
|
: response.data?.message || '验证成功',
|
||||||
|
status: 'success',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVerifyButtonText(status: VerifyButtonStatus): string {
|
||||||
|
if (status === 'loading') {
|
||||||
|
return '验证中';
|
||||||
|
}
|
||||||
|
if (status === 'success') {
|
||||||
|
return '验证成功';
|
||||||
|
}
|
||||||
|
if (status === 'warning') {
|
||||||
|
return '流式不可用';
|
||||||
|
}
|
||||||
|
if (status === 'error') {
|
||||||
|
return '验证失败';
|
||||||
|
}
|
||||||
|
return '验证配置';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user