frist commit

This commit is contained in:
dengqichen
2025-04-30 17:27:52 +08:00
commit e503b90cd7
4295 changed files with 340056 additions and 0 deletions
@@ -0,0 +1,253 @@
package cn.iocoder.yudao.framework.ai.config;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory;
import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl;
import cn.iocoder.yudao.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel;
import cn.iocoder.yudao.framework.ai.core.model.doubao.DouBaoChatModel;
import cn.iocoder.yudao.framework.ai.core.model.hunyuan.HunYuanChatModel;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreProperties;
import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties;
import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties;
import org.springframework.ai.embedding.BatchingStrategy;
import org.springframework.ai.embedding.TokenCountBatchingStrategy;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* 芋道 AI 自动配置
*
* @author fansili
*/
@AutoConfiguration
@EnableConfigurationProperties({ YudaoAiProperties.class,
QdrantVectorStoreProperties.class, // 解析 Qdrant 配置
RedisVectorStoreProperties.class, // 解析 Redis 配置
MilvusVectorStoreProperties.class, MilvusServiceClientProperties.class // 解析 Milvus 配置
})
@Slf4j
public class YudaoAiAutoConfiguration {
@Bean
public AiModelFactory aiModelFactory() {
return new AiModelFactoryImpl();
}
// ========== 各种 AI Client 创建 ==========
@Bean
@ConditionalOnProperty(value = "yudao.ai.deepseek.enable", havingValue = "true")
public DeepSeekChatModel deepSeekChatModel(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.DeepSeekProperties properties = yudaoAiProperties.getDeepseek();
return buildDeepSeekChatModel(properties);
}
public DeepSeekChatModel buildDeepSeekChatModel(YudaoAiProperties.DeepSeekProperties properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(DeepSeekChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(DeepSeekChatModel.BASE_URL)
.apiKey(properties.getApiKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new DeepSeekChatModel(openAiChatModel);
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true")
public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.DouBaoProperties properties = yudaoAiProperties.getDoubao();
return buildDouBaoChatClient(properties);
}
public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBaoProperties properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(DouBaoChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(DouBaoChatModel.BASE_URL)
.apiKey(properties.getApiKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new DouBaoChatModel(openAiChatModel);
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true")
public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.SiliconFlowProperties properties = yudaoAiProperties.getSiliconflow();
return buildSiliconFlowChatClient(properties);
}
public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlowProperties properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL)
.apiKey(properties.getApiKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new SiliconFlowChatModel(openAiChatModel);
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true")
public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.HunYuanProperties properties = yudaoAiProperties.getHunyuan();
return buildHunYuanChatClient(properties);
}
public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuanProperties properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(HunYuanChatModel.MODEL_DEFAULT);
}
// 特殊:由于混元大模型不提供 deepseek,而是通过知识引擎,所以需要区分下 URL
if (StrUtil.isEmpty(properties.getBaseUrl())) {
properties.setBaseUrl(
StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL
: HunYuanChatModel.BASE_URL);
}
// 创建 OpenAiChatModel、HunYuanChatModel 对象
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(properties.getBaseUrl())
.apiKey(properties.getApiKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new HunYuanChatModel(openAiChatModel);
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true")
public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo();
return buildXingHuoChatClient(properties);
}
public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuoProperties properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(XingHuoChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(XingHuoChatModel.BASE_URL)
.apiKey(properties.getAppKey() + ":" + properties.getSecretKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new XingHuoChatModel(openAiChatModel);
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true")
public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.BaiChuanProperties properties = yudaoAiProperties.getBaichuan();
return buildBaiChuanChatClient(properties);
}
public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuanProperties properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(BaiChuanChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(BaiChuanChatModel.BASE_URL)
.apiKey(properties.getApiKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new BaiChuanChatModel(openAiChatModel);
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true")
public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.MidjourneyProperties config = yudaoAiProperties.getMidjourney();
return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl());
}
@Bean
@ConditionalOnProperty(value = "yudao.ai.suno.enable", havingValue = "true")
public SunoApi sunoApi(YudaoAiProperties yudaoAiProperties) {
return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl());
}
// ========== RAG 相关 ==========
@Bean
public TokenCountEstimator tokenCountEstimator() {
return new JTokkitTokenCountEstimator();
}
@Bean
public BatchingStrategy batchingStrategy() {
return new TokenCountBatchingStrategy();
}
private static ToolCallingManager getToolCallingManager() {
return SpringUtil.getBean(ToolCallingManager.class);
}
}
@@ -0,0 +1,164 @@
package cn.iocoder.yudao.framework.ai.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 芋道 AI 配置类
*
* @author fansili
* @since 1.0
*/
@ConfigurationProperties(prefix = "yudao.ai")
@Data
public class YudaoAiProperties {
/**
* DeepSeek
*/
@SuppressWarnings("SpellCheckingInspection")
private DeepSeekProperties deepseek;
/**
* 字节豆包
*/
@SuppressWarnings("SpellCheckingInspection")
private DouBaoProperties doubao;
/**
* 腾讯混元
*/
@SuppressWarnings("SpellCheckingInspection")
private HunYuanProperties hunyuan;
/**
* 硅基流动
*/
@SuppressWarnings("SpellCheckingInspection")
private SiliconFlowProperties siliconflow;
/**
* 讯飞星火
*/
@SuppressWarnings("SpellCheckingInspection")
private XingHuoProperties xinghuo;
/**
* 百川
*/
@SuppressWarnings("SpellCheckingInspection")
private BaiChuanProperties baichuan;
/**
* Midjourney 绘图
*/
private MidjourneyProperties midjourney;
/**
* Suno 音乐
*/
@SuppressWarnings("SpellCheckingInspection")
private SunoProperties suno;
@Data
public static class DeepSeekProperties {
private String enable;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class DouBaoProperties {
private String enable;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class HunYuanProperties {
private String enable;
private String baseUrl;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class SiliconFlowProperties {
private String enable;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class XingHuoProperties {
private String enable;
private String appId;
private String appKey;
private String secretKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class BaiChuanProperties {
private String enable;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data
public static class MidjourneyProperties {
private String enable;
private String baseUrl;
private String apiKey;
private String notifyUrl;
}
@Data
public static class SunoProperties {
private boolean enable = false;
private String baseUrl;
}
}
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.framework.ai.core.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* AI 模型类型的枚举
*
* @author 芋道源码
*/
@Getter
@RequiredArgsConstructor
public enum AiModelTypeEnum implements ArrayValuable<Integer> {
CHAT(1, "对话"),
IMAGE(2, "图片"),
VOICE(3, "语音"),
VIDEO(4, "视频"),
EMBEDDING(5, "向量"),
RERANK(6, "重排序");
/**
* 类型
*/
private final Integer type;
/**
* 类型名
*/
private final String name;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(AiModelTypeEnum::getType).toArray(Integer[]::new);
@Override
public Integer[] array() {
return ARRAYS;
}
}
@@ -0,0 +1,69 @@
package cn.iocoder.yudao.framework.ai.core.enums;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* AI 模型平台
*
* @author fansili
*/
@Getter
@AllArgsConstructor
public enum AiPlatformEnum implements ArrayValuable<String> {
// ========== 国内平台 ==========
TONG_YI("TongYi", "通义千问"), // 阿里
YI_YAN("YiYan", "文心一言"), // 百度
DEEP_SEEK("DeepSeek", "DeepSeek"), // DeepSeek
ZHI_PU("ZhiPu", "智谱"), // 智谱 AI
XING_HUO("XingHuo", "星火"), // 讯飞
DOU_BAO("DouBao", "豆包"), // 字节
HUN_YUAN("HunYuan", "混元"), // 腾讯
SILICON_FLOW("SiliconFlow", "硅基流动"), // 硅基流动
MINI_MAX("MiniMax", "MiniMax"), // 稀宇科技
MOONSHOT("Moonshot", "月之暗灭"), // KIMI
BAI_CHUAN("BaiChuan", "百川智能"), // 百川智能
// ========== 国外平台 ==========
OPENAI("OpenAI", "OpenAI"), // OpenAI 官方
AZURE_OPENAI("AzureOpenAI", "AzureOpenAI"), // OpenAI 微软
OLLAMA("Ollama", "Ollama"),
STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI
MIDJOURNEY("Midjourney", "Midjourney"), // Midjourney
SUNO("Suno", "Suno"), // Suno AI
;
/**
* 平台
*/
private final String platform;
/**
* 平台名
*/
private final String name;
public static final String[] ARRAYS = Arrays.stream(values()).map(AiPlatformEnum::getPlatform).toArray(String[]::new);
public static AiPlatformEnum validatePlatform(String platform) {
for (AiPlatformEnum platformEnum : AiPlatformEnum.values()) {
if (platformEnum.getPlatform().equals(platform)) {
return platformEnum;
}
}
throw new IllegalArgumentException("非法平台: " + platform);
}
@Override
public String[] array() {
return ARRAYS;
}
}
@@ -0,0 +1,113 @@
package cn.iocoder.yudao.framework.ai.core.factory;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.vectorstore.VectorStore;
import java.util.Map;
/**
* AI Model 模型工厂的接口类
*
* @author fansili
*/
public interface AiModelFactory {
/**
* 基于指定配置,获得 ChatModel 对象
*
* 如果不存在,则进行创建
*
* @param platform 平台
* @param apiKey API KEY
* @param url API URL
* @return ChatModel 对象
*/
ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url);
/**
* 基于默认配置,获得 ChatModel 对象
*
* 默认配置,指的是在 application.yaml 配置文件中的 spring.ai 相关的配置
*
* @param platform 平台
* @return ChatModel 对象
*/
ChatModel getDefaultChatModel(AiPlatformEnum platform);
/**
* 基于默认配置,获得 ImageModel 对象
*
* 默认配置,指的是在 application.yaml 配置文件中的 spring.ai 相关的配置
*
* @param platform 平台
* @return ImageModel 对象
*/
ImageModel getDefaultImageModel(AiPlatformEnum platform);
/**
* 基于指定配置,获得 ImageModel 对象
*
* 如果不存在,则进行创建
*
* @param platform 平台
* @param apiKey API KEY
* @param url API URL
* @return ImageModel 对象
*/
ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url);
/**
* 基于指定配置,获得 MidjourneyApi 对象
*
* 如果不存在,则进行创建
*
* @param apiKey API KEY
* @param url API URL
* @return MidjourneyApi 对象
*/
MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url);
/**
* 基于指定配置,获得 SunoApi 对象
*
* 如果不存在,则进行创建
*
* @param apiKey API KEY
* @param url API URL
* @return SunoApi 对象
*/
SunoApi getOrCreateSunoApi(String apiKey, String url);
/**
* 基于指定配置,获得 EmbeddingModel 对象
*
* 如果不存在,则进行创建
*
* @param platform 平台
* @param apiKey API KEY
* @param url API URL
* @param model 模型
* @return ChatModel 对象
*/
EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url, String model);
/**
* 基于指定配置,获得 VectorStore 对象
*
* 如果不存在,则进行创建
*
* @param type 向量存储类型
* @param embeddingModel 向量模型
* @param metadataFields 元数据字段
* @return VectorStore 对象
*/
VectorStore getOrCreateVectorStore(Class<? extends VectorStore> type,
EmbeddingModel embeddingModel,
Map<String, Class<?>> metadataFields);
}
@@ -0,0 +1,752 @@
package cn.iocoder.yudao.framework.ai.core.factory;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.lang.func.Func0;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.RuntimeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration;
import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel;
import cn.iocoder.yudao.framework.ai.core.model.doubao.DouBaoChatModel;
import cn.iocoder.yudao.framework.ai.core.model.hunyuan.HunYuanChatModel;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowImageApi;
import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowImageModel;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAutoConfiguration;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel;
import com.azure.ai.openai.OpenAIClientBuilder;
import io.micrometer.observation.ObservationRegistry;
import io.milvus.client.MilvusServiceClient;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import lombok.SneakyThrows;
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration;
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties;
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties;
import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiEmbeddingProperties;
import org.springframework.ai.autoconfigure.minimax.MiniMaxAutoConfiguration;
import org.springframework.ai.autoconfigure.moonshot.MoonshotAutoConfiguration;
import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration;
import org.springframework.ai.autoconfigure.stabilityai.StabilityAiImageAutoConfiguration;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientConnectionDetails;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreProperties;
import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration;
import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties;
import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreAutoConfiguration;
import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties;
import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.embedding.BatchingStrategy;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.image.ImageModel;
import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;
import org.springframework.ai.minimax.api.MiniMaxApi;
import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.moonshot.MoonshotChatModel;
import org.springframework.ai.moonshot.MoonshotChatOptions;
import org.springframework.ai.moonshot.api.MoonshotApi;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.openai.OpenAiEmbeddingOptions;
import org.springframework.ai.openai.OpenAiImageModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
import org.springframework.ai.qianfan.QianFanChatModel;
import org.springframework.ai.qianfan.QianFanEmbeddingModel;
import org.springframework.ai.qianfan.QianFanEmbeddingOptions;
import org.springframework.ai.qianfan.QianFanImageModel;
import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.ai.qianfan.api.QianFanImageApi;
import org.springframework.ai.stabilityai.StabilityAiImageModel;
import org.springframework.ai.stabilityai.api.StabilityAiApi;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.milvus.MilvusVectorStore;
import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.ai.vectorstore.redis.RedisVectorStore;
import org.springframework.ai.zhipuai.*;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.web.client.RestClient;
import redis.clients.jedis.JedisPooled;
import java.io.File;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE;
/**
* AI Model 模型工厂的实现类
*
* @author 芋道源码
*/
public class AiModelFactoryImpl implements AiModelFactory {
@Override
public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url) {
String cacheKey = buildClientCacheKey(ChatModel.class, platform, apiKey, url);
return Singleton.get(cacheKey, (Func0<ChatModel>) () -> {
// noinspection EnhancedSwitchMigration
switch (platform) {
case TONG_YI:
return buildTongYiChatModel(apiKey);
case YI_YAN:
return buildYiYanChatModel(apiKey);
case DEEP_SEEK:
return buildDeepSeekChatModel(apiKey);
case DOU_BAO:
return buildDouBaoChatModel(apiKey);
case HUN_YUAN:
return buildHunYuanChatModel(apiKey, url);
case SILICON_FLOW:
return buildSiliconFlowChatModel(apiKey);
case ZHI_PU:
return buildZhiPuChatModel(apiKey, url);
case MINI_MAX:
return buildMiniMaxChatModel(apiKey, url);
case MOONSHOT:
return buildMoonshotChatModel(apiKey, url);
case XING_HUO:
return buildXingHuoChatModel(apiKey);
case BAI_CHUAN:
return buildBaiChuanChatModel(apiKey);
case OPENAI:
return buildOpenAiChatModel(apiKey, url);
case AZURE_OPENAI:
return buildAzureOpenAiChatModel(apiKey, url);
case OLLAMA:
return buildOllamaChatModel(url);
default:
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
}
});
}
@Override
public ChatModel getDefaultChatModel(AiPlatformEnum platform) {
// noinspection EnhancedSwitchMigration
switch (platform) {
case TONG_YI:
return SpringUtil.getBean(DashScopeChatModel.class);
case YI_YAN:
return SpringUtil.getBean(QianFanChatModel.class);
case DEEP_SEEK:
return SpringUtil.getBean(DeepSeekChatModel.class);
case DOU_BAO:
return SpringUtil.getBean(DouBaoChatModel.class);
case HUN_YUAN:
return SpringUtil.getBean(HunYuanChatModel.class);
case SILICON_FLOW:
return SpringUtil.getBean(SiliconFlowChatModel.class);
case ZHI_PU:
return SpringUtil.getBean(ZhiPuAiChatModel.class);
case MINI_MAX:
return SpringUtil.getBean(MiniMaxChatModel.class);
case MOONSHOT:
return SpringUtil.getBean(MoonshotChatModel.class);
case XING_HUO:
return SpringUtil.getBean(XingHuoChatModel.class);
case BAI_CHUAN:
return SpringUtil.getBean(AzureOpenAiChatModel.class);
case OPENAI:
return SpringUtil.getBean(OpenAiChatModel.class);
case AZURE_OPENAI:
return SpringUtil.getBean(AzureOpenAiChatModel.class);
case OLLAMA:
return SpringUtil.getBean(OllamaChatModel.class);
default:
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
}
}
@Override
public ImageModel getDefaultImageModel(AiPlatformEnum platform) {
// noinspection EnhancedSwitchMigration
switch (platform) {
case TONG_YI:
return SpringUtil.getBean(DashScopeImageModel.class);
case YI_YAN:
return SpringUtil.getBean(QianFanImageModel.class);
case ZHI_PU:
return SpringUtil.getBean(ZhiPuAiImageModel.class);
case SILICON_FLOW:
return SpringUtil.getBean(SiliconFlowImageModel.class);
case OPENAI:
return SpringUtil.getBean(OpenAiImageModel.class);
case STABLE_DIFFUSION:
return SpringUtil.getBean(StabilityAiImageModel.class);
default:
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
}
}
@Override
public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url) {
// noinspection EnhancedSwitchMigration
switch (platform) {
case TONG_YI:
return buildTongYiImagesModel(apiKey);
case YI_YAN:
return buildQianFanImageModel(apiKey);
case ZHI_PU:
return buildZhiPuAiImageModel(apiKey, url);
case OPENAI:
return buildOpenAiImageModel(apiKey, url);
case SILICON_FLOW:
return buildSiliconFlowImageModel(apiKey,url);
case STABLE_DIFFUSION:
return buildStabilityAiImageModel(apiKey, url);
default:
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
}
}
@Override
public MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url) {
String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey,
url);
return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> {
YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class)
.getMidjourney();
return new MidjourneyApi(url, apiKey, properties.getNotifyUrl());
});
}
@Override
public SunoApi getOrCreateSunoApi(String apiKey, String url) {
String cacheKey = buildClientCacheKey(SunoApi.class, AiPlatformEnum.SUNO.getPlatform(), apiKey, url);
return Singleton.get(cacheKey, (Func0<SunoApi>) () -> new SunoApi(url));
}
@Override
@SuppressWarnings("EnhancedSwitchMigration")
public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url, String model) {
String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url, model);
return Singleton.get(cacheKey, (Func0<EmbeddingModel>) () -> {
switch (platform) {
case TONG_YI:
return buildTongYiEmbeddingModel(apiKey, model);
case YI_YAN:
return buildYiYanEmbeddingModel(apiKey, model);
case ZHI_PU:
return buildZhiPuEmbeddingModel(apiKey, url, model);
case MINI_MAX:
return buildMiniMaxEmbeddingModel(apiKey, url, model);
case OPENAI:
return buildOpenAiEmbeddingModel(apiKey, url, model);
case AZURE_OPENAI:
return buildAzureOpenAiEmbeddingModel(apiKey, url, model);
case OLLAMA:
return buildOllamaEmbeddingModel(url, model);
default:
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
}
});
}
@Override
public VectorStore getOrCreateVectorStore(Class<? extends VectorStore> type,
EmbeddingModel embeddingModel,
Map<String, Class<?>> metadataFields) {
String cacheKey = buildClientCacheKey(VectorStore.class, embeddingModel, type);
return Singleton.get(cacheKey, (Func0<VectorStore>) () -> {
if (type == SimpleVectorStore.class) {
return buildSimpleVectorStore(embeddingModel);
}
if (type == QdrantVectorStore.class) {
return buildQdrantVectorStore(embeddingModel);
}
if (type == RedisVectorStore.class) {
return buildRedisVectorStore(embeddingModel, metadataFields);
}
if (type == MilvusVectorStore.class) {
return buildMilvusVectorStore(embeddingModel);
}
throw new IllegalArgumentException(StrUtil.format("未知类型({})", type));
});
}
private static String buildClientCacheKey(Class<?> clazz, Object... params) {
if (ArrayUtil.isEmpty(params)) {
return clazz.getName();
}
return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_"));
}
// ========== 各种创建 spring-ai 客户端的方法 ==========
/**
* 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeChatModel 方法
*/
private static DashScopeChatModel buildTongYiChatModel(String key) {
DashScopeApi dashScopeApi = new DashScopeApi(key);
DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL)
.withTemperature(0.7).build();
return new DashScopeChatModel(dashScopeApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
}
/**
* 可参考 {@link DashScopeAutoConfiguration} 的 dashScopeImageModel 方法
*/
private static DashScopeImageModel buildTongYiImagesModel(String key) {
DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key);
return new DashScopeImageModel(dashScopeImageApi);
}
/**
* 可参考 {@link QianFanAutoConfiguration} 的 qianFanChatModel 方法
*/
private static QianFanChatModel buildYiYanChatModel(String key) {
List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
String appKey = keys.get(0);
String secretKey = keys.get(1);
QianFanApi qianFanApi = new QianFanApi(appKey, secretKey);
return new QianFanChatModel(qianFanApi);
}
/**
* 可参考 {@link QianFanAutoConfiguration} 的 qianFanImageModel 方法
*/
private QianFanImageModel buildQianFanImageModel(String key) {
List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
String appKey = keys.get(0);
String secretKey = keys.get(1);
QianFanImageApi qianFanApi = new QianFanImageApi(appKey, secretKey);
return new QianFanImageModel(qianFanApi);
}
/**
* 可参考 {@link YudaoAiAutoConfiguration#deepSeekChatModel(YudaoAiProperties)}
*/
private static DeepSeekChatModel buildDeepSeekChatModel(String apiKey) {
YudaoAiProperties.DeepSeekProperties properties = new YudaoAiProperties.DeepSeekProperties()
.setApiKey(apiKey);
return new YudaoAiAutoConfiguration().buildDeepSeekChatModel(properties);
}
/**
* 可参考 {@link YudaoAiAutoConfiguration#douBaoChatClient(YudaoAiProperties)}
*/
private ChatModel buildDouBaoChatModel(String apiKey) {
YudaoAiProperties.DouBaoProperties properties = new YudaoAiProperties.DouBaoProperties()
.setApiKey(apiKey);
return new YudaoAiAutoConfiguration().buildDouBaoChatClient(properties);
}
/**
* 可参考 {@link YudaoAiAutoConfiguration#hunYuanChatClient(YudaoAiProperties)}
*/
private ChatModel buildHunYuanChatModel(String apiKey, String url) {
YudaoAiProperties.HunYuanProperties properties = new YudaoAiProperties.HunYuanProperties()
.setBaseUrl(url).setApiKey(apiKey);
return new YudaoAiAutoConfiguration().buildHunYuanChatClient(properties);
}
/**
* 可参考 {@link YudaoAiAutoConfiguration#siliconFlowChatClient(YudaoAiProperties)}
*/
private ChatModel buildSiliconFlowChatModel(String apiKey) {
YudaoAiProperties.SiliconFlowProperties properties = new YudaoAiProperties.SiliconFlowProperties()
.setApiKey(apiKey);
return new YudaoAiAutoConfiguration().buildSiliconFlowChatClient(properties);
}
/**
* 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiChatModel 方法
*/
private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) {
ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey)
: new ZhiPuAiApi(url, apiKey);
ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
return new ZhiPuAiChatModel(zhiPuAiApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
}
/**
* 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiImageModel 方法
*/
private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) {
ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey)
: new ZhiPuAiImageApi(url, apiKey, RestClient.builder());
return new ZhiPuAiImageModel(zhiPuAiApi);
}
/**
* 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxChatModel 方法
*/
private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) {
MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey)
: new MiniMaxApi(url, apiKey);
MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
return new MiniMaxChatModel(miniMaxApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
}
/**
* 可参考 {@link MoonshotAutoConfiguration} 的 moonshotChatModel 方法
*/
private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) {
MoonshotApi moonshotApi = StrUtil.isEmpty(url)? new MoonshotApi(apiKey)
: new MoonshotApi(url, apiKey);
MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build();
return new MoonshotChatModel(moonshotApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
}
/**
* 可参考 {@link YudaoAiAutoConfiguration#xingHuoChatClient(YudaoAiProperties)}
*/
private static XingHuoChatModel buildXingHuoChatModel(String key) {
List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式");
YudaoAiProperties.XingHuoProperties properties = new YudaoAiProperties.XingHuoProperties()
.setAppKey(keys.get(0)).setSecretKey(keys.get(1));
return new YudaoAiAutoConfiguration().buildXingHuoChatClient(properties);
}
/**
* 可参考 {@link YudaoAiAutoConfiguration#baiChuanChatClient(YudaoAiProperties)}
*/
private BaiChuanChatModel buildBaiChuanChatModel(String apiKey) {
YudaoAiProperties.BaiChuanProperties properties = new YudaoAiProperties.BaiChuanProperties()
.setApiKey(apiKey);
return new YudaoAiAutoConfiguration().buildBaiChuanChatClient(properties);
}
/**
* 可参考 {@link OpenAiAutoConfiguration} 的 openAiChatModel 方法
*/
private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) {
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build();
return OpenAiChatModel.builder().openAiApi(openAiApi).toolCallingManager(getToolCallingManager()).build();
}
// TODO @芋艿:手头暂时没密钥,使用建议再测试下
/**
* 可参考 {@link AzureOpenAiAutoConfiguration}
*/
private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) {
AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration();
// 创建 OpenAIClient 对象
AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties();
connectionProperties.setApiKey(apiKey);
connectionProperties.setEndpoint(url);
OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null);
// 获取 AzureOpenAiChatProperties 对象
AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class);
return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties,
getToolCallingManager(), null, null);
}
/**
* 可参考 {@link OpenAiAutoConfiguration} 的 openAiImageModel 方法
*/
private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) {
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
OpenAiImageApi openAiApi = OpenAiImageApi.builder().baseUrl(url).apiKey(openAiToken).build();
return new OpenAiImageModel(openAiApi);
}
/**
* 创建 SiliconFlowImageModel 对象
*/
private SiliconFlowImageModel buildSiliconFlowImageModel(String apiToken, String url) {
url = StrUtil.blankToDefault(url, SiliconFlowApiConstants.DEFAULT_BASE_URL);
SiliconFlowImageApi openAiApi = new SiliconFlowImageApi(url, apiToken);
return new SiliconFlowImageModel(openAiApi);
}
/**
* 可参考 {@link OllamaAutoConfiguration} 的 ollamaApi 方法
*/
private static OllamaChatModel buildOllamaChatModel(String url) {
OllamaApi ollamaApi = new OllamaApi(url);
return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build();
}
/**
* 可参考 {@link StabilityAiImageAutoConfiguration} 的 stabilityAiImageModel 方法
*/
private StabilityAiImageModel buildStabilityAiImageModel(String apiKey, String url) {
url = StrUtil.blankToDefault(url, StabilityAiApi.DEFAULT_BASE_URL);
StabilityAiApi stabilityAiApi = new StabilityAiApi(apiKey, StabilityAiApi.DEFAULT_IMAGE_MODEL, url);
return new StabilityAiImageModel(stabilityAiApi);
}
// ========== 各种创建 EmbeddingModel 的方法 ==========
/**
* 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeEmbeddingModel 方法
*/
private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) {
DashScopeApi dashScopeApi = new DashScopeApi(apiKey);
DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model).build();
return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions);
}
/**
* 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法
*/
private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) {
ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey)
: new ZhiPuAiApi(url, apiKey);
ZhiPuAiEmbeddingOptions zhiPuAiEmbeddingOptions = ZhiPuAiEmbeddingOptions.builder().model(model).build();
return new ZhiPuAiEmbeddingModel(zhiPuAiApi, MetadataMode.EMBED, zhiPuAiEmbeddingOptions);
}
/**
* 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxEmbeddingModel 方法
*/
private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) {
MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey)
: new MiniMaxApi(url, apiKey);
MiniMaxEmbeddingOptions miniMaxEmbeddingOptions = MiniMaxEmbeddingOptions.builder().model(model).build();
return new MiniMaxEmbeddingModel(miniMaxApi, MetadataMode.EMBED, miniMaxEmbeddingOptions);
}
/**
* 可参考 {@link QianFanAutoConfiguration} 的 qianFanEmbeddingModel 方法
*/
private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) {
List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
String appKey = keys.get(0);
String secretKey = keys.get(1);
QianFanApi qianFanApi = new QianFanApi(appKey, secretKey);
QianFanEmbeddingOptions qianFanEmbeddingOptions = QianFanEmbeddingOptions.builder().model(model).build();
return new QianFanEmbeddingModel(qianFanApi, MetadataMode.EMBED, qianFanEmbeddingOptions);
}
private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) {
OllamaApi ollamaApi = new OllamaApi(url);
OllamaOptions ollamaOptions = OllamaOptions.builder().model(model).build();
return OllamaEmbeddingModel.builder().ollamaApi(ollamaApi).defaultOptions(ollamaOptions).build();
}
/**
* 可参考 {@link OpenAiAutoConfiguration} 的 openAiEmbeddingModel 方法
*/
private OpenAiEmbeddingModel buildOpenAiEmbeddingModel(String openAiToken, String url, String model) {
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build();
OpenAiEmbeddingOptions openAiEmbeddingProperties = OpenAiEmbeddingOptions.builder().model(model).build();
return new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED, openAiEmbeddingProperties);
}
// TODO @芋艿:手头暂时没密钥,使用建议再测试下
/**
* 可参考 {@link AzureOpenAiAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法
*/
private AzureOpenAiEmbeddingModel buildAzureOpenAiEmbeddingModel(String apiKey, String url, String model) {
AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration();
// 创建 OpenAIClient 对象
AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties();
connectionProperties.setApiKey(apiKey);
connectionProperties.setEndpoint(url);
OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null);
// 获取 AzureOpenAiChatProperties 对象
AzureOpenAiEmbeddingProperties embeddingProperties = SpringUtil.getBean(AzureOpenAiEmbeddingProperties.class);
return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClient, embeddingProperties,
null, null);
}
// ========== 各种创建 VectorStore 的方法 ==========
/**
* 注意:仅适合本地测试使用,生产建议还是使用 Qdrant、Milvus 等
*/
@SneakyThrows
@SuppressWarnings("ResultOfMethodCallIgnored")
private SimpleVectorStore buildSimpleVectorStore(EmbeddingModel embeddingModel) {
SimpleVectorStore vectorStore = SimpleVectorStore.builder(embeddingModel).build();
// 启动加载
File file = new File(StrUtil.format("{}/vector_store/simple_{}.json",
FileUtil.getUserHomePath(), embeddingModel.getClass().getSimpleName()));
if (!file.exists()) {
FileUtil.mkParentDirs(file);
file.createNewFile();
} else if (file.length() > 0) {
vectorStore.load(file);
}
// 定时持久化,每分钟一次
Timer timer = new Timer("SimpleVectorStoreTimer-" + file.getAbsolutePath());
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
vectorStore.save(file);
}
}, Duration.ofMinutes(1).toMillis(), Duration.ofMinutes(1).toMillis());
// 关闭时,进行持久化
RuntimeUtil.addShutdownHook(() -> vectorStore.save(file));
return vectorStore;
}
/**
* 参考 {@link QdrantVectorStoreAutoConfiguration} 的 vectorStore 方法
*/
@SneakyThrows
private QdrantVectorStore buildQdrantVectorStore(EmbeddingModel embeddingModel) {
QdrantVectorStoreAutoConfiguration configuration = new QdrantVectorStoreAutoConfiguration();
QdrantVectorStoreProperties properties = SpringUtil.getBean(QdrantVectorStoreProperties.class);
// 参考 QdrantVectorStoreAutoConfiguration 实现,创建 QdrantClient 对象
QdrantGrpcClient.Builder grpcClientBuilder = QdrantGrpcClient.newBuilder(
properties.getHost(), properties.getPort(), properties.isUseTls());
if (StrUtil.isNotEmpty(properties.getApiKey())) {
grpcClientBuilder.withApiKey(properties.getApiKey());
}
QdrantClient qdrantClient = new QdrantClient(grpcClientBuilder.build());
// 创建 QdrantVectorStore 对象
QdrantVectorStore vectorStore = configuration.vectorStore(embeddingModel, properties, qdrantClient,
getObservationRegistry(), getCustomObservationConvention(), getBatchingStrategy());
// 初始化索引
vectorStore.afterPropertiesSet();
return vectorStore;
}
/**
* 参考 {@link RedisVectorStoreAutoConfiguration} 的 vectorStore 方法
*/
private RedisVectorStore buildRedisVectorStore(EmbeddingModel embeddingModel,
Map<String, Class<?>> metadataFields) {
// 创建 JedisPooled 对象
RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class);
JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort());
// 创建 RedisVectorStoreProperties 对象
RedisVectorStoreAutoConfiguration configuration = new RedisVectorStoreAutoConfiguration();
RedisVectorStoreProperties properties = SpringUtil.getBean(RedisVectorStoreProperties.class);
RedisVectorStore redisVectorStore = RedisVectorStore.builder(jedisPooled, embeddingModel)
.indexName(properties.getIndex()).prefix(properties.getPrefix())
.initializeSchema(properties.isInitializeSchema())
.metadataFields(convertList(metadataFields.entrySet(), entry -> {
String fieldName = entry.getKey();
Class<?> fieldType = entry.getValue();
if (Number.class.isAssignableFrom(fieldType)) {
return RedisVectorStore.MetadataField.numeric(fieldName);
}
if (Boolean.class.isAssignableFrom(fieldType)) {
return RedisVectorStore.MetadataField.tag(fieldName);
}
return RedisVectorStore.MetadataField.text(fieldName);
}))
.observationRegistry(getObservationRegistry().getObject())
.customObservationConvention(getCustomObservationConvention().getObject())
.batchingStrategy(getBatchingStrategy())
.build();
// 初始化索引
redisVectorStore.afterPropertiesSet();
return redisVectorStore;
}
/**
* 参考 {@link MilvusVectorStoreAutoConfiguration} 的 vectorStore 方法
*/
@SneakyThrows
private MilvusVectorStore buildMilvusVectorStore(EmbeddingModel embeddingModel) {
MilvusVectorStoreAutoConfiguration configuration = new MilvusVectorStoreAutoConfiguration();
// 获取配置属性
MilvusVectorStoreProperties serverProperties = SpringUtil.getBean(MilvusVectorStoreProperties.class);
MilvusServiceClientProperties clientProperties = SpringUtil.getBean(MilvusServiceClientProperties.class);
// 创建 MilvusServiceClient 对象
MilvusServiceClient milvusClient = configuration.milvusClient(serverProperties, clientProperties,
new MilvusServiceClientConnectionDetails() {
@Override
public String getHost() {
return clientProperties.getHost();
}
@Override
public int getPort() {
return clientProperties.getPort();
}
}
);
// 创建 MilvusVectorStore 对象
MilvusVectorStore vectorStore = configuration.vectorStore(milvusClient, embeddingModel, serverProperties,
getBatchingStrategy(), getObservationRegistry(), getCustomObservationConvention());
// 初始化索引
vectorStore.afterPropertiesSet();
return vectorStore;
}
private static ObjectProvider<ObservationRegistry> getObservationRegistry() {
return new ObjectProvider<>() {
@Override
public ObservationRegistry getObject() throws BeansException {
return SpringUtil.getBean(ObservationRegistry.class);
}
};
}
private static ObjectProvider<VectorStoreObservationConvention> getCustomObservationConvention() {
return new ObjectProvider<>() {
@Override
public VectorStoreObservationConvention getObject() throws BeansException {
return new DefaultVectorStoreObservationConvention();
}
};
}
private static BatchingStrategy getBatchingStrategy() {
return SpringUtil.getBean(BatchingStrategy.class);
}
private static ToolCallingManager getToolCallingManager() {
return SpringUtil.getBean(ToolCallingManager.class);
}
private static FunctionCallbackResolver getFunctionCallbackResolver() {
return SpringUtil.getBean(FunctionCallbackResolver.class);
}
}
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.framework.ai.core.model.baichuan;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux;
/**
* 百川 {@link ChatModel} 实现类
*
* @author 芋道源码
*/
@Slf4j
@RequiredArgsConstructor
public class BaiChuanChatModel implements ChatModel {
public static final String BASE_URL = "https://api.baichuan-ai.com";
public static final String MODEL_DEFAULT = "Baichuan4-Turbo";
/**
* 兼容 OpenAI 接口,进行复用
*/
private final OpenAiChatModel openAiChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
}
}
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.framework.ai.core.model.deepseek;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux;
/**
* DeepSeek {@link ChatModel} 实现类
*
* @author fansili
*/
@Slf4j
@RequiredArgsConstructor
public class DeepSeekChatModel implements ChatModel {
public static final String BASE_URL = "https://api.deepseek.com";
public static final String MODEL_DEFAULT = "deepseek-chat";
/**
* 兼容 OpenAI 接口,进行复用
*/
private final OpenAiChatModel openAiChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
}
}
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.framework.ai.core.model.doubao;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux;
/**
* 字节豆包 {@link ChatModel} 实现类
*
* @author fansili
*/
@Slf4j
@RequiredArgsConstructor
public class DouBaoChatModel implements ChatModel {
public static final String BASE_URL = "https://ark.cn-beijing.volces.com/api";
public static final String MODEL_DEFAULT = "doubao-1-5-lite-32k-250115";
/**
* 兼容 OpenAI 接口,进行复用
*/
private final OpenAiChatModel openAiChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
}
}
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.framework.ai.core.model.hunyuan;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux;
/**
* 腾云混元 {@link ChatModel} 实现类
*
* 1. 混元大模型:基于 <a href="https://cloud.tencent.com/document/product/1729/111007">知识引擎原子能力</a> 实现
* 2. 知识引擎原子能力:基于 <a href="https://cloud.tencent.com/document/product/1772/115969">知识引擎原子能力</a> 实现
*
* @author fansili
*/
@Slf4j
@RequiredArgsConstructor
public class HunYuanChatModel implements ChatModel {
public static final String BASE_URL = "https://api.hunyuan.cloud.tencent.com";
public static final String MODEL_DEFAULT = "hunyuan-turbo";
public static final String DEEP_SEEK_BASE_URL = "https://api.lkeap.cloud.tencent.com";
public static final String DEEP_SEEK_MODEL_DEFAULT = "deepseek-v3";
/**
* 兼容 OpenAI 接口,进行复用
*/
private final OpenAiChatModel openAiChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
}
}
@@ -0,0 +1,351 @@
package cn.iocoder.yudao.framework.ai.core.model.midjourney.api;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* Midjourney API
*
* @author fansili
* @since 1.0
*/
@Slf4j
public class MidjourneyApi {
private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful();
private final Function<Object, Function<ClientResponse, Mono<? extends Throwable>>> EXCEPTION_FUNCTION =
reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> {
HttpRequest request = response.request();
log.error("[midjourney-api] 调用失败!请求方式:[{}],请求地址:[{}],请求参数:[{}],响应数据: [{}]",
request.getMethod(), request.getURI(), reqParam, responseBody);
sink.error(new IllegalStateException("[midjourney-api] 调用失败!"));
});
private final WebClient webClient;
/**
* 回调地址
*/
private final String notifyUrl;
public MidjourneyApi(String baseUrl, String apiKey, String notifyUrl) {
this.webClient = WebClient.builder()
.baseUrl(baseUrl)
.defaultHeaders(httpHeaders -> {
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
httpHeaders.setBearerAuth(apiKey);
})
.build();
this.notifyUrl = notifyUrl;
}
/**
* imagine - 根据提示词提交绘画任务
*
* @param request 请求
* @return 提交结果
*/
public SubmitResponse imagine(ImagineRequest request) {
if (StrUtil.isEmpty(request.getNotifyHook())) {
request.setNotifyHook(notifyUrl);
}
String response = post("/submit/imagine", request);
return JsonUtils.parseObject(response, SubmitResponse.class);
}
/**
* action - 放大、缩小、U1、U2...
*
* @param request 请求
* @return 提交结果
*/
public SubmitResponse action(ActionRequest request) {
if (StrUtil.isEmpty(request.getNotifyHook())) {
request.setNotifyHook(notifyUrl);
}
String response = post("/submit/action", request);
return JsonUtils.parseObject(response, SubmitResponse.class);
}
/**
* 批量查询 task 任务
*
* @param ids 任务编号数组
* @return task 任务
*/
public List<Notify> getTaskList(Collection<String> ids) {
String res = post("/task/list-by-condition", ImmutableMap.of("ids", ids));
return JsonUtils.parseArray(res, Notify.class);
}
private String post(String uri, Object body) {
return webClient.post()
.uri(uri)
.body(Mono.just(JsonUtils.toJsonString(body)), String.class)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(body))
.bodyToMono(String.class)
.block();
}
// ========== record 结构 ==========
/**
* Imagine 请求(生成图片)
*/
@Data
public static final class ImagineRequest {
/**
* 垫图(参考图) base64 数组
*/
private List<String> base64Array;
/**
* 提示词
*/
private String prompt;
/**
* 通知地址
*/
private String notifyHook;
/**
* 自定义参数
*/
private String state;
public ImagineRequest(List<String> base64Array, String prompt, String notifyHook, String state) {
this.base64Array = base64Array;
this.prompt = prompt;
this.notifyHook = notifyHook;
this.state = state;
}
public static String buildState(Integer width, Integer height, String version, String model) {
StringBuilder params = new StringBuilder();
// --ar 来设置尺寸
params.append(String.format(" --ar %s:%s ", width, height));
// --niji 模型
if (ModelEnum.NIJI.getModel().equals(model)) {
params.append(String.format(" --niji %s ", version));
} else {
params.append(String.format(" --v %s ", version));
}
return params.toString();
}
}
/**
* Action 请求
*/
@Data
public static final class ActionRequest {
private String customId;
private String taskId;
private String notifyHook;
public ActionRequest(String taskId, String customId, String notifyHook) {
this.customId = customId;
this.taskId = taskId;
this.notifyHook = notifyHook;
}
}
/**
* Submit 统一返回
*
* @param code 状态码: 1(提交成功), 21(已存在), 22(排队中), other(错误)
* @param description 描述
* @param properties 扩展字段
* @param result 任务ID
*/
public record SubmitResponse(String code,
String description,
Map<String, Object> properties,
String result) {
}
/**
* 通知 request
*
* @param id job id
* @param action 任务类型 {@link TaskActionEnum}
* @param status 任务状态 {@link TaskStatusEnum}
* @param prompt 提示词
* @param promptEn 提示词-英文
* @param description 任务描述
* @param state 自定义参数
* @param submitTime 提交时间
* @param startTime 开始执行时间
* @param finishTime 结束时间
* @param imageUrl 图片url
* @param progress 任务进度
* @param failReason 失败原因
* @param buttons 任务完成后的可执行按钮
*/
public record Notify(String id,
String action,
String status,
String prompt,
String promptEn,
String description,
String state,
Long submitTime,
Long startTime,
Long finishTime,
String imageUrl,
String progress,
String failReason,
List<Button> buttons) {
}
/**
* button
*
* @param customId MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识
* @param emoji 图标 emoji
* @param label Make Variations 文本
* @param type 类型,系统内部使用
* @param style 样式: 2(Primary)、3(Green)
*/
public record Button(String customId,
String emoji,
String label,
String type,
String style) {
}
// ============ enums ============
/**
* 模型枚举
*/
@AllArgsConstructor
@Getter
public enum ModelEnum {
MIDJOURNEY("midjourney", "midjourney"),
NIJI("niji", "niji"),
;
private final String model;
private final String name;
}
/**
* 提交返回的状态码的枚举
*/
@Getter
@AllArgsConstructor
public enum SubmitCodeEnum {
SUBMIT_SUCCESS("1", "提交成功"),
ALREADY_EXISTS("21", "已存在"),
QUEUING("22", "排队中"),
;
public static final List<String> SUCCESS_CODES = Lists.newArrayList(
SUBMIT_SUCCESS.code,
ALREADY_EXISTS.code,
QUEUING.code
);
private final String code;
private final String name;
}
/**
* Action 枚举
*/
@Getter
@AllArgsConstructor
public enum TaskActionEnum {
/**
* 生成图片
*/
IMAGINE,
/**
* 选中放大
*/
UPSCALE,
/**
* 选中其中的一张图,生成四张相似的
*/
VARIATION,
/**
* 重新执行
*/
REROLL,
/**
* 图转 prompt
*/
DESCRIBE,
/**
* 多图混合
*/
BLEND
}
/**
* 任务状态枚举
*/
@Getter
@AllArgsConstructor
public enum TaskStatusEnum {
/**
* 未启动
*/
NOT_START(0),
/**
* 已提交
*/
SUBMITTED(1),
/**
* 执行中
*/
IN_PROGRESS(3),
/**
* 失败
*/
FAILURE(4),
/**
* 成功
*/
SUCCESS(4);
private final int order;
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.iocoder.yudao.framework.ai.core.model.siliconflow;
/**
* SiliconFlow API 枚举类
*
* @author zzt
*/
public final class SiliconFlowApiConstants {
public static final String DEFAULT_BASE_URL = "https://api.siliconflow.cn";
public static final String MODEL_DEFAULT = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B";
public static final String DEFAULT_IMAGE_MODEL = "Kwai-Kolors/Kolors";
public static final String PROVIDER_NAME = "Siiconflow";
}
@@ -0,0 +1,43 @@
package cn.iocoder.yudao.framework.ai.core.model.siliconflow;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux;
/**
* 硅基流动 {@link ChatModel} 实现类
*
* 1. API 文档:<a href="https://docs.siliconflow.cn/cn/api-reference/chat-completions/chat-completions">API 文档</a>
*
* @author fansili
*/
@Slf4j
@RequiredArgsConstructor
public class SiliconFlowChatModel implements ChatModel {
/**
* 兼容 OpenAI 接口,进行复用
*/
private final OpenAiChatModel openAiChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
}
}
@@ -0,0 +1,115 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.iocoder.yudao.framework.ai.core.model.siliconflow;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.ai.model.ApiKey;
import org.springframework.ai.model.NoopApiKey;
import org.springframework.ai.model.SimpleApiKey;
import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient;
import java.util.Map;
/**
* 硅基流动 Image API
*
* @see <a href= "https://docs.siliconflow.cn/cn/api-reference/images/images-generations">Images</a>
*
* @author zzt
*/
public class SiliconFlowImageApi {
private final RestClient restClient;
public SiliconFlowImageApi(String aiToken) {
this(SiliconFlowApiConstants.DEFAULT_BASE_URL, aiToken, RestClient.builder());
}
public SiliconFlowImageApi(String baseUrl, String openAiToken) {
this(baseUrl, openAiToken, RestClient.builder());
}
public SiliconFlowImageApi(String baseUrl, String openAiToken, RestClient.Builder restClientBuilder) {
this(baseUrl, openAiToken, restClientBuilder, RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER);
}
public SiliconFlowImageApi(String baseUrl, String apiKey, RestClient.Builder restClientBuilder,
ResponseErrorHandler responseErrorHandler) {
this(baseUrl, apiKey, CollectionUtils.toMultiValueMap(Map.of()), restClientBuilder, responseErrorHandler);
}
public SiliconFlowImageApi(String baseUrl, String apiKey, MultiValueMap<String, String> headers,
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
this(baseUrl, new SimpleApiKey(apiKey), headers, restClientBuilder, responseErrorHandler);
}
public SiliconFlowImageApi(String baseUrl, ApiKey apiKey, MultiValueMap<String, String> headers,
RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
// @formatter:off
this.restClient = restClientBuilder.baseUrl(baseUrl)
.defaultHeaders(h -> {
if(!(apiKey instanceof NoopApiKey)) {
h.setBearerAuth(apiKey.getValue());
}
h.setContentType(MediaType.APPLICATION_JSON);
h.addAll(headers);
})
.defaultStatusHandler(responseErrorHandler)
.build();
// @formatter:on
}
public ResponseEntity<OpenAiImageApi.OpenAiImageResponse> createImage(SiliconflowImageRequest siliconflowImageRequest) {
Assert.notNull(siliconflowImageRequest, "Image request cannot be null.");
Assert.hasLength(siliconflowImageRequest.prompt(), "Prompt cannot be empty.");
return this.restClient.post()
.uri("v1/images/generations")
.body(siliconflowImageRequest)
.retrieve()
.toEntity(OpenAiImageApi.OpenAiImageResponse.class);
}
// @formatter:off
@JsonInclude(JsonInclude.Include.NON_NULL)
public record SiliconflowImageRequest (
@JsonProperty("prompt") String prompt,
@JsonProperty("model") String model,
@JsonProperty("batch_size") Integer batchSize,
@JsonProperty("negative_prompt") String negativePrompt,
@JsonProperty("seed") Integer seed,
@JsonProperty("num_inference_steps") Integer numInferenceSteps,
@JsonProperty("guidance_scale") Float guidanceScale,
@JsonProperty("image") String image) {
public SiliconflowImageRequest(String prompt, String model) {
this(prompt, model, null, null, null, null, null, null);
}
}
}
@@ -0,0 +1,159 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.iocoder.yudao.framework.ai.core.model.siliconflow;
import io.micrometer.observation.ObservationRegistry;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.image.*;
import org.springframework.ai.image.observation.DefaultImageModelObservationConvention;
import org.springframework.ai.image.observation.ImageModelObservationContext;
import org.springframework.ai.image.observation.ImageModelObservationConvention;
import org.springframework.ai.image.observation.ImageModelObservationDocumentation;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.openai.OpenAiImageModel;
import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.ai.openai.metadata.OpenAiImageGenerationMetadata;
import org.springframework.ai.retry.RetryUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
import java.util.List;
/**
* 硅基流动 {@link ImageModel} 实现类
*
* 参考 {@link OpenAiImageModel} 实现
*
* @author zzt
*/
public class SiliconFlowImageModel implements ImageModel {
private static final Logger logger = LoggerFactory.getLogger(SiliconFlowImageModel.class);
private static final ImageModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultImageModelObservationConvention();
private final SiliconFlowImageOptions defaultOptions;
private final RetryTemplate retryTemplate;
private final SiliconFlowImageApi siliconFlowImageApi;
private final ObservationRegistry observationRegistry;
@Setter
private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;
public SiliconFlowImageModel(SiliconFlowImageApi siliconFlowImageApi) {
this(siliconFlowImageApi, SiliconFlowImageOptions.builder().build(), RetryUtils.DEFAULT_RETRY_TEMPLATE);
}
public SiliconFlowImageModel(SiliconFlowImageApi siliconFlowImageApi, SiliconFlowImageOptions options, RetryTemplate retryTemplate) {
this(siliconFlowImageApi, options, retryTemplate, ObservationRegistry.NOOP);
}
public SiliconFlowImageModel(SiliconFlowImageApi siliconFlowImageApi, SiliconFlowImageOptions options, RetryTemplate retryTemplate,
ObservationRegistry observationRegistry) {
Assert.notNull(siliconFlowImageApi, "OpenAiImageApi must not be null");
Assert.notNull(options, "options must not be null");
Assert.notNull(retryTemplate, "retryTemplate must not be null");
Assert.notNull(observationRegistry, "observationRegistry must not be null");
this.siliconFlowImageApi = siliconFlowImageApi;
this.defaultOptions = options;
this.retryTemplate = retryTemplate;
this.observationRegistry = observationRegistry;
}
@Override
public ImageResponse call(ImagePrompt imagePrompt) {
SiliconFlowImageOptions requestImageOptions = mergeOptions(imagePrompt.getOptions(), this.defaultOptions);
SiliconFlowImageApi.SiliconflowImageRequest imageRequest = createRequest(imagePrompt, requestImageOptions);
var observationContext = ImageModelObservationContext.builder()
.imagePrompt(imagePrompt)
.provider(SiliconFlowApiConstants.PROVIDER_NAME)
.requestOptions(imagePrompt.getOptions())
.build();
return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION
.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
this.observationRegistry)
.observe(() -> {
ResponseEntity<OpenAiImageApi.OpenAiImageResponse> imageResponseEntity = this.retryTemplate
.execute(ctx -> this.siliconFlowImageApi.createImage(imageRequest));
ImageResponse imageResponse = convertResponse(imageResponseEntity, imageRequest);
observationContext.setResponse(imageResponse);
return imageResponse;
});
}
private SiliconFlowImageApi.SiliconflowImageRequest createRequest(ImagePrompt imagePrompt,
SiliconFlowImageOptions requestImageOptions) {
String instructions = imagePrompt.getInstructions().get(0).getText();
SiliconFlowImageApi.SiliconflowImageRequest imageRequest = new SiliconFlowImageApi.SiliconflowImageRequest(instructions,
SiliconFlowApiConstants.DEFAULT_IMAGE_MODEL);
return ModelOptionsUtils.merge(requestImageOptions, imageRequest, SiliconFlowImageApi.SiliconflowImageRequest.class);
}
private ImageResponse convertResponse(ResponseEntity<OpenAiImageApi.OpenAiImageResponse> imageResponseEntity,
SiliconFlowImageApi.SiliconflowImageRequest siliconflowImageRequest) {
OpenAiImageApi.OpenAiImageResponse imageApiResponse = imageResponseEntity.getBody();
if (imageApiResponse == null) {
logger.warn("No image response returned for request: {}", siliconflowImageRequest);
return new ImageResponse(List.of());
}
List<ImageGeneration> imageGenerationList = imageApiResponse.data()
.stream()
.map(entry -> new ImageGeneration(new Image(entry.url(), entry.b64Json()),
new OpenAiImageGenerationMetadata(entry.revisedPrompt())))
.toList();
ImageResponseMetadata openAiImageResponseMetadata = new ImageResponseMetadata(imageApiResponse.created());
return new ImageResponse(imageGenerationList, openAiImageResponseMetadata);
}
private SiliconFlowImageOptions mergeOptions(@Nullable ImageOptions runtimeOptions, SiliconFlowImageOptions defaultOptions) {
var runtimeOptionsForProvider = ModelOptionsUtils.copyToTarget(runtimeOptions, ImageOptions.class,
SiliconFlowImageOptions.class);
if (runtimeOptionsForProvider == null) {
return defaultOptions;
}
return SiliconFlowImageOptions.builder()
// Handle portable image options
.model(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getModel(), defaultOptions.getModel()))
.batchSize(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getN(), defaultOptions.getN()))
.width(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getWidth(), defaultOptions.getWidth()))
.height(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getHeight(), defaultOptions.getHeight()))
// Handle SiliconFlow specific image options
.negativePrompt(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getNegativePrompt(), defaultOptions.getNegativePrompt()))
.numInferenceSteps(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getNumInferenceSteps(), defaultOptions.getNumInferenceSteps()))
.guidanceScale(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getGuidanceScale(), defaultOptions.getGuidanceScale()))
.seed(ModelOptionsUtils.mergeOption(runtimeOptionsForProvider.getSeed(), defaultOptions.getSeed()))
.build();
}
}
@@ -0,0 +1,105 @@
package cn.iocoder.yudao.framework.ai.core.model.siliconflow;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.image.ImageOptions;
/**
* 硅基流动 {@link ImageOptions}
*
* @author zzt
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SiliconFlowImageOptions implements ImageOptions {
@JsonProperty("model")
private String model;
@JsonProperty("negative_prompt")
private String negativePrompt;
/**
* The number of images to generate. Must be between 1 and 4.
*/
@JsonProperty("image_size")
private String imageSize;
/**
* The number of images to generate. Must be between 1 and 4.
*/
@JsonProperty("batch_size")
private Integer batchSize = 1;
/**
* number of inference steps
*/
@JsonProperty("num_inference_steps")
private Integer numInferenceSteps = 25;
/**
* This value is used to control the degree of match between the generated image and the given prompt. The higher the value, the more the generated image will tend to strictly match the text prompt. The lower the value, the more creative and diverse the generated image will be, potentially containing more unexpected elements.
*
* Required range: 0 <= x <= 20
*/
@JsonProperty("guidance_scale")
private Float guidanceScale = 0.75F;
/**
* 如果想要每次都生成固定的图片,可以把 seed 设置为固定值
*
*/
@JsonProperty("seed")
private Integer seed = (int)(Math.random() * 1_000_000_000);
/**
* The image that needs to be uploaded should be converted into base64 format.
*/
@JsonProperty("image")
private String image;
/**
* 宽
*/
private Integer width;
/**
* 高
*/
private Integer height;
public void setHeight(Integer height) {
this.height = height;
if (this.width != null && this.height != null) {
this.imageSize = this.width + "x" + this.height;
}
}
public void setWidth(Integer width) {
this.width = width;
if (this.width != null && this.height != null) {
this.imageSize = this.width + "x" + this.height;
}
}
@Override
public Integer getN() {
return batchSize;
}
@Override
public String getResponseFormat() {
return "url";
}
@Override
public String getStyle() {
return null;
}
}
@@ -0,0 +1,200 @@
package cn.iocoder.yudao.framework.ai.core.model.suno.api;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.StrPool;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* Suno API
* <p>
* 对接 Suno Proxy:<a href="https://github.com/gcui-art/suno-api">suno-api</a>
*
* @author xiaoxin
*/
@Slf4j
public class SunoApi {
private final WebClient webClient;
private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful();
private final Function<Object, Function<ClientResponse, Mono<? extends Throwable>>> EXCEPTION_FUNCTION =
reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> {
HttpRequest request = response.request();
log.error("[suno-api] 调用失败!请求方式:[{}],请求地址:[{}],请求参数:[{}],响应数据: [{}]",
request.getMethod(), request.getURI(), reqParam, responseBody);
sink.error(new IllegalStateException("[suno-api] 调用失败!"));
});
public SunoApi(String baseUrl) {
this.webClient = WebClient.builder()
.baseUrl(baseUrl)
.defaultHeaders((headers) -> headers.setContentType(MediaType.APPLICATION_JSON))
.build();
}
public List<MusicData> generate(MusicGenerateRequest request) {
return this.webClient.post()
.uri("/api/generate")
.body(Mono.just(request), MusicGenerateRequest.class)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
.bodyToMono(new ParameterizedTypeReference<List<MusicData>>() {
})
.block();
}
public List<MusicData> customGenerate(MusicGenerateRequest request) {
return this.webClient.post()
.uri("/api/custom_generate")
.body(Mono.just(request), MusicGenerateRequest.class)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
.bodyToMono(new ParameterizedTypeReference<List<MusicData>>() {
})
.block();
}
public LyricsData generateLyrics(String prompt) {
return this.webClient.post()
.uri("/api/generate_lyrics")
.body(Mono.just(new MusicGenerateRequest(prompt)), MusicGenerateRequest.class)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(prompt))
.bodyToMono(LyricsData.class)
.block();
}
public List<MusicData> getMusicList(List<String> ids) {
return this.webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/api/get")
.queryParam("ids", CollUtil.join(ids, StrPool.COMMA))
.build())
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(ids))
.bodyToMono(new ParameterizedTypeReference<List<MusicData>>() {
})
.block();
}
public LimitUsageData getLimitUsage() {
return this.webClient.get()
.uri("/api/get_limit")
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(null))
.bodyToMono(LimitUsageData.class)
.block();
}
/**
* 根据提示生成音频
*
* @param prompt 用于生成音乐音频的提示
* @param tags 音乐风格
* @param title 音乐名称
* @param model 模型
* @param waitAudio false 表示后台模式,仅返回音频任务信息,需要调用 get API 获取详细的音频信息。
* true 表示同步模式,API 最多等待 100s,音频生成完毕后直接返回音频链接等信息,建议在 GPT 等 agent 中使用。
* @param makeInstrumental 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record MusicGenerateRequest(
String prompt,
String tags,
String title,
String model,
@JsonProperty("wait_audio") boolean waitAudio,
@JsonProperty("make_instrumental") boolean makeInstrumental
) {
public MusicGenerateRequest(String prompt) {
this(prompt, null, null, null, false, false);
}
public MusicGenerateRequest(String prompt, String model, boolean makeInstrumental) {
this(prompt, null, null, model, false, makeInstrumental);
}
public MusicGenerateRequest(String prompt, String model, String tags, String title) {
this(prompt, tags, title, model, false, false);
}
}
/**
* Suno API 响应的音频数据
*
* @param id 音乐数据的 ID
* @param title 音乐音频的标题
* @param imageUrl 音乐音频的图片 URL
* @param lyric 音乐音频的歌词
* @param audioUrl 音乐音频的 URL
* @param videoUrl 音乐视频的 URL
* @param createdAt 音乐音频的创建时间
* @param modelName 模型名称
* @param status submitted、queued、streaming、complete
* @param gptDescriptionPrompt 描述词
* @param prompt 生成音乐音频的提示
* @param type 操作类型
* @param tags 音乐类型标签
* @param duration 音乐时长
*/
public record MusicData(
String id,
String title,
@JsonProperty("image_url") String imageUrl,
String lyric,
@JsonProperty("audio_url") String audioUrl,
@JsonProperty("video_url") String videoUrl,
@JsonProperty("created_at") String createdAt,
@JsonProperty("model_name") String modelName,
String status,
@JsonProperty("gpt_description_prompt") String gptDescriptionPrompt,
@JsonProperty("error_message") String errorMessage,
String prompt,
String type,
String tags,
Double duration
) {
}
/**
* Suno API 响应的歌词数据。
*
* @param text 歌词
* @param title 标题
* @param status 状态
*/
public record LyricsData(
String text,
String title,
String status
) {
}
/**
* Suno API 响应的限额数据,目前每日免费 50
*/
public record LimitUsageData(
@JsonProperty("credits_left") Long creditsLeft,
String period,
@JsonProperty("monthly_limit") Long monthlyLimit,
@JsonProperty("monthly_usage") Long monthlyUsage
) {
}
}
@@ -0,0 +1,381 @@
package cn.iocoder.yudao.framework.ai.core.model.wenduoduo.api;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* 文多多 API
*
* @author xiaoxin
* @see <a href="https://docmee.cn/open-platform/api">PPT 生成 API</a>
*/
@Slf4j
public class WenDuoDuoPptApi {
public static final String BASE_URL = "https://docmee.cn";
public static final String TOKEN_NAME = "token";
private final WebClient webClient;
private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful();
private final Function<Object, Function<ClientResponse, Mono<? extends Throwable>>> EXCEPTION_FUNCTION =
reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> {
HttpRequest request = response.request();
log.error("[WenDuoDuoPptApi] 调用失败!请求方式:[{}],请求地址:[{}],请求参数:[{}],响应数据: [{}]",
request.getMethod(), request.getURI(), reqParam, responseBody);
sink.error(new IllegalStateException("[WenDuoDuoPptApi] 调用失败!"));
});
public WenDuoDuoPptApi(String token) {
Assert.hasText(token, "token 不能为空");
this.webClient = WebClient.builder()
.baseUrl(BASE_URL)
.defaultHeaders((headers) -> {
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add(TOKEN_NAME, token);
})
.build();
}
/**
* 创建 token
*
* @param request 请求信息
* @return token
*/
public String createApiToken(CreateTokenRequest request) {
return this.webClient.post()
.uri("/api/user/createApiToken")
.header("Api-Key", request.apiKey)
.body(Mono.just(request), CreateTokenRequest.class)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
.bodyToMono(ApiResponse.class)
.<String>handle((response, sink) -> {
if (response.code != 0) {
sink.error(new IllegalStateException("创建 token 异常," + response.message));
return;
}
sink.next(response.data.get("token").toString());
})
.block();
}
/**
* 创建任务
*
* @param type 类型
* @param content 内容
* @param files 文件列表
* @return 任务 ID
* @see <a href="https://docmee.cn/open-platform/api#%E5%88%9B%E5%BB%BA%E4%BB%BB%E5%8A%A1">创建任务</a>
*/
public ApiResponse createTask(Integer type, String content, List<MultipartFile> files) {
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
formData.add("type", type);
if (content != null) {
formData.add("content", content);
}
if (files != null) {
for (MultipartFile file : files) {
formData.add("file", file.getResource());
}
}
return this.webClient.post()
.uri("/api/ppt/v2/createTask")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(formData))
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(formData))
.bodyToMono(ApiResponse.class)
.block();
}
/**
* 获取生成选项
*
* @param lang 语种
* @return 生成选项
*/
public Map<String, Object> getOptions(String lang) {
String uri = "/api/ppt/v2/options";
if (lang != null) {
uri += "?lang=" + lang;
}
return this.webClient.get()
.uri(uri)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(lang))
.bodyToMono(new ParameterizedTypeReference<ApiResponse>() {
})
.<Map<String, Object>>handle((response, sink) -> {
if (response.code != 0) {
sink.error(new IllegalStateException("获取生成选项异常," + response.message));
return;
}
sink.next(response.data);
})
.block();
}
/**
* 分页查询 PPT 模板
*
* @param token 令牌
* @param request 请求体
* @return 模板列表
*/
public PagePptTemplateInfo getTemplatePage(TemplateQueryRequest request) {
return this.webClient.post()
.uri("/api/ppt/templates")
.bodyValue(request)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
.bodyToMono(new ParameterizedTypeReference<PagePptTemplateInfo>() {
})
.block();
}
/**
* 生成大纲内容
*
* @return 大纲内容流
*/
public Flux<Map<String, Object>> createOutline(CreateOutlineRequest request) {
return this.webClient.post()
.uri("/api/ppt/v2/generateContent")
.body(Mono.just(request), CreateOutlineRequest.class)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
.bodyToFlux(new ParameterizedTypeReference<>() {
});
}
/**
* 修改大纲内容
*
* @param request 请求体
* @return 大纲内容流
*/
public Flux<Map<String, Object>> updateOutline(UpdateOutlineRequest request) {
return this.webClient.post()
.uri("/api/ppt/v2/updateContent")
.body(Mono.just(request), UpdateOutlineRequest.class)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
.bodyToFlux(new ParameterizedTypeReference<>() {
});
}
/**
* 生成 PPT
*
* @return PPT信息
*/
public PptInfo create(PptCreateRequest request) {
return this.webClient.post()
.uri("/api/ppt/v2/generatePptx")
.body(Mono.just(request), PptCreateRequest.class)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
.bodyToMono(ApiResponse.class)
.<PptInfo>handle((response, sink) -> {
if (response.code != 0) {
sink.error(new IllegalStateException("生成 PPT 异常," + response.message));
return;
}
sink.next(Objects.requireNonNull(JsonUtils.parseObject(JsonUtils.toJsonString(response.data.get("pptInfo")), PptInfo.class)));
})
.block();
}
/**
* 创建 Token 请求参数
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record CreateTokenRequest(
String apiKey,
String uid,
Integer limit
) {
public CreateTokenRequest(String apiKey) {
this(apiKey, null, null);
}
}
/**
* API 通用响应
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record ApiResponse(
Integer code,
String message,
Map<String, Object> data
) {
}
/**
* 创建任务
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record CreateTaskRequest(
Integer type,
String content,
List<MultipartFile> files
) {
}
/**
* 生成大纲内容请求
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record CreateOutlineRequest(
String id,
String length,
String scene,
String audience,
String lang,
String prompt
) {
}
/**
* 修改大纲内容请求
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record UpdateOutlineRequest(
String id,
String markdown,
String question
) {
}
/**
* 生成 PPT 请求参数
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record PptCreateRequest(
String id,
String templateId,
String markdown
) {
}
/**
* PPT 信息
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record PptInfo(
String id,
String name,
String subject,
String coverUrl,
String fileUrl,
String templateId,
String pptxProperty,
String userId,
String userName,
int companyId,
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime updateTime,
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime createTime,
String createUser,
String updateUser
) {
}
/**
* 模板查询请求参数
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record TemplateQueryRequest(
int page,
int size,
Filter filters
) {
/**
* 模板查询过滤条件
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record Filter(
int type,
String category,
String style,
String themeColor
) {
}
}
/**
* PPT模板分页信息
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record PagePptTemplateInfo(
List<PptTemplateInfo> data,
String total
) {
}
/**
* PPT模板信息
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record PptTemplateInfo(
String id,
int type,
Integer subType,
String layout,
String category,
String style,
String themeColor,
String lang,
boolean animation,
String subject,
String coverUrl,
String fileUrl,
List<String> pageCoverUrls,
String pptxProperty,
int sort,
int num,
Integer imgNum,
int isDeleted,
String userId,
int companyId,
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime updateTime,
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime createTime,
String createUser,
String updateUser
) {
}
}
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.framework.ai.core.model.xinghuo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux;
/**
* 讯飞星火 {@link ChatModel} 实现类
*
* @author fansili
*/
@Slf4j
@RequiredArgsConstructor
public class XingHuoChatModel implements ChatModel {
public static final String BASE_URL = "https://spark-api-open.xf-yun.com";
public static final String MODEL_DEFAULT = "generalv3.5";
/**
* 兼容 OpenAI 接口,进行复用
*/
private final OpenAiChatModel openAiChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
}
}
@@ -0,0 +1,522 @@
package cn.iocoder.yudao.framework.ai.core.model.xinghuo.api;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* 讯飞智能 PPT 生成 API
*
* @author xiaoxin
* @see <a href="https://www.xfyun.cn/doc/spark/PPTv2.html">智能 PPT 生成 API</a>
*/
@Slf4j
public class XunFeiPptApi {
public static final String BASE_URL = "https://zwapi.xfyun.cn/api/ppt/v2";
private static final String HEADER_APP_ID = "appId";
private static final String HEADER_TIMESTAMP = "timestamp";
private static final String HEADER_SIGNATURE = "signature";
private final WebClient webClient;
private final String appId;
private final String apiSecret;
private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful();
private final Function<Object, Function<ClientResponse, Mono<? extends Throwable>>> EXCEPTION_FUNCTION =
reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> {
log.error("[XunFeiPptApi] 调用失败!请求参数:[{}],响应数据: [{}]", reqParam, responseBody);
sink.error(new IllegalStateException("[XunFeiPptApi] 调用失败!"));
});
public XunFeiPptApi(String appId, String apiSecret) {
this.appId = appId;
this.apiSecret = apiSecret;
this.webClient = WebClient.builder()
.baseUrl(BASE_URL)
.defaultHeaders((headers) -> {
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add(HEADER_APP_ID, appId);
})
.build();
}
/**
* 获取签名
*
* @return 签名信息
*/
private SignatureInfo getSignature() {
long timestamp = System.currentTimeMillis() / 1000;
String ts = String.valueOf(timestamp);
String signature = generateSignature(appId, apiSecret, timestamp);
return new SignatureInfo(ts, signature);
}
/**
* 生成签名
*
* @param appId 应用ID
* @param apiSecret 应用密钥
* @param timestamp 时间戳(秒)
* @return 签名
*/
private String generateSignature(String appId, String apiSecret, long timestamp) {
String auth = SecureUtil.md5(appId + timestamp);
return SecureUtil.hmac(HmacAlgorithm.HmacSHA1, apiSecret).digestBase64(auth, false);
}
/**
* 获取 PPT 模板列表
*
* @param style 风格,如"商务"
* @param pageSize 每页数量
* @return 模板列表
*/
public TemplatePageResponse getTemplatePage(String style, Integer pageSize) {
SignatureInfo signInfo = getSignature();
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("style", style);
requestBody.put("pageSize", ObjUtil.defaultIfNull(pageSize, 20));
return this.webClient.post()
.uri("/template/list")
.header(HEADER_TIMESTAMP, signInfo.timestamp)
.header(HEADER_SIGNATURE, signInfo.signature)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(requestBody))
.bodyToMono(TemplatePageResponse.class)
.block();
}
/**
* 创建大纲(通过文本)
*
* @param query 查询文本
* @return 大纲创建响应
*/
public CreateResponse createOutline(String query) {
SignatureInfo signInfo = getSignature();
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
formData.add("query", query);
return this.webClient.post()
.uri("/createOutline")
.header(HEADER_TIMESTAMP, signInfo.timestamp)
.header(HEADER_SIGNATURE, signInfo.signature)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(formData))
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(formData))
.bodyToMono(CreateResponse.class)
.block();
}
/**
* 直接创建 PPT(简化版 - 通过文本)
*
* @param query 查询文本
* @return 创建响应
*/
public CreateResponse create(String query) {
CreatePptRequest request = CreatePptRequest.builder()
.query(query)
.build();
return create(request);
}
/**
* 直接创建 PPT(简化版 - 通过文件)
*
* @param file 文件
* @param fileName 文件名
* @return 创建响应
*/
public CreateResponse create(MultipartFile file, String fileName) {
CreatePptRequest request = CreatePptRequest.builder()
.file(file).fileName(fileName).build();
return create(request);
}
/**
* 直接创建 PPT(完整版)
*
* @param request 请求参数
* @return 创建响应
*/
public CreateResponse create(CreatePptRequest request) {
SignatureInfo signInfo = getSignature();
MultiValueMap<String, Object> formData = buildCreatePptFormData(request);
return this.webClient.post()
.uri("/create")
.header(HEADER_TIMESTAMP, signInfo.timestamp)
.header(HEADER_SIGNATURE, signInfo.signature)
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(formData))
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(formData))
.bodyToMono(CreateResponse.class)
.block();
}
/**
* 通过大纲创建 PPT(简化版)
*
* @param outline 大纲内容
* @param query 查询文本
* @return 创建响应
*/
public CreateResponse createPptByOutline(OutlineData outline, String query) {
CreatePptByOutlineRequest request = CreatePptByOutlineRequest.builder()
.outline(outline)
.query(query)
.build();
return createPptByOutline(request);
}
/**
* 通过大纲创建 PPT(完整版)
*
* @param request 请求参数
* @return 创建响应
*/
public CreateResponse createPptByOutline(CreatePptByOutlineRequest request) {
SignatureInfo signInfo = getSignature();
return this.webClient.post()
.uri("/createPptByOutline")
.header(HEADER_TIMESTAMP, signInfo.timestamp)
.header(HEADER_SIGNATURE, signInfo.signature)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(request)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(request))
.bodyToMono(CreateResponse.class)
.block();
}
/**
* 检查 PPT 生成进度
*
* @param sid 任务 ID
* @return 进度响应
*/
public ProgressResponse checkProgress(String sid) {
SignatureInfo signInfo = getSignature();
return this.webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/progress")
.queryParam("sid", sid)
.build())
.header(HEADER_TIMESTAMP, signInfo.timestamp)
.header(HEADER_SIGNATURE, signInfo.signature)
.retrieve()
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(sid))
.bodyToMono(ProgressResponse.class)
.block();
}
/**
* 签名信息
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
private record SignatureInfo(
String timestamp,
String signature
) {
}
/**
* 模板列表响应
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record TemplatePageResponse(
boolean flag,
int code,
String desc,
Integer count,
TemplatePageData data
) {
}
/**
* 模板列表数据
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record TemplatePageData(
String total,
List<TemplateInfo> records,
Integer pageNum
) {
}
/**
* 模板信息
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record TemplateInfo(
String templateIndexId,
Integer pageCount,
String type,
String color,
String industry,
String style,
String detailImage
) {
}
/**
* 创建响应
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record CreateResponse(
boolean flag,
int code,
String desc,
Integer count,
CreateResponseData data
) {
}
/**
* 创建响应数据
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record CreateResponseData(
String sid,
String coverImgSrc,
String title,
String subTitle,
OutlineData outline
) {
}
/**
* 大纲数据结构
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record OutlineData(
String title,
String subTitle,
List<Chapter> chapters
) {
/**
* 章节结构
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record Chapter(
String chapterTitle,
List<ChapterContent> chapterContents
) {
/**
* 章节内容
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record ChapterContent(
String chapterTitle
) {
}
}
/**
* 将大纲对象转换为JSON字符串
*
* @return 大纲JSON字符串
*/
public String toJsonString() {
return JsonUtils.toJsonString(this);
}
}
/**
* 进度响应
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record ProgressResponse(
int code,
String desc,
ProgressResponseData data
) {
}
/**
* 进度响应数据
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
public record ProgressResponseData(
int process,
String pptId,
String pptUrl,
String pptStatus,
String aiImageStatus,
String cardNoteStatus,
String errMsg,
Integer totalPages,
Integer donePages
) {
/**
* 是否全部完成
*
* @return 是否全部完成
*/
public boolean isAllDone() {
return "done".equals(pptStatus)
&& ("done".equals(aiImageStatus) || aiImageStatus == null)
&& ("done".equals(cardNoteStatus) || cardNoteStatus == null);
}
/**
* 是否失败
*
* @return 是否失败
*/
public boolean isFailed() {
return "build_failed".equals(pptStatus);
}
/**
* 获取进度百分比
*
* @return 进度百分比
*/
public int getProgressPercent() {
if (totalPages == null || totalPages == 0 || donePages == null) {
return process; // 兼容旧版返回
}
return (int) (donePages * 100.0 / totalPages);
}
}
/**
* 通过大纲创建 PPT 请求参数
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
@Builder
public record CreatePptByOutlineRequest(
String query, // 用户生成PPT要求(最多8000字)
String outlineSid, // 已生成大纲后,响应返回的请求大纲唯一id
OutlineData outline, // 大纲内容
String templateId, // 模板ID
String businessId, // 业务ID(非必传)
String author, // PPT作者名
Boolean isCardNote, // 是否生成PPT演讲备注
Boolean search, // 是否联网搜索
String language, // 语种
String fileUrl, // 文件地址
String fileName, // 文件名(带文件名后缀)
Boolean isFigure, // 是否自动配图
String aiImage // ai配图类型:normal、advanced
) {
}
/**
* 构建创建 PPT 的表单数据
*
* @param request 请求参数
* @return 表单数据
*/
private MultiValueMap<String, Object> buildCreatePptFormData(CreatePptRequest request) {
MultiValueMap<String, Object> formData = new LinkedMultiValueMap<>();
if (request.file() != null) {
try {
formData.add("file", new ByteArrayResource(request.file().getBytes()) {
@Override
public String getFilename() {
return request.file().getOriginalFilename();
}
});
} catch (IOException e) {
log.error("[XunFeiPptApi] 文件处理失败", e);
throw new IllegalStateException("[XunFeiPptApi] 文件处理失败", e);
}
}
Map<String, Object> param = new HashMap<>();
addIfPresent(param, "query", request.query());
addIfPresent(param, "fileUrl", request.fileUrl());
addIfPresent(param, "fileName", request.fileName());
addIfPresent(param, "templateId", request.templateId());
addIfPresent(param, "businessId", request.businessId());
addIfPresent(param, "author", request.author());
addIfPresent(param, "isCardNote", request.isCardNote());
addIfPresent(param, "search", request.search());
addIfPresent(param, "language", request.language());
addIfPresent(param, "isFigure", request.isFigure());
addIfPresent(param, "aiImage", request.aiImage());
param.forEach(formData::add);
return formData;
}
public static <K, V> void addIfPresent(Map<K, V> map, K key, V value) {
if (ObjUtil.isNull(key) || ObjUtil.isNull(map)) {
return;
}
boolean isPresent = false;
if (ObjUtil.isNotNull(value)) {
if (value instanceof String) {
// 字符串:需要有实际内容
isPresent = StringUtils.hasText((String) value);
} else {
// 其他类型:非 null 即视为存在
isPresent = true;
}
}
if (isPresent) {
map.put(key, value);
}
}
/**
* 直接生成PPT请求参数
*/
@JsonInclude(value = JsonInclude.Include.NON_NULL)
@Builder
public record CreatePptRequest(
String query, // 用户生成PPT要求(最多8000字)
MultipartFile file, // 上传文件
String fileUrl, // 文件地址
String fileName, // 文件名(带文件名后缀)
String templateId, // 模板ID
String businessId, // 业务ID(非必传)
String author, // PPT作者名
Boolean isCardNote, // 是否生成PPT演讲备注
Boolean search, // 是否联网搜索
String language, // 语种
Boolean isFigure, // 是否自动配图
String aiImage // ai配图类型:normal、advanced
) {
}
}
@@ -0,0 +1,87 @@
package cn.iocoder.yudao.framework.ai.core.util;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.moonshot.MoonshotChatOptions;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.qianfan.QianFanChatOptions;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
import java.util.Collections;
import java.util.Set;
/**
* Spring AI 工具类
*
* @author 芋道源码
*/
public class AiUtils {
public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
return buildChatOptions(platform, model, temperature, maxTokens, null);
}
public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens,
Set<String> toolNames) {
toolNames = ObjUtil.defaultIfNull(toolNames, Collections.emptySet());
// noinspection EnhancedSwitchMigration
switch (platform) {
case TONG_YI:
return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
.withFunctions(toolNames).build();
case YI_YAN:
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
case ZHI_PU:
return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.functions(toolNames).build();
case MINI_MAX:
return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.functions(toolNames).build();
case MOONSHOT:
return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.functions(toolNames).build();
case OPENAI:
case DEEP_SEEK: // 复用 OpenAI 客户端
case DOU_BAO: // 复用 OpenAI 客户端
case HUN_YUAN: // 复用 OpenAI 客户端
case XING_HUO: // 复用 OpenAI 客户端
case SILICON_FLOW: // 复用 OpenAI 客户端
case BAI_CHUAN: // 复用 OpenAI 客户端
return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolNames(toolNames).build();
case AZURE_OPENAI:
// TODO 芋艿:貌似没 model 字段???!
return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens)
.toolNames(toolNames).build();
case OLLAMA:
return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens)
.toolNames(toolNames).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));
}
}
@@ -0,0 +1,13 @@
/**
* AI 大模型组件,基于 Spring AI 拓展
*
* models 包路径:
* 1. xinghuo 包:【讯飞】星火,自己实现
* 2. deepseek 包:【深度求索】DeepSeek,自己实现
* 3. doubao 包:【字节豆包】DouBao,自己实现
* 4. hunyuan 包:【腾讯混元】HunYuan,自己实现
* 5. siliconflow 包:【硅基硅流】SiliconFlow,自己实现
* 6. midjourney 包:Midjourney API,对接 https://github.com/novicezk/midjourney-proxy 实现
* 7. suno 包:Suno API,对接 https://github.com/gcui-art/suno-api 实现
*/
package cn.iocoder.yudao.framework.ai;
@@ -0,0 +1 @@
cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration