| | |
| | | package cn.iocoder.yudao.module.ai.framework.ai.config; |
| | | |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.hutool.extra.spring.SpringUtil; |
| | | import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory; |
| | | import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactoryImpl; |
| | | import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; |
| | | import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; |
| | | import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel; |
| | | import cn.iocoder.yudao.module.ai.framework.ai.core.model.grok.GrokChatModel; |
| | | import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; |
| | | import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; |
| | | import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; |
| | | import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel; |
| | | import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; |
| | | import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; |
| | | import com.openai.client.okhttp.OpenAIOkHttpClient; |
| | | import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; |
| | | import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient; |
| | | import cn.iocoder.yudao.module.ai.tool.method.PersonService; |
| | | import io.micrometer.observation.ObservationRegistry; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.ai.chat.model.ChatModel; |
| | | import org.springframework.ai.deepseek.DeepSeekChatModel; |
| | | import org.springframework.ai.deepseek.DeepSeekChatOptions; |
| | | import org.springframework.ai.deepseek.api.DeepSeekApi; |
| | | import org.springframework.ai.document.MetadataMode; |
| | | 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.support.ToolCallbacks; |
| | | import org.springframework.ai.openai.OpenAiEmbeddingModel; |
| | | import org.springframework.ai.openai.OpenAiEmbeddingOptions; |
| | | import org.springframework.ai.retry.RetryUtils; |
| | | import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; |
| | | import org.springframework.ai.tokenizer.TokenCountEstimator; |
| | | import org.springframework.ai.tool.ToolCallback; |
| | | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties; |
| | | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties; |
| | | import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties; |
| | | import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; |
| | | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | |
| | | import java.util.List; |
| | | import java.util.Optional; |
| | | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| | | |
| | | /** |
| | | * 芋道 AI 自动配置 |
| | | * |
| | | * @author fansili |
| | | * 使用 OpenAI 兼容接口对接通义千问 DashScope |
| | | */ |
| | | @Configuration |
| | | @EnableConfigurationProperties({ YudaoAiProperties.class, |
| | | QdrantVectorStoreProperties.class, // 解析 Qdrant 配置 |
| | | RedisVectorStoreProperties.class, // 解析 Redis 配置 |
| | | MilvusVectorStoreProperties.class, MilvusServiceClientProperties.class // 解析 Milvus 配置 |
| | | @EnableConfigurationProperties({ |
| | | YudaoAiProperties.class, |
| | | QdrantVectorStoreProperties.class, |
| | | RedisVectorStoreProperties.class, |
| | | MilvusVectorStoreProperties.class, |
| | | MilvusServiceClientProperties.class |
| | | }) |
| | | @Slf4j |
| | | public class AiAutoConfiguration { |
| | | |
| | | private static final String DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"; |
| | | |
| | | @Bean |
| | | public AiModelFactory aiModelFactory() { |
| | |
| | | @Bean |
| | | @ConditionalOnMissingBean |
| | | public ObservationRegistry observationRegistry() { |
| | | // 特殊:兜底有 ObservationRegistry Bean,避免相关的 ChatModel 创建报错。相关 issue:https://t.zsxq.com/CuPu4 |
| | | return ObservationRegistry.NOOP; |
| | | } |
| | | |
| | | // ========== 各种 AI Client 创建 ========== |
| | | |
| | | @Bean |
| | | @ConditionalOnProperty(value = "yudao.ai.gemini.enable", havingValue = "true") |
| | | public GeminiChatModel geminiChatModel(YudaoAiProperties yudaoAiProperties) { |
| | | YudaoAiProperties.Gemini properties = yudaoAiProperties.getGemini(); |
| | | return buildGeminiChatClient(properties); |
| | | } |
| | | |
| | | public GeminiChatModel buildGeminiChatClient(YudaoAiProperties.Gemini properties) { |
| | | if (StrUtil.isEmpty(properties.getModel())) { |
| | | properties.setModel(GeminiChatModel.MODEL_DEFAULT); |
| | | } |
| | | OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() |
| | | .openAiClient(OpenAIOkHttpClient.builder() |
| | | .baseUrl(GeminiChatModel.BASE_URL) |
| | | .apiKey(properties.getApiKey()) |
| | | .build()) |
| | | .options(OpenAiChatOptions.builder() |
| | | .model(properties.getModel()) |
| | | .temperature(properties.getTemperature()) |
| | | .maxTokens(properties.getMaxTokens()) |
| | | .topP(properties.getTopP()) |
| | | .build()) |
| | | .toolCallingManager(getToolCallingManager()) |
| | | .build(); |
| | | return new GeminiChatModel(openAiChatModel); |
| | | } |
| | | |
| | | @Bean |
| | | @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") |
| | | public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { |
| | | YudaoAiProperties.DouBao properties = yudaoAiProperties.getDoubao(); |
| | | return buildDouBaoChatClient(properties); |
| | | } |
| | | |
| | | public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBao properties) { |
| | | if (StrUtil.isEmpty(properties.getModel())) { |
| | | properties.setModel(DouBaoChatModel.MODEL_DEFAULT); |
| | | } |
| | | OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() |
| | | .openAiClient(OpenAIOkHttpClient.builder() |
| | | .baseUrl(DouBaoChatModel.BASE_URL) |
| | | .apiKey(properties.getApiKey()) |
| | | .build()) |
| | | .options(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.SiliconFlow properties = yudaoAiProperties.getSiliconflow(); |
| | | return buildSiliconFlowChatClient(properties); |
| | | } |
| | | |
| | | public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlow properties) { |
| | | if (StrUtil.isEmpty(properties.getModel())) { |
| | | properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT); |
| | | } |
| | | DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() |
| | | .deepSeekApi(DeepSeekApi.builder() |
| | | .baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL) |
| | | .apiKey(properties.getApiKey()) |
| | | .build()) |
| | | .defaultOptions(DeepSeekChatOptions.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.HunYuan properties = yudaoAiProperties.getHunyuan(); |
| | | return buildHunYuanChatClient(properties); |
| | | } |
| | | |
| | | public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuan 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); |
| | | } |
| | | // 创建 DeepSeekChatModel、HunYuanChatModel 对象 |
| | | DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() |
| | | .deepSeekApi(DeepSeekApi.builder() |
| | | .baseUrl(properties.getBaseUrl()) |
| | | .completionsPath(HunYuanChatModel.COMPLETE_PATH) |
| | | .apiKey(properties.getApiKey()) |
| | | .build()) |
| | | .defaultOptions(DeepSeekChatOptions.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.XingHuo properties = yudaoAiProperties.getXinghuo(); |
| | | return buildXingHuoChatClient(properties); |
| | | } |
| | | |
| | | public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuo properties) { |
| | | if (StrUtil.isEmpty(properties.getModel())) { |
| | | properties.setModel(XingHuoChatModel.MODEL_DEFAULT); |
| | | } |
| | | OpenAIOkHttpClient.Builder builder = OpenAIOkHttpClient.builder() |
| | | .baseUrl(XingHuoChatModel.BASE_URL_V1) |
| | | .apiKey(properties.getAppKey() + ":" + properties.getSecretKey()); |
| | | if ("x1".equals(properties.getModel())) { |
| | | builder.baseUrl(XingHuoChatModel.BASE_URL_V2); |
| | | } |
| | | OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() |
| | | .openAiClient(builder.build()) |
| | | .options(OpenAiChatOptions.builder() |
| | | .model(properties.getModel()) |
| | | .temperature(properties.getTemperature()) |
| | | .maxTokens(properties.getMaxTokens()) |
| | | .topP(properties.getTopP()) |
| | | .build()) |
| | | // TODO @芋艿:星火的 function call 有 bug,会报 ToolResponseMessage must have an id 错误!!! |
| | | .toolCallingManager(getToolCallingManager()) |
| | | .build(); |
| | | return new XingHuoChatModel(openAiChatModel); |
| | | } |
| | | |
| | | @Bean |
| | | @ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true") |
| | | public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) { |
| | | YudaoAiProperties.BaiChuan properties = yudaoAiProperties.getBaichuan(); |
| | | return buildBaiChuanChatClient(properties); |
| | | } |
| | | |
| | | public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuan properties) { |
| | | if (StrUtil.isEmpty(properties.getModel())) { |
| | | properties.setModel(BaiChuanChatModel.MODEL_DEFAULT); |
| | | } |
| | | OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() |
| | | .openAiClient(OpenAIOkHttpClient.builder() |
| | | .baseUrl(BaiChuanChatModel.BASE_URL) |
| | | .apiKey(properties.getApiKey()) |
| | | .build()) |
| | | .options(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.Midjourney 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()); |
| | | } |
| | | |
| | | public ChatModel buildGrokChatClient(YudaoAiProperties.Grok properties) { |
| | | if (StrUtil.isEmpty(properties.getModel())) { |
| | | properties.setModel(GrokChatModel.MODEL_DEFAULT); |
| | | } |
| | | OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() |
| | | .openAiClient(OpenAIOkHttpClient.builder() |
| | | .baseUrl(Optional.ofNullable(properties.getBaseUrl()) |
| | | .orElse(GrokChatModel.BASE_URL)) |
| | | .apiKey(properties.getApiKey()) |
| | | .build()) |
| | | .options(OpenAiChatOptions.builder() |
| | | .model(properties.getModel()) |
| | | .temperature(properties.getTemperature()) |
| | | .maxTokens(properties.getMaxTokens()) |
| | | .topP(properties.getTopP()) |
| | | .build()) |
| | | .toolCallingManager(getToolCallingManager()) |
| | | .build(); |
| | | return new DouBaoChatModel(openAiChatModel); |
| | | } |
| | | |
| | | // ========== RAG 相关 ========== |
| | | |
| | | @Bean |
| | | public TokenCountEstimator tokenCountEstimator() { |
| | | return new JTokkitTokenCountEstimator(); |
| | | } |
| | | |
| | | @Bean |
| | | @ConditionalOnMissingBean |
| | | public BatchingStrategy batchingStrategy() { |
| | | return new TokenCountBatchingStrategy(); |
| | | } |
| | | |
| | | private static ToolCallingManager getToolCallingManager() { |
| | | return SpringUtil.getBean(ToolCallingManager.class); |
| | | @Bean |
| | | @ConditionalOnMissingBean |
| | | public TokenCountEstimator tokenCountEstimator() { |
| | | return new JTokkitTokenCountEstimator(); |
| | | } |
| | | |
| | | // ========== Web Search 相关 ========== |
| | | // ========== 通义千问 Chat(通过 OpenAI 兼容接口)========== |
| | | |
| | | @Bean |
| | | @ConditionalOnProperty(value = "yudao.ai.web-search.enable", havingValue = "true") |
| | | public AiWebSearchClient webSearchClient(YudaoAiProperties yudaoAiProperties) { |
| | | return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey()); |
| | | public static OpenAiChatModel buildTongYiChatModel(String apiKey, String model) { |
| | | return OpenAiChatModel.builder() |
| | | .options(OpenAiChatOptions.builder() |
| | | .baseUrl(DASHSCOPE_BASE_URL) |
| | | .apiKey(apiKey) |
| | | .model(StrUtil.blankToDefault(model, "qwen-plus")) |
| | | .temperature(0.7) |
| | | .build()) |
| | | .toolCallingManager(ToolCallingManager.builder().build()) |
| | | .build(); |
| | | } |
| | | |
| | | // ========== MCP 相关 ========== |
| | | // ========== 通义千问 Embedding(通过 OpenAI 兼容接口)========== |
| | | |
| | | /** |
| | | * 参考自 <a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html">MCP Server Boot Starter</> |
| | | */ |
| | | @Bean |
| | | public List<ToolCallback> toolCallbacks(PersonService personService) { |
| | | return List.of(ToolCallbacks.from(personService)); |
| | | public static OpenAiEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) { |
| | | return OpenAiEmbeddingModel.builder() |
| | | .options(OpenAiEmbeddingOptions.builder() |
| | | .baseUrl(DASHSCOPE_BASE_URL) |
| | | .apiKey(apiKey) |
| | | .model(StrUtil.blankToDefault(model, "text-embedding-v3")) |
| | | .build()) |
| | | .metadataMode(MetadataMode.EMBED) |
| | | .build(); |
| | | } |
| | | |
| | | } |