package cn.iocoder.yudao.module.ai.util;
|
|
import cn.hutool.core.map.MapUtil;
|
import cn.hutool.core.util.ObjUtil;
|
import cn.hutool.core.util.StrUtil;
|
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
|
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
|
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
|
import cn.hutool.extra.spring.SpringUtil;
|
import org.springframework.ai.anthropic.AnthropicChatOptions;
|
import org.springframework.ai.chat.messages.*;
|
import org.springframework.ai.chat.model.ChatResponse;
|
import org.springframework.ai.chat.prompt.ChatOptions;
|
import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
|
import org.springframework.ai.deepseek.DeepSeekChatOptions;
|
import org.springframework.ai.google.genai.GoogleGenAiChatOptions;
|
import org.springframework.ai.ollama.api.OllamaChatOptions;
|
import org.springframework.ai.openai.OpenAiChatOptions;
|
import org.springframework.ai.tool.ToolCallback;
|
import org.springframework.core.env.Environment;
|
|
import java.util.*;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.API_CONFIG_PLACEHOLDER_NOT_RESOLVED;
|
|
/**
|
* Spring AI 工具类
|
*
|
* @author 芋道源码
|
*/
|
public class AiUtils {
|
|
public static final String TOOL_CONTEXT_LOGIN_USER = "LOGIN_USER";
|
public static final String TOOL_CONTEXT_TENANT_ID = "TENANT_ID";
|
|
/**
|
* 解析 DB 等动态配置里的 Spring 占位符,例如 ${OPENAI_API_KEY}
|
*
|
* @param value 待解析的配置值
|
* @return 解析后的配置值
|
*/
|
public static String resolveSpringPlaceholders(String value) {
|
if (StrUtil.isBlank(value) || !StrUtil.contains(value, "${")) {
|
return value;
|
}
|
try {
|
return SpringUtil.getBean(Environment.class).resolveRequiredPlaceholders(value);
|
} catch (IllegalArgumentException ex) {
|
throw exception(API_CONFIG_PLACEHOLDER_NOT_RESOLVED, value);
|
}
|
}
|
|
/**
|
* 校验 API Key,避免集成测试使用默认占位值发起调用。
|
*
|
* @param apiKey API Key
|
*/
|
public static void validateApiKey(String apiKey) {
|
if (StrUtil.isBlank(apiKey) || "sk-xxxx".equals(apiKey)) {
|
throw new IllegalStateException("apiKey 不能为空");
|
}
|
}
|
|
/**
|
* 通义千问支持多模态的模型
|
*
|
* @see <a href="https://bailian.console.aliyun.com/cn-beijing/?tab=model#/model-market/all?providers=qwen&capabilities=VU">模型广场</a>
|
* @see <a href="https://help.aliyun.com/zh/model-studio/error-code#error-url">必须开启 withMultiModel 参数</a>
|
*/
|
public static final Set<String> TONG_YI_MULTI_MODELS = SetUtils.asSet(
|
// qwen3.5 / 3.6 / 3.7 系列(统一多模态主干)
|
"qwen3.7-max", "qwen3.7-plus", "qwen3.7-flash",
|
"qwen3.6-plus", "qwen3.6-flash",
|
"qwen3.5-plus", "qwen3.5-flash",
|
// qwen-vl 视觉理解
|
"qwen3-vl-plus", "qwen3-vl-flash",
|
"qwen-vl-max", "qwen-vl-plus",
|
"qwen2.5-vl-72b-instruct", "qwen2.5-vl-32b-instruct",
|
"qwen2.5-vl-7b-instruct", "qwen2.5-vl-3b-instruct",
|
// qvq 视觉推理
|
"qvq-max", "qvq-plus",
|
// qwen-omni 全模态
|
"qwen3.5-omni-plus", "qwen3.5-omni-flash",
|
"qwen3-omni-flash", "qwen-omni-turbo"
|
);
|
|
public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
|
return buildChatOptions(platform, model, temperature, maxTokens, null, null);
|
}
|
|
public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens,
|
List<ToolCallback> toolCallbacks, Map<String, Object> toolContext) {
|
toolCallbacks = ObjUtil.defaultIfNull(toolCallbacks, Collections.emptyList());
|
toolContext = ObjUtil.defaultIfNull(toolContext, Collections.emptyMap());
|
// noinspection EnhancedSwitchMigration
|
switch (platform) {
|
case TONG_YI:
|
return DashScopeChatOptions.builder().model(model).temperature(temperature).maxToken(maxTokens)
|
.enableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
|
.multiModel(TONG_YI_MULTI_MODELS.contains(model)) // 是否多模态模型
|
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
|
case DEEP_SEEK:
|
case DOU_BAO: // 复用 DeepSeek 客户端
|
case HUN_YUAN: // 复用 DeepSeek 客户端
|
case SILICON_FLOW: // 复用 DeepSeek 客户端
|
case YI_YAN: // 复用 DeepSeek 客户端
|
case ZHI_PU: // 复用 DeepSeek 客户端
|
case XING_HUO: // 复用 DeepSeek 客户端
|
case MINI_MAX: // 复用 DeepSeek 客户端
|
case MOONSHOT: // 复用 DeepSeek 客户端
|
case BAI_CHUAN: // 复用 DeepSeek 客户端
|
case STEP_FUN: // 复用 DeepSeek 客户端
|
return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
|
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
|
case OPENAI:
|
case GROK: // 复用 OpenAI 客户端
|
return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
|
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
|
case GEMINI:
|
return GoogleGenAiChatOptions.builder().model(model).temperature(temperature).maxOutputTokens(maxTokens)
|
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
|
case AZURE_OPENAI:
|
return OpenAiChatOptions.builder().model(model).deploymentName(model).azure(true)
|
.temperature(temperature).maxTokens(maxTokens)
|
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
|
case ANTHROPIC:
|
return AnthropicChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
|
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
|
case OLLAMA:
|
return OllamaChatOptions.builder().model(model).temperature(temperature).numPredict(maxTokens)
|
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
|
default:
|
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
|
}
|
}
|
|
public static Message buildMessage(String type, String content) {
|
if (MessageType.USER.getValue().equals(type)) {
|
return new UserMessage(content);
|
}
|
if (MessageType.ASSISTANT.getValue().equals(type)) {
|
return new AssistantMessage(content);
|
}
|
if (MessageType.SYSTEM.getValue().equals(type)) {
|
return new SystemMessage(content);
|
}
|
if (MessageType.TOOL.getValue().equals(type)) {
|
throw new UnsupportedOperationException("暂不支持 tool 消息:" + content);
|
}
|
throw new IllegalArgumentException(StrUtil.format("未知消息类型({})", type));
|
}
|
|
public static Map<String, Object> buildCommonToolContext() {
|
Map<String, Object> context = new HashMap<>();
|
context.put(TOOL_CONTEXT_LOGIN_USER, SecurityFrameworkUtils.getLoginUser());
|
context.put(TOOL_CONTEXT_TENANT_ID, TenantContextHolder.getTenantId());
|
return context;
|
}
|
|
@SuppressWarnings("ConstantValue")
|
public static String getChatResponseContent(ChatResponse response) {
|
if (response == null
|
|| response.getResult() == null
|
|| response.getResult().getOutput() == null) {
|
return null;
|
}
|
return response.getResult().getOutput().getText();
|
}
|
|
@SuppressWarnings("ConstantValue")
|
public static String getChatResponseReasoningContent(ChatResponse response) {
|
if (response == null
|
|| response.getResult() == null
|
|| response.getResult().getOutput() == null) {
|
return null;
|
}
|
AssistantMessage output = response.getResult().getOutput();
|
// DeepSeek 通过专属 AssistantMessage 暴露 reasoningContent
|
if (output instanceof DeepSeekAssistantMessage) {
|
return ((DeepSeekAssistantMessage) output).getReasoningContent();
|
}
|
// 通义千问等通过 metadata 透传 reasoningContent
|
return MapUtil.getStr(output.getMetadata(), "reasoningContent");
|
}
|
|
}
|