From c8485a1d6dbf288874f2c2f4ad26cdf6d11bd068 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 20 Feb 2025 12:49:06 +0800 Subject: [PATCH 01/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E8=AF=84?= =?UTF-8?q?=E5=AE=A1=E3=80=91IoT=EF=BC=9A=E7=A7=BB=E9=99=A4=20tongyi=20?= =?UTF-8?q?=E5=BA=93=EF=BC=8C=E5=87=86=E5=A4=87=E6=8D=A2=E5=88=B0=20alibab?= =?UTF-8?q?a=20ai=20=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/tongyi/TongYiAutoConfiguration.java | 253 --------- .../ai/tongyi/TongYiConnectionProperties.java | 52 -- .../ai/tongyi/audio/AudioSpeechModels.java | 40 -- .../audio/AudioTranscriptionModels.java | 43 -- .../audio/speech/TongYiAudioSpeechModel.java | 228 --------- .../speech/TongYiAudioSpeechOptions.java | 261 ---------- .../speech/TongYiAudioSpeechProperties.java | 77 --- .../ai/tongyi/audio/speech/api/Speech.java | 87 ---- .../audio/speech/api/SpeechMessage.java | 80 --- .../audio/speech/api/SpeechMetadata.java | 43 -- .../tongyi/audio/speech/api/SpeechModel.java | 51 -- .../tongyi/audio/speech/api/SpeechPrompt.java | 89 ---- .../audio/speech/api/SpeechResponse.java | 100 ---- .../audio/speech/api/SpeechStreamModel.java | 54 -- .../TongYiAudioTranscriptionModel.java | 187 ------- .../TongYiAudioTranscriptionOptions.java | 203 -------- .../TongYiAudioTranscriptionProperties.java | 72 --- .../api/AudioTranscriptionPrompt.java | 56 -- .../api/AudioTranscriptionResponse.java | 67 --- .../api/AudioTranscriptionResult.java | 68 --- .../cloud/ai/tongyi/chat/TongYiChatModel.java | 483 ------------------ .../ai/tongyi/chat/TongYiChatOptions.java | 463 ----------------- .../ai/tongyi/chat/TongYiChatProperties.java | 83 --- .../common/constants/TongYiConstants.java | 44 -- .../common/exception/TongYiException.java | 38 -- .../exception/TongYiImagesException.java | 39 -- .../embedding/TongYiEmbeddingOptions.java | 84 --- .../embedding/TongYiTextEmbeddingModel.java | 176 ------- .../TongYiTextEmbeddingProperties.java | 50 -- .../ai/tongyi/image/TongYiImagesModel.java | 237 --------- .../ai/tongyi/image/TongYiImagesOptions.java | 187 ------- .../tongyi/image/TongYiImagesProperties.java | 77 --- .../TongYiAiChatResponseMetadata.java | 89 ---- .../ai/tongyi/metadata/TongYiAiUsage.java | 81 --- .../TongYiImagesResponseMetadata.java | 131 ----- .../TongYiTextEmbeddingResponseMetadata.java | 53 -- .../TongYiAudioSpeechResponseMetadata.java | 133 ----- .../TongYiAudioTranscriptionMetadata.java | 43 -- ...gYiAudioTranscriptionResponseMetadata.java | 98 ---- 39 files changed, 4700 deletions(-) delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiAutoConfiguration.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiConnectionProperties.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioSpeechModels.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioTranscriptionModels.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechModel.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechOptions.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechProperties.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/Speech.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMessage.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMetadata.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechModel.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechPrompt.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechResponse.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechStreamModel.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionOptions.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionProperties.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionPrompt.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResponse.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResult.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatOptions.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatProperties.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/constants/TongYiConstants.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiException.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiImagesException.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiEmbeddingOptions.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingProperties.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesModel.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesOptions.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesProperties.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiChatResponseMetadata.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiUsage.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiImagesResponseMetadata.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiTextEmbeddingResponseMetadata.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioSpeechResponseMetadata.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionMetadata.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionResponseMetadata.java diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiAutoConfiguration.java deleted file mode 100644 index 1add717c36..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiAutoConfiguration.java +++ /dev/null @@ -1,253 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi; - -import com.alibaba.cloud.ai.tongyi.audio.speech.TongYiAudioSpeechModel; -import com.alibaba.cloud.ai.tongyi.audio.speech.TongYiAudioSpeechProperties; -import com.alibaba.cloud.ai.tongyi.audio.transcription.TongYiAudioTranscriptionModel; -import com.alibaba.cloud.ai.tongyi.audio.transcription.TongYiAudioTranscriptionProperties; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatProperties; -import com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants; -import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException; -import com.alibaba.cloud.ai.tongyi.embedding.TongYiTextEmbeddingModel; -import com.alibaba.cloud.ai.tongyi.embedding.TongYiTextEmbeddingProperties; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesProperties; -import com.alibaba.dashscope.aigc.generation.Generation; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import com.alibaba.dashscope.audio.asr.transcription.Transcription; -import com.alibaba.dashscope.audio.tts.SpeechSynthesizer; -import com.alibaba.dashscope.common.MessageManager; -import com.alibaba.dashscope.embeddings.TextEmbedding; -import com.alibaba.dashscope.exception.NoApiKeyException; -import com.alibaba.dashscope.utils.ApiKey; -import com.alibaba.dashscope.utils.Constants; -import org.springframework.ai.model.function.FunctionCallbackContext; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -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.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Scope; - -import java.util.Objects; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -@AutoConfiguration -@ConditionalOnClass({ - MessageManager.class, - TongYiChatModel.class, - TongYiImagesModel.class, - TongYiAudioSpeechModel.class, - TongYiTextEmbeddingModel.class, - TongYiAudioTranscriptionModel.class -}) -@EnableConfigurationProperties({ - TongYiChatProperties.class, - TongYiImagesProperties.class, - TongYiAudioSpeechProperties.class, - TongYiConnectionProperties.class, - TongYiTextEmbeddingProperties.class, - TongYiAudioTranscriptionProperties.class -}) -public class TongYiAutoConfiguration { - - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - public Generation generation() { - - return new Generation(); - } - - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - public MessageManager msgManager() { - - return new MessageManager(10); - } - - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - public ImageSynthesis imageSynthesis() { - - return new ImageSynthesis(); - } - - @Bean - @Scope("prototype") - @ConditionalOnMissingBean - public SpeechSynthesizer speechSynthesizer() { - - return new SpeechSynthesizer(); - } - - @Bean - @ConditionalOnMissingBean - public Transcription transcription() { - - return new Transcription(); - } - - @Bean - @ConditionalOnMissingBean - public TextEmbedding textEmbedding() { - - return new TextEmbedding(); - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackContext springAiFunctionManager(ApplicationContext context) { - - FunctionCallbackContext manager = new FunctionCallbackContext(); - manager.setApplicationContext(context); - - return manager; - } - - @Bean - @ConditionalOnProperty( - prefix = TongYiChatProperties.CONFIG_PREFIX, - name = "enabled", - havingValue = "true", - matchIfMissing = true - ) - public TongYiChatModel tongYiChatClient(Generation generation, - TongYiChatProperties chatOptions, - TongYiConnectionProperties connectionProperties - ) { - - settingApiKey(connectionProperties); - - return new TongYiChatModel(generation, chatOptions.getOptions()); - } - - @Bean - @ConditionalOnProperty( - prefix = TongYiImagesProperties.CONFIG_PREFIX, - name = "enabled", - havingValue = "true", - matchIfMissing = true - ) - public TongYiImagesModel tongYiImagesClient( - ImageSynthesis imageSynthesis, - TongYiImagesProperties imagesOptions, - TongYiConnectionProperties connectionProperties - ) { - - settingApiKey(connectionProperties); - - return new TongYiImagesModel(imageSynthesis, imagesOptions.getOptions()); - } - - @Bean - @ConditionalOnProperty( - prefix = TongYiAudioSpeechProperties.CONFIG_PREFIX, - name = "enabled", - havingValue = "true", - matchIfMissing = true - ) - public TongYiAudioSpeechModel tongYiAudioSpeechClient( - SpeechSynthesizer speechSynthesizer, - TongYiAudioSpeechProperties speechProperties, - TongYiConnectionProperties connectionProperties - ) { - - settingApiKey(connectionProperties); - - return new TongYiAudioSpeechModel(speechSynthesizer, speechProperties.getOptions()); - } - - @Bean - @ConditionalOnProperty( - prefix = TongYiAudioTranscriptionProperties.CONFIG_PREFIX, - name = "enabled", - havingValue = "true", - matchIfMissing = true - ) - public TongYiAudioTranscriptionModel tongYiAudioTranscriptionClient( - Transcription transcription, - TongYiAudioTranscriptionProperties transcriptionProperties, - TongYiConnectionProperties connectionProperties) { - - settingApiKey(connectionProperties); - - return new TongYiAudioTranscriptionModel( - transcriptionProperties.getOptions(), - transcription - ); - } - - @Bean - @ConditionalOnProperty( - prefix = TongYiTextEmbeddingProperties.CONFIG_PREFIX, - name = "enabled", - havingValue = "true", - matchIfMissing = true - ) - public TongYiTextEmbeddingModel tongYiTextEmbeddingClient( - TextEmbedding textEmbedding, - TongYiConnectionProperties connectionProperties - ) { - - settingApiKey(connectionProperties); - return new TongYiTextEmbeddingModel(textEmbedding); - } - - /** - * Setting the API key. - * @param connectionProperties {@link TongYiConnectionProperties} - */ - private void settingApiKey(TongYiConnectionProperties connectionProperties) { - - String apiKey; - - try { - // It is recommended to set the key by defining the api-key in an environment variable. - var envKey = System.getenv(TongYiConstants.SCA_AI_TONGYI_API_KEY); - if (Objects.nonNull(envKey)) { - Constants.apiKey = envKey; - return; - } - if (Objects.nonNull(connectionProperties.getApiKey())) { - apiKey = connectionProperties.getApiKey(); - } - else { - apiKey = ApiKey.getApiKey(null); - } - - Constants.apiKey = apiKey; - } - catch (NoApiKeyException e) { - - throw new TongYiException(e.getMessage()); - } - - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiConnectionProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiConnectionProperties.java deleted file mode 100644 index 74141bd227..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiConnectionProperties.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION; - -/** - * Spring Cloud Alibaba AI TongYi LLM connection properties. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -@ConfigurationProperties(TongYiConnectionProperties.CONFIG_PREFIX) -public class TongYiConnectionProperties { - - /** - * Spring Cloud Alibaba AI connection configuration Prefix. - */ - public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "tongyi"; - - /** - * TongYi LLM API key. - */ - private String apiKey; - - public String getApiKey() { - return apiKey; - } - - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioSpeechModels.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioSpeechModels.java deleted file mode 100644 index 2c8bd707bf..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioSpeechModels.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio; - -/** - * More models see: https://help.aliyun.com/zh/dashscope/model-list?spm=a2c4g.11186623.0.i5 - * Support all models in list. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public final class AudioSpeechModels { - - private AudioSpeechModels() { - } - - /** - * Male Voice of the Tongue(舌尖男声). - * zh & en. - * Default sample rate: 48 Hz. - */ - public static final String SAMBERT_ZHICHU_V1 = "sambert-zhichu-v1"; - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioTranscriptionModels.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioTranscriptionModels.java deleted file mode 100644 index 4b8435629a..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioTranscriptionModels.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio; - -/** - * @author xYLiu - * @author yuluo - * @since 2023.0.1.0 - */ - -public final class AudioTranscriptionModels { - - private AudioTranscriptionModels() { - } - - /** - * Paraformer Chinese and English speech recognition model supports audio or video speech recognition with a sampling rate of 16kHz or above. - */ - public static final String Paraformer_V1 = "paraformer-v1"; - /** - * Paraformer Chinese speech recognition model, support 8kHz telephone speech recognition. - */ - public static final String Paraformer_8K_V1 = "paraformer-8k-v1"; - /** - * The Paraformer multilingual speech recognition model supports audio or video speech recognition with a sample rate of 16kHz or above. - */ - public static final String Paraformer_MTL_V1 = "paraformer-mtl-v1"; - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechModel.java deleted file mode 100644 index f450042da0..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechModel.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.speech; - -import com.alibaba.cloud.ai.tongyi.audio.AudioSpeechModels; -import com.alibaba.cloud.ai.tongyi.audio.speech.api.*; -import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioSpeechResponseMetadata; -import com.alibaba.dashscope.audio.tts.SpeechSynthesisParam; -import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult; -import com.alibaba.dashscope.audio.tts.SpeechSynthesizer; -import com.alibaba.dashscope.common.ResultCallback; -import io.reactivex.Flowable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.util.Assert; -import reactor.core.publisher.Flux; -import reactor.core.scheduler.Schedulers; - -import java.nio.ByteBuffer; - -/** - * TongYiAudioSpeechClient is a client for TongYi audio speech service for Spring Cloud Alibaba AI. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAudioSpeechModel implements SpeechModel, SpeechStreamModel { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - /** - * Default speed rate. - */ - private static final float SPEED_RATE = 1.0f; - - /** - * TongYi models api. - */ - private final SpeechSynthesizer speechSynthesizer; - - /** - * TongYi models options. - */ - private final TongYiAudioSpeechOptions defaultOptions; - - /** - * TongYiAudioSpeechClient constructor. - * @param speechSynthesizer the speech synthesizer - */ - public TongYiAudioSpeechModel(SpeechSynthesizer speechSynthesizer) { - - this(speechSynthesizer, null); - } - - /** - * TongYiAudioSpeechClient constructor. - * @param speechSynthesizer the speech synthesizer - * @param tongYiAudioOptions the tongYi audio options - */ - public TongYiAudioSpeechModel(SpeechSynthesizer speechSynthesizer, TongYiAudioSpeechOptions tongYiAudioOptions) { - - Assert.notNull(speechSynthesizer, "speechSynthesizer must not be null"); - Assert.notNull(tongYiAudioOptions, "tongYiAudioOptions must not be null"); - - this.speechSynthesizer = speechSynthesizer; - this.defaultOptions = tongYiAudioOptions; - } - - /** - * Call the TongYi audio speech service. - * @param text the text message to be converted to audio. - * @return the audio byte buffer. - */ - @Override - public ByteBuffer call(String text) { - - var speechRequest = new SpeechPrompt(text); - - return call(speechRequest).getResult().getOutput(); - } - - - /** - * Call the TongYi audio speech service. - * @param prompt the speech prompt. - * @return the speech response. - */ - @Override - public SpeechResponse call(SpeechPrompt prompt) { - - var SCASpeechParam = merge(prompt.getOptions()); - var speechSynthesisParams = toSpeechSynthesisParams(SCASpeechParam); - speechSynthesisParams.setText(prompt.getInstructions().getText()); - logger.info(speechSynthesisParams.toString()); - - var res = speechSynthesizer.call(speechSynthesisParams); - - return convert(res, null); - } - - /** - * Call the TongYi audio speech service. - * @param prompt the speech prompt. - * @param callback the result callback. - * {@link SpeechSynthesizer#call(SpeechSynthesisParam, ResultCallback)} - */ - public void call(SpeechPrompt prompt, ResultCallback callback) { - - var SCASpeechParam = merge(prompt.getOptions()); - var speechSynthesisParams = toSpeechSynthesisParams(SCASpeechParam); - speechSynthesisParams.setText(prompt.getInstructions().getText()); - - speechSynthesizer.call(speechSynthesisParams, callback); - } - - /** - * Stream the TongYi audio speech service. - * @param prompt the speech prompt. - * @return the speech response. - * {@link SpeechSynthesizer#streamCall(SpeechSynthesisParam)} - */ - @Override - public Flux stream(SpeechPrompt prompt) { - - var SCASpeechParam = merge(prompt.getOptions()); - - Flowable resultFlowable = speechSynthesizer - .streamCall(toSpeechSynthesisParams(SCASpeechParam)); - - return Flux.from(resultFlowable) - .flatMap( - res -> Flux.just(res.getAudioFrame()) - .map(audio -> { - var speech = new Speech(audio); - var respMetadata = TongYiAudioSpeechResponseMetadata.from(res); - return new SpeechResponse(speech, respMetadata); - }) - ).publishOn(Schedulers.parallel()); - } - - public TongYiAudioSpeechOptions merge(TongYiAudioSpeechOptions target) { - - var mergeBuilder = TongYiAudioSpeechOptions.builder(); - - mergeBuilder.withModel(defaultOptions.getModel() != null ? defaultOptions.getModel() : target.getModel()); - mergeBuilder.withPitch(defaultOptions.getPitch() != null ? defaultOptions.getPitch() : target.getPitch()); - mergeBuilder.withRate(defaultOptions.getRate() != null ? defaultOptions.getRate() : target.getRate()); - mergeBuilder.withFormat(defaultOptions.getFormat() != null ? defaultOptions.getFormat() : target.getFormat()); - mergeBuilder.withSampleRate(defaultOptions.getSampleRate() != null ? defaultOptions.getSampleRate() : target.getSampleRate()); - mergeBuilder.withTextType(defaultOptions.getTextType() != null ? defaultOptions.getTextType() : target.getTextType()); - mergeBuilder.withVolume(defaultOptions.getVolume() != null ? defaultOptions.getVolume() : target.getVolume()); - mergeBuilder.withEnablePhonemeTimestamp(defaultOptions.isEnablePhonemeTimestamp() != null ? defaultOptions.isEnablePhonemeTimestamp() : target.isEnablePhonemeTimestamp()); - mergeBuilder.withEnableWordTimestamp(defaultOptions.isEnableWordTimestamp() != null ? defaultOptions.isEnableWordTimestamp() : target.isEnableWordTimestamp()); - - return mergeBuilder.build(); - } - - public SpeechSynthesisParam toSpeechSynthesisParams(TongYiAudioSpeechOptions source) { - - var mergeBuilder = SpeechSynthesisParam.builder(); - - mergeBuilder.model(source.getModel() != null ? source.getModel() : AudioSpeechModels.SAMBERT_ZHICHU_V1); - mergeBuilder.text(source.getText() != null ? source.getText() : ""); - - if (source.getFormat() != null) { - mergeBuilder.format(source.getFormat()); - } - if (source.getRate() != null) { - mergeBuilder.rate(source.getRate()); - } - if (source.getPitch() != null) { - mergeBuilder.pitch(source.getPitch()); - } - if (source.getTextType() != null) { - mergeBuilder.textType(source.getTextType()); - } - if (source.getSampleRate() != null) { - mergeBuilder.sampleRate(source.getSampleRate()); - } - if (source.isEnablePhonemeTimestamp() != null) { - mergeBuilder.enablePhonemeTimestamp(source.isEnablePhonemeTimestamp()); - } - if (source.isEnableWordTimestamp() != null) { - mergeBuilder.enableWordTimestamp(source.isEnableWordTimestamp()); - } - if (source.getVolume() != null) { - mergeBuilder.volume(source.getVolume()); - } - - return mergeBuilder.build(); - } - - /** - * Convert the TongYi audio speech service result to the speech response. - * @param result the audio byte buffer. - * @param synthesisResult the synthesis result. - * @return the speech response. - */ - private SpeechResponse convert(ByteBuffer result, SpeechSynthesisResult synthesisResult) { - - if (synthesisResult == null) { - - return new SpeechResponse(new Speech(result)); - } - - var responseMetadata = TongYiAudioSpeechResponseMetadata.from(synthesisResult); - var speech = new Speech(synthesisResult.getAudioFrame()); - - return new SpeechResponse(speech, responseMetadata); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechOptions.java deleted file mode 100644 index 09f188f8ea..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechOptions.java +++ /dev/null @@ -1,261 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.speech; - -import com.alibaba.cloud.ai.tongyi.audio.AudioSpeechModels; -import com.alibaba.dashscope.audio.tts.SpeechSynthesisAudioFormat; -import com.alibaba.dashscope.audio.tts.SpeechSynthesisTextType; -import org.springframework.ai.model.ModelOptions; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAudioSpeechOptions implements ModelOptions { - - - /** - * Audio Speech models. - */ - private String model = AudioSpeechModels.SAMBERT_ZHICHU_V1; - - /** - * Text content. - */ - private String text; - - /** - * Input text type. - */ - private SpeechSynthesisTextType textType = SpeechSynthesisTextType.PLAIN_TEXT; - - /** - * synthesis audio format. - */ - private SpeechSynthesisAudioFormat format = SpeechSynthesisAudioFormat.WAV; - - /** - * synthesis audio sample rate. - */ - private Integer sampleRate = 16000; - - /** - * synthesis audio volume. - */ - private Integer volume = 50; - - /** - * synthesis audio speed. - */ - private Float rate = 1.0f; - - /** - * synthesis audio pitch. - */ - private Float pitch = 1.0f; - - /** - * enable word level timestamp. - */ - private Boolean enableWordTimestamp = false; - - /** - * enable phoneme level timestamp. - */ - private Boolean enablePhonemeTimestamp = false; - - public static Builder builder() { - return new Builder(); - } - - public String getModel() { - - return model; - } - - public void setModel(String model) { - - this.model = model; - } - - public String getText() { - - return text; - } - - public void setText(String text) { - - this.text = text; - } - - public SpeechSynthesisTextType getTextType() { - - return textType; - } - - public void setTextType(SpeechSynthesisTextType textType) { - - this.textType = textType; - } - - public SpeechSynthesisAudioFormat getFormat() { - - return format; - } - - public void setFormat(SpeechSynthesisAudioFormat format) { - - this.format = format; - } - - public Integer getSampleRate() { - - return sampleRate; - } - - public void setSampleRate(Integer sampleRate) { - - this.sampleRate = sampleRate; - } - - public Integer getVolume() { - - return volume; - } - - public void setVolume(Integer volume) { - - this.volume = volume; - } - - public Float getRate() { - - return rate; - } - - public void setRate(Float rate) { - - this.rate = rate; - } - - public Float getPitch() { - - return pitch; - } - - public void setPitch(Float pitch) { - - this.pitch = pitch; - } - - public Boolean isEnableWordTimestamp() { - - return enableWordTimestamp; - } - - public void setEnableWordTimestamp(Boolean enableWordTimestamp) { - - this.enableWordTimestamp = enableWordTimestamp; - } - - public Boolean isEnablePhonemeTimestamp() { - - return enablePhonemeTimestamp; - } - - public void setEnablePhonemeTimestamp(Boolean enablePhonemeTimestamp) { - - this.enablePhonemeTimestamp = enablePhonemeTimestamp; - } - - /** - * Build a options instances. - */ - public static class Builder { - - private final TongYiAudioSpeechOptions options = new TongYiAudioSpeechOptions(); - - public Builder withModel(String model) { - - options.model = model; - return this; - } - - public Builder withText(String text) { - - options.text = text; - return this; - } - - public Builder withTextType(SpeechSynthesisTextType textType) { - - options.textType = textType; - return this; - } - - public Builder withFormat(SpeechSynthesisAudioFormat format) { - - options.format = format; - return this; - } - - public Builder withSampleRate(Integer sampleRate) { - - options.sampleRate = sampleRate; - return this; - } - - public Builder withVolume(Integer volume) { - - options.volume = volume; - return this; - } - - public Builder withRate(Float rate) { - - options.rate = rate; - return this; - } - - public Builder withPitch(Float pitch) { - - options.pitch = pitch; - return this; - } - - public Builder withEnableWordTimestamp(Boolean enableWordTimestamp) { - - options.enableWordTimestamp = enableWordTimestamp; - return this; - } - - public Builder withEnablePhonemeTimestamp(Boolean enablePhonemeTimestamp) { - - options.enablePhonemeTimestamp = enablePhonemeTimestamp; - return this; - } - - public TongYiAudioSpeechOptions build() { - - return options; - } - - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechProperties.java deleted file mode 100644 index a3b4357709..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechProperties.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.speech; - -import com.alibaba.cloud.ai.tongyi.audio.AudioSpeechModels; -import com.alibaba.dashscope.audio.tts.SpeechSynthesisAudioFormat; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION; - -/** - * TongYi audio speech configuration properties. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -@ConfigurationProperties(TongYiAudioSpeechProperties.CONFIG_PREFIX) -public class TongYiAudioSpeechProperties { - - /** - * Spring Cloud Alibaba AI configuration prefix. - */ - public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "audio.speech"; - /** - * Default TongYi Chat model. - */ - public static final String DEFAULT_AUDIO_MODEL_NAME = AudioSpeechModels.SAMBERT_ZHICHU_V1; - - /** - * Enable TongYiQWEN ai audio client. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private TongYiAudioSpeechOptions options = TongYiAudioSpeechOptions.builder() - .withModel(DEFAULT_AUDIO_MODEL_NAME) - .withFormat(SpeechSynthesisAudioFormat.WAV) - .build(); - - public TongYiAudioSpeechOptions getOptions() { - - return this.options; - } - - public void setOptions(TongYiAudioSpeechOptions options) { - - this.options = options; - } - - public boolean isEnabled() { - - return this.enabled; - } - - public void setEnabled(boolean enabled) { - - this.enabled = enabled; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/Speech.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/Speech.java deleted file mode 100644 index adeef4e799..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/Speech.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.speech.api; - -import org.springframework.ai.model.ModelResult; -import org.springframework.lang.Nullable; - -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Objects; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class Speech implements ModelResult { - - private final ByteBuffer audio; - - private SpeechMetadata speechMetadata; - - public Speech(ByteBuffer audio) { - this.audio = audio; - } - - @Override - public ByteBuffer getOutput() { - return this.audio; - } - - @Override - public SpeechMetadata getMetadata() { - - return speechMetadata != null ? speechMetadata : SpeechMetadata.NULL; - } - - public Speech withSpeechMetadata(@Nullable SpeechMetadata speechMetadata) { - - this.speechMetadata = speechMetadata; - return this; - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - - return true; - } - if (!(o instanceof Speech that)) { - - return false; - } - - return Arrays.equals(audio.array(), that.audio.array()) - && Objects.equals(speechMetadata, that.speechMetadata); - } - - @Override - public int hashCode() { - - return Objects.hash(Arrays.hashCode(audio.array()), speechMetadata); - } - - @Override - public String toString() { - - return "Speech{" + "text=" + audio + ", speechMetadata=" + speechMetadata + '}'; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMessage.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMessage.java deleted file mode 100644 index 9748bcf507..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMessage.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.speech.api; - -import java.util.Objects; - -/** - * The {@link SpeechMessage} class represents a single text message to - * be converted to speech by the TongYi LLM TTS. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class SpeechMessage { - - private String text; - - /** - * Constructs a new {@link SpeechMessage} object with the given text. - * @param text the text to be converted to speech - */ - public SpeechMessage(String text) { - this.text = text; - } - - /** - * Returns the text of this speech message. - * @return the text of this speech message - */ - public String getText() { - return text; - } - - /** - * Sets the text of this speech message. - * @param text the new text for this speech message - */ - public void setText(String text) { - this.text = text; - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - - return true; - } - - if (!(o instanceof SpeechMessage that)) { - - return false; - } - - return Objects.equals(text, that.text); - } - - @Override - public int hashCode() { - - return Objects.hash(text); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMetadata.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMetadata.java deleted file mode 100644 index 513a2839c1..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMetadata.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.speech.api; - -import org.springframework.ai.model.ResultMetadata; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public interface SpeechMetadata extends ResultMetadata { - - /** - * Null Object. - */ - SpeechMetadata NULL = SpeechMetadata.create(); - - /** - * Factory method used to construct a new {@link SpeechMetadata}. - * @return a new {@link SpeechMetadata} - */ - static SpeechMetadata create() { - return new SpeechMetadata() { - }; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechModel.java deleted file mode 100644 index 601dce72cf..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechModel.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.speech.api; - -import org.springframework.ai.model.Model; - -import java.nio.ByteBuffer; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.0.0-RC1 - */ - -@FunctionalInterface -public interface SpeechModel extends Model { - - /** - * Generates spoken audio from the provided text message. - * @param message the text message to be converted to audio. - * @return the resulting audio bytes. - */ - default ByteBuffer call(String message) { - - SpeechPrompt prompt = new SpeechPrompt(message); - - return call(prompt).getResult().getOutput(); - } - - /** - * Sends a speech request to the TongYi TTS API and returns the resulting speech response. - * @param request the speech prompt containing the input text and other parameters. - * @return the speech response containing the generated audio. - */ - SpeechResponse call(SpeechPrompt request); - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechPrompt.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechPrompt.java deleted file mode 100644 index dc89ff31a6..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechPrompt.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.speech.api; - -import com.alibaba.cloud.ai.tongyi.audio.speech.TongYiAudioSpeechOptions; -import org.springframework.ai.model.ModelRequest; - -import java.util.Objects; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class SpeechPrompt implements ModelRequest { - - private TongYiAudioSpeechOptions speechOptions; - - private final SpeechMessage message; - - public SpeechPrompt(String instructions) { - - this(new SpeechMessage(instructions), TongYiAudioSpeechOptions.builder().build()); - } - - public SpeechPrompt(String instructions, TongYiAudioSpeechOptions speechOptions) { - - this(new SpeechMessage(instructions), speechOptions); - } - - public SpeechPrompt(SpeechMessage speechMessage) { - this(speechMessage, TongYiAudioSpeechOptions.builder().build()); - } - - public SpeechPrompt(SpeechMessage speechMessage, TongYiAudioSpeechOptions speechOptions) { - - this.message = speechMessage; - this.speechOptions = speechOptions; - } - - @Override - public SpeechMessage getInstructions() { - return this.message; - } - - @Override - public TongYiAudioSpeechOptions getOptions() { - - return speechOptions; - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - - return true; - } - if (!(o instanceof SpeechPrompt that)) { - - return false; - } - - return Objects.equals(speechOptions, that.speechOptions) && Objects.equals(message, that.message); - } - - @Override - public int hashCode() { - - return Objects.hash(speechOptions, message); - } - - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechResponse.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechResponse.java deleted file mode 100644 index b8d7484cf7..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechResponse.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.speech.api; - -import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioSpeechResponseMetadata; -import org.springframework.ai.model.ModelResponse; - -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class SpeechResponse implements ModelResponse { - - private final Speech speech; - - private final TongYiAudioSpeechResponseMetadata speechResponseMetadata; - - /** - * Creates a new instance of SpeechResponse with the given speech result. - * @param speech the speech result to be set in the SpeechResponse - * @see Speech - */ - public SpeechResponse(Speech speech) { - this(speech, TongYiAudioSpeechResponseMetadata.NULL); - } - - /** - * Creates a new instance of SpeechResponse with the given speech result and speech - * response metadata. - * @param speech the speech result to be set in the SpeechResponse - * @param speechResponseMetadata the speech response metadata to be set in the - * SpeechResponse - * @see Speech - * @see TongYiAudioSpeechResponseMetadata - */ - public SpeechResponse(Speech speech, TongYiAudioSpeechResponseMetadata speechResponseMetadata) { - - this.speech = speech; - this.speechResponseMetadata = speechResponseMetadata; - } - - @Override - public Speech getResult() { - return speech; - } - - @Override - public List getResults() { - return Collections.singletonList(speech); - } - - @Override - public TongYiAudioSpeechResponseMetadata getMetadata() { - return speechResponseMetadata; - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - - return true; - } - if (!(o instanceof SpeechResponse that)) { - - return false; - } - - return Objects.equals(speech, that.speech) - && Objects.equals(speechResponseMetadata, that.speechResponseMetadata); - } - - @Override - public int hashCode() { - - return Objects.hash(speech, speechResponseMetadata); - } - - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechStreamModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechStreamModel.java deleted file mode 100644 index d39c79ab2f..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechStreamModel.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.speech.api; - -import org.springframework.ai.model.StreamingModel; -import reactor.core.publisher.Flux; - -import java.nio.ByteBuffer; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -@FunctionalInterface -public interface SpeechStreamModel extends StreamingModel { - - /** - * Generates a stream of audio bytes from the provided text message. - * - * @param message the text message to be converted to audio - * @return a Flux of audio bytes representing the generated speech - */ - default Flux stream(String message) { - - SpeechPrompt prompt = new SpeechPrompt(message); - return stream(prompt).map(SpeechResponse::getResult).map(Speech::getOutput); - } - - /** - * Sends a speech request to the TongYi TTS API and returns a stream of the resulting - * speech responses. - * @param prompt the speech prompt containing the input text and other parameters. - * @return a Flux of speech responses, each containing a portion of the generated audio. - */ - @Override - Flux stream(SpeechPrompt prompt); - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java deleted file mode 100644 index 0f0dca9c02..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.transcription; - -import cn.hutool.core.collection.ListUtil; -import com.alibaba.cloud.ai.tongyi.audio.AudioTranscriptionModels; -import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionPrompt; -import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionResponse; -import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionResult; -import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException; -import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioTranscriptionResponseMetadata; -import com.alibaba.dashscope.audio.asr.transcription.*; -import org.springframework.ai.model.Model; -import org.springframework.core.io.Resource; -import org.springframework.util.Assert; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -/** - * TongYiAudioTranscriptionModel is a client for TongYi audio transcription service for - * Spring Cloud Alibaba AI. - * @author xYLiu - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAudioTranscriptionModel - implements Model { - - /** - * TongYi models options. - */ - private final TongYiAudioTranscriptionOptions defaultOptions; - - /** - * TongYi models api. - */ - private final Transcription transcription; - - public TongYiAudioTranscriptionModel(Transcription transcription) { - this(null, transcription); - } - - public TongYiAudioTranscriptionModel(TongYiAudioTranscriptionOptions defaultOptions, - Transcription transcription) { - Assert.notNull(transcription, "transcription must not be null"); - Assert.notNull(defaultOptions, "defaultOptions must not be null"); - - this.defaultOptions = defaultOptions; - this.transcription = transcription; - } - - @Override - public AudioTranscriptionResponse call(AudioTranscriptionPrompt prompt) { - - TranscriptionParam transcriptionParam; - - if (prompt.getOptions() != null) { - var param = merge(prompt.getOptions()); - transcriptionParam = toTranscriptionParam(param); - transcriptionParam.setFileUrls(prompt.getOptions().getFileUrls()); - } - else { - Resource instructions = prompt.getInstructions(); - try { - transcriptionParam = TranscriptionParam.builder() - .model(AudioTranscriptionModels.Paraformer_V1) - .fileUrls(ListUtil.of(String.valueOf(instructions.getURL()))) - .build(); - } - catch (IOException e) { - throw new TongYiException("Failed to create resource", e); - } - } - - List taskResultList; - try { - // Submit a transcription request - TranscriptionResult result = transcription.asyncCall(transcriptionParam); - // Wait for the transcription to complete - result = transcription.wait(TranscriptionQueryParam - .FromTranscriptionParam(transcriptionParam, result.getTaskId())); - // Get the transcription results - System.out.println(result.getOutput().getAsJsonObject()); - taskResultList = result.getResults(); - System.out.println(Arrays.toString(taskResultList.toArray())); - - return new AudioTranscriptionResponse( - taskResultList.stream().map(taskResult -> - new AudioTranscriptionResult(taskResult.getTranscriptionUrl()) - ).collect(Collectors.toList()), - TongYiAudioTranscriptionResponseMetadata.from(result) - ); - } - catch (Exception e) { - throw new TongYiException("Failed to call audio transcription", e); - } - - } - - public TongYiAudioTranscriptionOptions merge(TongYiAudioTranscriptionOptions target) { - var mergeBuilder = TongYiAudioTranscriptionOptions.builder(); - - mergeBuilder - .withModel(defaultOptions.getModel() != null ? defaultOptions.getModel() - : target.getModel()); - mergeBuilder.withChannelId( - defaultOptions.getChannelId() != null ? defaultOptions.getChannelId() - : target.getChannelId()); - mergeBuilder.withDiarizationEnabled(defaultOptions.getDiarizationEnabled() != null - ? defaultOptions.getDiarizationEnabled() - : target.getDiarizationEnabled()); - mergeBuilder.withDisfluencyRemovalEnabled( - defaultOptions.getDisfluencyRemovalEnabled() != null - ? defaultOptions.getDisfluencyRemovalEnabled() - : target.getDisfluencyRemovalEnabled()); - mergeBuilder.withTimestampAlignmentEnabled( - defaultOptions.getTimestampAlignmentEnabled() != null - ? defaultOptions.getTimestampAlignmentEnabled() - : target.getTimestampAlignmentEnabled()); - mergeBuilder.withSpecialWordFilter(defaultOptions.getSpecialWordFilter() != null - ? defaultOptions.getSpecialWordFilter() - : target.getSpecialWordFilter()); - mergeBuilder.withAudioEventDetectionEnabled( - defaultOptions.getAudioEventDetectionEnabled() != null - ? defaultOptions.getAudioEventDetectionEnabled() - : target.getAudioEventDetectionEnabled()); - - return mergeBuilder.build(); - } - - public TranscriptionParam toTranscriptionParam( - TongYiAudioTranscriptionOptions source) { - var mergeBuilder = TranscriptionParam.builder(); - - mergeBuilder.model(source.getModel() != null ? source.getModel() - : AudioTranscriptionModels.Paraformer_V1); - mergeBuilder.fileUrls( - source.getFileUrls() != null ? source.getFileUrls() : new ArrayList<>()); - if (source.getPhraseId() != null) { - mergeBuilder.phraseId(source.getPhraseId()); - } - if (source.getChannelId() != null) { - mergeBuilder.channelId(source.getChannelId()); - } - if (source.getDiarizationEnabled() != null) { - mergeBuilder.diarizationEnabled(source.getDiarizationEnabled()); - } - if (source.getSpeakerCount() != null) { - mergeBuilder.speakerCount(source.getSpeakerCount()); - } - if (source.getDisfluencyRemovalEnabled() != null) { - mergeBuilder.disfluencyRemovalEnabled(source.getDisfluencyRemovalEnabled()); - } - if (source.getTimestampAlignmentEnabled() != null) { - mergeBuilder.timestampAlignmentEnabled(source.getTimestampAlignmentEnabled()); - } - if (source.getSpecialWordFilter() != null) { - mergeBuilder.specialWordFilter(source.getSpecialWordFilter()); - } - if (source.getAudioEventDetectionEnabled() != null) { - mergeBuilder - .audioEventDetectionEnabled(source.getAudioEventDetectionEnabled()); - } - - return mergeBuilder.build(); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionOptions.java deleted file mode 100644 index 3fec6375ce..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionOptions.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.transcription; - -import com.alibaba.cloud.ai.tongyi.audio.AudioTranscriptionModels; -import org.springframework.ai.model.ModelOptions; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * @author xYLiu - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAudioTranscriptionOptions implements ModelOptions { - - private String model = AudioTranscriptionModels.Paraformer_V1; - - private List fileUrls = new ArrayList<>(); - - private String phraseId = null; - - private List channelId = Collections.singletonList(0); - - private Boolean diarizationEnabled = false; - - private Integer speakerCount = null; - - private Boolean disfluencyRemovalEnabled = false; - - private Boolean timestampAlignmentEnabled = false; - - private String specialWordFilter = ""; - - private Boolean audioEventDetectionEnabled = false; - - public static Builder builder() { - - return new Builder(); - } - - public String getModel() { - return model; - } - - public void setModel(String model) { - this.model = model; - } - - public List getFileUrls() { - return fileUrls; - } - - public void setFileUrls(List fileUrls) { - this.fileUrls = fileUrls; - } - - public String getPhraseId() { - return phraseId; - } - - public void setPhraseId(String phraseId) { - this.phraseId = phraseId; - } - - public List getChannelId() { - return channelId; - } - - public void setChannelId(List channelId) { - this.channelId = channelId; - } - - public Boolean getDiarizationEnabled() { - return diarizationEnabled; - } - - public void setDiarizationEnabled(Boolean diarizationEnabled) { - this.diarizationEnabled = diarizationEnabled; - } - - public Integer getSpeakerCount() { - return speakerCount; - } - - public void setSpeakerCount(Integer speakerCount) { - this.speakerCount = speakerCount; - } - - public Boolean getDisfluencyRemovalEnabled() { - return disfluencyRemovalEnabled; - } - - public void setDisfluencyRemovalEnabled(Boolean disfluencyRemovalEnabled) { - this.disfluencyRemovalEnabled = disfluencyRemovalEnabled; - } - - public Boolean getTimestampAlignmentEnabled() { - return timestampAlignmentEnabled; - } - - public void setTimestampAlignmentEnabled(Boolean timestampAlignmentEnabled) { - this.timestampAlignmentEnabled = timestampAlignmentEnabled; - } - - public String getSpecialWordFilter() { - return specialWordFilter; - } - - public void setSpecialWordFilter(String specialWordFilter) { - this.specialWordFilter = specialWordFilter; - } - - public Boolean getAudioEventDetectionEnabled() { - return audioEventDetectionEnabled; - } - - public void setAudioEventDetectionEnabled(Boolean audioEventDetectionEnabled) { - this.audioEventDetectionEnabled = audioEventDetectionEnabled; - } - - /** - * Builder class for constructing TongYiAudioTranscriptionOptions instances. - */ - public static class Builder { - - private final TongYiAudioTranscriptionOptions options = new TongYiAudioTranscriptionOptions(); - - public Builder withModel(String model) { - options.model = model; - return this; - } - - public Builder withFileUrls(List fileUrls) { - options.fileUrls = fileUrls; - return this; - } - - public Builder withPhraseId(String phraseId) { - options.phraseId = phraseId; - return this; - } - - public Builder withChannelId(List channelId) { - options.channelId = channelId; - return this; - } - - public Builder withDiarizationEnabled(Boolean diarizationEnabled) { - options.diarizationEnabled = diarizationEnabled; - return this; - } - - public Builder withSpeakerCount(Integer speakerCount) { - options.speakerCount = speakerCount; - return this; - } - - public Builder withDisfluencyRemovalEnabled(Boolean disfluencyRemovalEnabled) { - options.disfluencyRemovalEnabled = disfluencyRemovalEnabled; - return this; - } - - public Builder withTimestampAlignmentEnabled(Boolean timestampAlignmentEnabled) { - options.timestampAlignmentEnabled = timestampAlignmentEnabled; - return this; - } - - public Builder withSpecialWordFilter(String specialWordFilter) { - options.specialWordFilter = specialWordFilter; - return this; - } - - public Builder withAudioEventDetectionEnabled( - Boolean audioEventDetectionEnabled) { - options.audioEventDetectionEnabled = audioEventDetectionEnabled; - return this; - } - - public TongYiAudioTranscriptionOptions build() { - // Perform any necessary validation here before returning the built object - return options; - } - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionProperties.java deleted file mode 100644 index fe86d0d72b..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionProperties.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.transcription; - -import com.alibaba.cloud.ai.tongyi.audio.AudioTranscriptionModels; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION; - -/** - * @author xYLiu - * @author yuluo - * @since 2023.0.1.0 - */ - -@ConfigurationProperties(TongYiAudioTranscriptionProperties.CONFIG_PREFIX) -public class TongYiAudioTranscriptionProperties { - - /** - * Spring Cloud Alibaba AI configuration prefix. - */ - public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "audio.transcription"; - - /** - * Default TongYi Chat model. - */ - public static final String DEFAULT_AUDIO_MODEL_NAME = AudioTranscriptionModels.Paraformer_V1; - - /** - * Enable TongYiQWEN ai audio client. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private TongYiAudioTranscriptionOptions options = TongYiAudioTranscriptionOptions - .builder().withModel(DEFAULT_AUDIO_MODEL_NAME).build(); - - public TongYiAudioTranscriptionOptions getOptions() { - - return this.options; - } - - public void setOptions(TongYiAudioTranscriptionOptions options) { - - this.options = options; - } - - public boolean isEnabled() { - - return this.enabled; - } - - public void setEnabled(boolean enabled) { - - this.enabled = enabled; - } -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionPrompt.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionPrompt.java deleted file mode 100644 index f4c2882456..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionPrompt.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.transcription.api; - -import com.alibaba.cloud.ai.tongyi.audio.transcription.TongYiAudioTranscriptionOptions; -import org.springframework.ai.model.ModelRequest; -import org.springframework.core.io.Resource; - -/** - * @author xYLiu - * @author yuluo - * @since 2023.0.1.0 - */ - -public class AudioTranscriptionPrompt implements ModelRequest { - - private Resource audioResource; - - private TongYiAudioTranscriptionOptions transcriptionOptions; - - public AudioTranscriptionPrompt(Resource resource) { - this.audioResource = resource; - } - - public AudioTranscriptionPrompt(Resource resource, TongYiAudioTranscriptionOptions transcriptionOptions) { - this.audioResource = resource; - this.transcriptionOptions = transcriptionOptions; - } - - @Override - public Resource getInstructions() { - - return audioResource; - } - - @Override - public TongYiAudioTranscriptionOptions getOptions() { - - return transcriptionOptions; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResponse.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResponse.java deleted file mode 100644 index 2e740158bb..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResponse.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.transcription.api; - -import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioTranscriptionResponseMetadata; -import org.springframework.ai.model.ModelResponse; -import org.springframework.ai.model.ResponseMetadata; - -import java.util.List; - -/** - * @author xYLiu - * @author yuluo - * @since 2023.0.1.0 - */ - -public class AudioTranscriptionResponse implements ModelResponse { - - private List resultList; - - private TongYiAudioTranscriptionResponseMetadata transcriptionResponseMetadata; - - public AudioTranscriptionResponse(List result) { - - this(result, TongYiAudioTranscriptionResponseMetadata.NULL); - } - - public AudioTranscriptionResponse(List result, - TongYiAudioTranscriptionResponseMetadata transcriptionResponseMetadata) { - - this.resultList = List.copyOf(result); - this.transcriptionResponseMetadata = transcriptionResponseMetadata; - } - - @Override - public AudioTranscriptionResult getResult() { - - return this.resultList.get(0); - } - - @Override - public List getResults() { - - return this.resultList; - } - - @Override - public ResponseMetadata getMetadata() { - - return this.transcriptionResponseMetadata; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResult.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResult.java deleted file mode 100644 index 651855caf2..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResult.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.audio.transcription.api; - -import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioTranscriptionMetadata; -import org.springframework.ai.model.ModelResult; -import org.springframework.ai.model.ResultMetadata; - -import java.util.Objects; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ -public class AudioTranscriptionResult implements ModelResult { - - private String text; - - private TongYiAudioTranscriptionMetadata transcriptionMetadata; - - public AudioTranscriptionResult(String text) { - this.text = text; - } - - @Override - public String getOutput() { - - return this.text; - } - - @Override - public ResultMetadata getMetadata() { - - return transcriptionMetadata != null ? transcriptionMetadata : TongYiAudioTranscriptionMetadata.NULL; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AudioTranscriptionResult that = (AudioTranscriptionResult) o; - return Objects.equals(text, that.text) && Objects.equals(transcriptionMetadata, that.transcriptionMetadata); - } - - @Override - public int hashCode() { - return Objects.hash(text, transcriptionMetadata); - } -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java deleted file mode 100644 index 11328a02e5..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java +++ /dev/null @@ -1,483 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.chat; - -import cn.hutool.core.collection.ListUtil; -import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException; -import com.alibaba.dashscope.aigc.conversation.ConversationParam; -import com.alibaba.dashscope.aigc.generation.Generation; -import com.alibaba.dashscope.aigc.generation.GenerationOutput; -import com.alibaba.dashscope.aigc.generation.GenerationResult; -import com.alibaba.dashscope.common.MessageManager; -import com.alibaba.dashscope.common.Role; -import com.alibaba.dashscope.exception.InputRequiredException; -import com.alibaba.dashscope.exception.NoApiKeyException; -import com.alibaba.dashscope.tools.FunctionDefinition; -import com.alibaba.dashscope.tools.ToolCallBase; -import com.alibaba.dashscope.tools.ToolCallFunction; -import com.alibaba.dashscope.utils.ApiKeywords; -import com.alibaba.dashscope.utils.JsonUtils; -import io.reactivex.Flowable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.metadata.ChatGenerationMetadata; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.StreamingChatModel; -import org.springframework.ai.chat.prompt.ChatOptions; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.ModelOptionsUtils; -import org.springframework.ai.model.function.AbstractFunctionCallSupport; -import org.springframework.ai.model.function.FunctionCallbackContext; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.util.CollectionUtils; -import reactor.core.publisher.Flux; -import reactor.core.scheduler.Schedulers; - -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; - - -/** - * {@link ChatModel} and {@link StreamingChatModel} implementation for {@literal Alibaba DashScope} - * backed by {@link Generation}. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - * @see ChatModel - * @see com.alibaba.dashscope.aigc.generation - */ - -public class TongYiChatModel extends - AbstractFunctionCallSupport< - com.alibaba.dashscope.common.Message, - ConversationParam, - GenerationResult> - implements ChatModel, StreamingChatModel { - - private static final Logger logger = LoggerFactory.getLogger(TongYiChatModel.class); - - /** - * DashScope generation client. - */ - private final Generation generation; - - /** - * The TongYi models default chat completion api. - */ - private TongYiChatOptions defaultOptions; - - /** - * User role message manager. - */ - @Autowired - private MessageManager msgManager; - - /** - * Initializes an instance of the TongYiChatClient. - * @param generation DashScope generation client. - */ - public TongYiChatModel(Generation generation) { - - this(generation, - TongYiChatOptions.builder() - .withTopP(0.8) - .withEnableSearch(true) - .withResultFormat(ConversationParam.ResultFormat.MESSAGE) - .build(), - null - ); - } - - /** - * Initializes an instance of the TongYiChatClient. - * @param generation DashScope generation client. - * @param options TongYi model params. - */ - public TongYiChatModel(Generation generation, TongYiChatOptions options) { - - this(generation, options, null); - } - - /** - * Create a TongYi models client. - * @param generation DashScope model generation client. - * @param options TongYi default chat completion api. - */ - public TongYiChatModel(Generation generation, TongYiChatOptions options, - FunctionCallbackContext functionCallbackContext) { - - super(functionCallbackContext); - this.generation = generation; - this.defaultOptions = options; - } - - /** - * Get default sca chat options. - * - * @return TongYiChatOptions default object. - */ - public TongYiChatOptions getDefaultOptions() { - - return this.defaultOptions; - } - - @Override - public ChatResponse call(Prompt prompt) { - - ConversationParam params = toTongYiChatParams(prompt); - - // TongYi models context loader. - com.alibaba.dashscope.common.Message message = new com.alibaba.dashscope.common.Message(); - message.setRole(Role.USER.getValue()); - message.setContent(prompt.getContents()); - msgManager.add(message); - params.setMessages(msgManager.get()); - - logger.trace("TongYi ConversationOptions: {}", params); - GenerationResult chatCompletions = this.callWithFunctionSupport(params); - logger.trace("TongYi ConversationOptions: {}", params); - - msgManager.add(chatCompletions); - - List generations = - chatCompletions - .getOutput() - .getChoices() - .stream() - .map(choice -> - new org.springframework.ai.chat.model.Generation( - choice - .getMessage() - .getContent() - ).withGenerationMetadata(generateChoiceMetadata(choice) - )) - .toList(); - - return new ChatResponse(generations); - - } - - @Override - public Flux stream(Prompt prompt) { - - Flowable genRes; - ConversationParam tongYiChatParams = toTongYiChatParams(prompt); - - // See https://help.aliyun.com/zh/dashscope/developer-reference/api-details?spm=a2c4g.11186623.0.0.655fc11aRR0jj7#b9ad0a10cfhpe - // tongYiChatParams.setIncrementalOutput(true); - - try { - genRes = generation.streamCall(tongYiChatParams); - } - catch (NoApiKeyException | InputRequiredException e) { - logger.warn("TongYi chat client: " + e.getMessage()); - throw new TongYiException(e.getMessage()); - } - - return Flux.from(genRes) - .flatMap( - message -> Flux.just( - message.getOutput() - .getChoices() - .get(0) - .getMessage() - .getContent()) - .map(content -> { - var gen = new org.springframework.ai.chat.model.Generation(content) - .withGenerationMetadata(generateChoiceMetadata( - message.getOutput() - .getChoices() - .get(0) - )); - return new ChatResponse(ListUtil.of(gen)); - }) - ) - .publishOn(Schedulers.parallel()); - - } - - /** - * Configuration properties to Qwen model params. - * Test access. - * - * @param prompt {@link Prompt} - * @return Qwen models params {@link ConversationParam} - */ - public ConversationParam toTongYiChatParams(Prompt prompt) { - - Set functionsForThisRequest = new HashSet<>(); - - List tongYiMessage = prompt.getInstructions().stream() - .map(this::fromSpringAIMessage) - .toList(); - - ConversationParam chatParams = ConversationParam.builder() - .messages(tongYiMessage) - // models setting - // {@link HalfDuplexServiceParam#models} - .model(Generation.Models.QWEN_TURBO) - // {@link GenerationOutput} - .resultFormat(ConversationParam.ResultFormat.MESSAGE) - .incrementalOutput(true) - - .build(); - - if (this.defaultOptions != null) { - - chatParams = merge(chatParams, this.defaultOptions); - Set defaultEnabledFunctions = this.handleFunctionCallbackConfigurations(this.defaultOptions, !IS_RUNTIME_CALL); - functionsForThisRequest.addAll(defaultEnabledFunctions); - } - if (prompt.getOptions() != null) { - if (prompt.getOptions() instanceof ChatOptions runtimeOptions) { - TongYiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, - ChatOptions.class, TongYiChatOptions.class); - - chatParams = merge(updatedRuntimeOptions, chatParams); - - Set promptEnabledFunctions = this.handleFunctionCallbackConfigurations(updatedRuntimeOptions, - IS_RUNTIME_CALL); - functionsForThisRequest.addAll(promptEnabledFunctions); - - } - else { - throw new IllegalArgumentException("Prompt options are not of type ConversationParam:" - + prompt.getOptions().getClass().getSimpleName()); - } - } - - // Add the enabled functions definitions to the request's tools parameter. - - if (!CollectionUtils.isEmpty(functionsForThisRequest)) { - List tools = this.getFunctionTools(functionsForThisRequest); - - // todo chatParams.setTools(tools) - } - - return chatParams; - } - - private ChatGenerationMetadata generateChoiceMetadata(GenerationOutput.Choice choice) { - - return ChatGenerationMetadata.from( - String.valueOf(choice.getFinishReason()), - choice.getMessage().getContent() - ); - } - - private List getFunctionTools(Set functionNames) { - return this.resolveFunctionCallbacks(functionNames).stream().map(functionCallback -> { - - FunctionDefinition functionDefinition = FunctionDefinition.builder() - .name(functionCallback.getName()) - .description(functionCallback.getDescription()) - .parameters(JsonUtils.parametersToJsonObject( - ModelOptionsUtils.jsonToMap(functionCallback.getInputTypeSchema()) - )) - .build(); - - return functionDefinition; - }).toList(); - } - - - private ConversationParam merge(ConversationParam tongYiParams, TongYiChatOptions scaChatParams) { - - if (scaChatParams == null) { - - return tongYiParams; - } - - return ConversationParam.builder() - .messages(tongYiParams.getMessages()) - .maxTokens((tongYiParams.getMaxTokens() != null) ? tongYiParams.getMaxTokens() : scaChatParams.getMaxTokens()) - // When merge options. Because ConversationParams is must not null. So is setting. - .model(scaChatParams.getModel()) - .resultFormat((tongYiParams.getResultFormat() != null) ? tongYiParams.getResultFormat() : scaChatParams.getResultFormat()) - .enableSearch((tongYiParams.getEnableSearch() != null) ? tongYiParams.getEnableSearch() : scaChatParams.getEnableSearch()) - .topK((tongYiParams.getTopK() != null) ? tongYiParams.getTopK() : scaChatParams.getTopK()) - .topP((tongYiParams.getTopP() != null) ? tongYiParams.getTopP() : scaChatParams.getTopP()) - .incrementalOutput((tongYiParams.getIncrementalOutput() != null) ? tongYiParams.getIncrementalOutput() : scaChatParams.getIncrementalOutput()) - .temperature((tongYiParams.getTemperature() != null) ? tongYiParams.getTemperature() : scaChatParams.getTemperature()) - .repetitionPenalty((tongYiParams.getRepetitionPenalty() != null) ? tongYiParams.getRepetitionPenalty() : scaChatParams.getRepetitionPenalty()) - .seed((tongYiParams.getSeed() != null) ? tongYiParams.getSeed() : scaChatParams.getSeed()) - .build(); - - } - - private ConversationParam merge(TongYiChatOptions scaChatParams, ConversationParam tongYiParams) { - - if (scaChatParams == null) { - - return tongYiParams; - } - - ConversationParam mergedTongYiParams = ConversationParam.builder() - .model(Generation.Models.QWEN_TURBO) - .messages(tongYiParams.getMessages()) - .build(); - mergedTongYiParams = merge(tongYiParams, scaChatParams); - - if (scaChatParams.getMaxTokens() != null) { - mergedTongYiParams.setMaxTokens(scaChatParams.getMaxTokens()); - } - - if (scaChatParams.getStop() != null) { - mergedTongYiParams.setStopStrings(scaChatParams.getStop()); - } - - if (scaChatParams.getTemperature() != null) { - mergedTongYiParams.setTemperature(scaChatParams.getTemperature()); - } - - if (scaChatParams.getTopK() != null) { - mergedTongYiParams.setTopK(scaChatParams.getTopK()); - } - - if (scaChatParams.getTopK() != null) { - mergedTongYiParams.setTopK(scaChatParams.getTopK()); - } - - return mergedTongYiParams; - } - - private com.alibaba.dashscope.common.Message fromSpringAIMessage(Message message) { - - return switch (message.getMessageType()) { - case USER -> com.alibaba.dashscope.common.Message.builder() - .role(Role.USER.getValue()) - .content(message.getContent()) - .build(); - case SYSTEM -> com.alibaba.dashscope.common.Message.builder() - .role(Role.SYSTEM.getValue()) - .content(message.getContent()) - .build(); - case ASSISTANT -> com.alibaba.dashscope.common.Message.builder() - .role(Role.ASSISTANT.getValue()) - .content(message.getContent()) - .build(); - default -> throw new IllegalArgumentException("Unknown message type " + message.getMessageType()); - }; - - } - - @Override - protected ConversationParam doCreateToolResponseRequest( - ConversationParam previousRequest, - com.alibaba.dashscope.common.Message responseMessage, - List conversationHistory - ) { - for (ToolCallBase toolCall : responseMessage.getToolCalls()) { - if (toolCall instanceof ToolCallFunction toolCallFunction) { - if (toolCallFunction.getFunction() != null) { - var functionName = toolCallFunction.getFunction().getName(); - var functionArguments = toolCallFunction.getFunction().getArguments(); - - if (!this.functionCallbackRegister.containsKey(functionName)) { - throw new IllegalStateException("No function callback found for function name: " + functionName); - } - - String functionResponse = this.functionCallbackRegister.get(functionName).call(functionArguments); - - // Add the function response to the conversation. - conversationHistory - .add(com.alibaba.dashscope.common.Message.builder() - .content(functionResponse) - .role(Role.BOT.getValue()) - .toolCallId(toolCall.getId()) - .build() - ); - } - } - - } - - ConversationParam newRequest = ConversationParam.builder().messages(conversationHistory).build(); - - // todo: No @JsonProperty fields. - newRequest = ModelOptionsUtils.merge(newRequest, previousRequest, ConversationParam.class); - - return newRequest; - - } - - @Override - protected List doGetUserMessages(ConversationParam request) { - - return request.getMessages(); - } - - @Override - protected com.alibaba.dashscope.common.Message doGetToolResponseMessage(GenerationResult response) { - - var message = response.getOutput().getChoices().get(0).getMessage(); - var assistantMessage = com.alibaba.dashscope.common.Message.builder().role(Role.ASSISTANT.getValue()) - .content("").build(); - assistantMessage.setToolCalls(message.getToolCalls()); - - return assistantMessage; - } - - @Override - protected GenerationResult doChatCompletion(ConversationParam request) { - - GenerationResult result; - try { - result = generation.call(request); - } - catch (NoApiKeyException | InputRequiredException e) { - throw new RuntimeException(e); - } - - return result; - } - - @Override - protected Flux doChatCompletionStream(ConversationParam request) { - final Flowable genRes; - try { - genRes = generation.streamCall(request); - } - catch (NoApiKeyException | InputRequiredException e) { - logger.warn("TongYi chat client: " + e.getMessage()); - throw new TongYiException(e.getMessage()); - } - return Flux.from(genRes); - - } - - @Override - protected boolean isToolFunctionCall(GenerationResult response) { - - if (response == null || CollectionUtils.isEmpty(response.getOutput().getChoices())) { - - return false; - } - var choice = response.getOutput().getChoices().get(0); - if (choice == null || choice.getFinishReason() == null) { - - return false; - } - - return Objects.equals(choice.getFinishReason(), ApiKeywords.TOOL_CALLS); - } -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatOptions.java deleted file mode 100644 index 20005fd990..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatOptions.java +++ /dev/null @@ -1,463 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.chat; - -import com.alibaba.dashscope.aigc.generation.Generation; -import com.alibaba.dashscope.aigc.generation.GenerationParam; -import org.springframework.ai.chat.prompt.ChatOptions; -import org.springframework.ai.model.function.FunctionCallback; -import org.springframework.ai.model.function.FunctionCallingOptions; -import org.springframework.util.Assert; - -import java.util.*; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiChatOptions implements FunctionCallingOptions, ChatOptions { - - /** - * TongYi Models. - * {@link Generation.Models} - */ - private String model = Generation.Models.QWEN_TURBO; - - /** - * The random number seed used in generation, the user controls the randomness of the content generated by the model. - * seed supports unsigned 64-bit integers, with a default value of 1234. - * when using seed, the model will generate the same or similar results as much as possible, but there is currently no guarantee that the results will be exactly the same each time. - */ - private Integer seed = 1234; - - /** - * Used to specify the maximum number of tokens that the model can generate when generating content, - * it defines the upper limit of generation but does not guarantee that this number will be generated every time. - * For qwen-turbo the maximum and default values are 1500 tokens. - * The qwen-max, qwen-max-1201, qwen-max-longcontext, and qwen-plus models have a maximum and default value of 2000 tokens. - */ - private Integer maxTokens = 1500; - - /** - * The generation process kernel sampling method probability threshold, - * for example, takes the value of 0.8, only retains the smallest set of the most probable tokens with probabilities that add up to greater than or equal to 0.8 as the candidate set. - * The range of values is (0,1.0), the larger the value, the higher the randomness of generation; the lower the value, the higher the certainty of generation. - */ - private Double topP = 0.8; - - /** - * The size of the sampling candidate set at the time of generation. - * For example, with a value of 50, only the 50 highest scoring tokens in a single generation will form a randomly sampled candidate set. - * The larger the value, the higher the randomness of the generation; the smaller the value, the higher the certainty of the generation. - * This parameter is not passed by default, and a value of None or when top_k is greater than 100 indicates that the top_k policy is not enabled, - * at which time, only the top_p policy is in effect. - */ - private Integer topK; - - /** - * Used to control the repeatability of model generation. - * Increasing repetition_penalty reduces the repetition of model generation. 1.0 means no penalty. - */ - private Double repetitionPenalty = 1.1; - - /** - * is used to control the degree of randomness and diversity. - * Specifically, the temperature value controls the extent to which the probability distribution of each candidate word is smoothed when generating text. - * Higher values of temperature reduce the peak of the probability distribution, allowing more low-probability words to be selected and generating more diverse results, - * while lower values of temperature enhance the peak of the probability distribution, making it easier for high-probability words to be selected and generating more certain results. - * Range: [0, 2), 0 is not recommended, meaningless. - * java version >= 2.5.1 - */ - private Double temperature = 0.85; - - /** - * The stop parameter is used to realize precise control of the content generation process, automatically stopping when the generated content is about to contain the specified string or token_ids, - * and the generated content does not contain the specified content. - * For example, if stop is specified as "Hello", it means stop when "Hello" will be generated; if stop is specified as [37763, 367], it means stop when "Observation" will be generated. - * The stop parameter can be passed as a list of arrays of strings or token_ids to support the scenario of using multiple stops. - * Explanation: Do not mix strings and token_ids in list mode, the element types should be the same in list mode. - */ - private List stop; - - /** - * Whether or not to use stream output. When outputting the result in stream mode, the interface returns the result as generator, - * you need to iterate to get the result, the default output is the whole sequence of the current generation for each output, - * the last output is the final result of all the generation, you can change the output mode to non-incremental output by the parameter incremental_output to False. - */ - private Boolean stream = false; - - /** - * The model has a built-in Internet search service. - * This parameter controls whether the model refers to the use of Internet search results when generating text. The values are as follows: - * True: enable internet search, the model will use the search result as the reference information in the text generation process, but the model will "judge by itself" whether to use the internet search result based on its internal logic. - * False (default): Internet search is disabled. - */ - private Boolean enableSearch = false; - - /** - * [text|message], defaults to text, when it is message, - * the output refers to the message result example. - * It is recommended to prioritize the use of message format. - */ - private String resultFormat = GenerationParam.ResultFormat.MESSAGE; - - /** - * Control the streaming output mode, that is, the content will contain the content has been output; - * set to True, will open the incremental output mode, the output will not contain the content has been output, - * you need to splice the whole output, refer to the streaming output sample code. - */ - private Boolean incrementalOutput = false; - - /** - * A list of tools that the model can optionally call. - * Currently only functions are supported, and even if multiple functions are entered, the model will only select one to generate the result. - */ - private List tools; - - @Override - public Float getTemperature() { - - return this.temperature.floatValue(); - } - - public void setTemperature(Float temperature) { - - this.temperature = temperature.doubleValue(); - } - - @Override - public Float getTopP() { - - return this.topP.floatValue(); - } - - public void setTopP(Float topP) { - - this.topP = topP.doubleValue(); - } - - @Override - public Integer getTopK() { - - return this.topK; - } - - public void setTopK(Integer topK) { - - this.topK = topK; - } - - public String getModel() { - - return model; - } - - public void setModel(String model) { - - this.model = model; - } - - public Integer getSeed() { - - return seed; - } - - public String getResultFormat() { - - return resultFormat; - } - - public void setResultFormat(String resultFormat) { - - this.resultFormat = resultFormat; - } - - public void setSeed(Integer seed) { - - this.seed = seed; - } - - public Integer getMaxTokens() { - - return maxTokens; - } - - public void setMaxTokens(Integer maxTokens) { - - this.maxTokens = maxTokens; - } - - public Float getRepetitionPenalty() { - - return repetitionPenalty.floatValue(); - } - - public void setRepetitionPenalty(Float repetitionPenalty) { - - this.repetitionPenalty = repetitionPenalty.doubleValue(); - } - - public List getStop() { - - return stop; - } - - public void setStop(List stop) { - - this.stop = stop; - } - - public Boolean getStream() { - - return stream; - } - - public void setStream(Boolean stream) { - - this.stream = stream; - } - - public Boolean getEnableSearch() { - - return enableSearch; - } - - public void setEnableSearch(Boolean enableSearch) { - - this.enableSearch = enableSearch; - } - - public Boolean getIncrementalOutput() { - - return incrementalOutput; - } - - public void setIncrementalOutput(Boolean incrementalOutput) { - - this.incrementalOutput = incrementalOutput; - } - - public List getTools() { - - return tools; - } - - public void setTools(List tools) { - - this.tools = tools; - } - - private List functionCallbacks = new ArrayList<>(); - - private Set functions = new HashSet<>(); - - @Override - public List getFunctionCallbacks() { - - return this.functionCallbacks; - } - - @Override - public void setFunctionCallbacks(List functionCallbacks) { - - this.functionCallbacks = functionCallbacks; - } - - @Override - public Set getFunctions() { - - return this.functions; - } - - @Override - public void setFunctions(Set functions) { - - this.functions = functions; - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - return true; - } - - if (o == null || getClass() != o.getClass()) { - return false; - } - - TongYiChatOptions that = (TongYiChatOptions) o; - - return Objects.equals(model, that.model) - && Objects.equals(seed, that.seed) - && Objects.equals(maxTokens, that.maxTokens) - && Objects.equals(topP, that.topP) - && Objects.equals(topK, that.topK) - && Objects.equals(repetitionPenalty, that.repetitionPenalty) - && Objects.equals(temperature, that.temperature) - && Objects.equals(stop, that.stop) - && Objects.equals(stream, that.stream) - && Objects.equals(enableSearch, that.enableSearch) - && Objects.equals(resultFormat, that.resultFormat) - && Objects.equals(incrementalOutput, that.incrementalOutput) - && Objects.equals(tools, that.tools) - && Objects.equals(functionCallbacks, that.functionCallbacks) - && Objects.equals(functions, that.functions); - } - - @Override - public int hashCode() { - - return Objects.hash( - model, - seed, - maxTokens, - topP, - topK, - repetitionPenalty, - temperature, - stop, - stream, - enableSearch, - resultFormat, - incrementalOutput, - tools, - functionCallbacks, - functions - ); - } - - @Override - public String toString() { - - final StringBuilder sb = new StringBuilder("TongYiChatOptions{"); - - sb.append(", model='").append(model).append('\''); - sb.append(", seed=").append(seed); - sb.append(", maxTokens=").append(maxTokens); - sb.append(", topP=").append(topP); - sb.append(", topK=").append(topK); - sb.append(", repetitionPenalty=").append(repetitionPenalty); - sb.append(", temperature=").append(temperature); - sb.append(", stop=").append(stop); - sb.append(", stream=").append(stream); - sb.append(", enableSearch=").append(enableSearch); - sb.append(", resultFormat='").append(resultFormat).append('\''); - sb.append(", incrementalOutput=").append(incrementalOutput); - sb.append(", tools=").append(tools); - sb.append(", functionCallbacks=").append(functionCallbacks); - sb.append(", functions=").append(functions); - sb.append('}'); - - return sb.toString(); - } - - public static Builder builder() { - - return new Builder(); - } - - public static class Builder { - - protected TongYiChatOptions options; - - public Builder() { - - this.options = new TongYiChatOptions(); - } - - public Builder(TongYiChatOptions options) { - - this.options = options; - } - - public Builder withModel(String model) { - this.options.model = model; - return this; - } - - public Builder withMaxTokens(Integer maxTokens) { - this.options.maxTokens = maxTokens; - return this; - } - - public Builder withResultFormat(String rf) { - this.options.resultFormat = rf; - return this; - } - - public Builder withEnableSearch(Boolean enableSearch) { - this.options.enableSearch = enableSearch; - return this; - } - - public Builder withFunctionCallbacks(List functionCallbacks) { - this.options.functionCallbacks = functionCallbacks; - return this; - } - - public Builder withFunctions(Set functionNames) { - Assert.notNull(functionNames, "Function names must not be null"); - this.options.functions = functionNames; - return this; - } - - public Builder withFunction(String functionName) { - Assert.hasText(functionName, "Function name must not be empty"); - this.options.functions.add(functionName); - return this; - } - - public Builder withSeed(Integer seed) { - this.options.seed = seed; - return this; - } - - public Builder withStop(List stop) { - this.options.stop = stop; - return this; - } - - public Builder withTemperature(Double temperature) { - this.options.temperature = temperature; - return this; - } - - public Builder withTopP(Double topP) { - this.options.topP = topP; - return this; - } - - public Builder withTopK(Integer topK) { - this.options.topK = topK; - return this; - } - - public Builder withRepetitionPenalty(Double repetitionPenalty) { - this.options.repetitionPenalty = repetitionPenalty; - return this; - } - - public TongYiChatOptions build() { - - return this.options; - } - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatProperties.java deleted file mode 100644 index 9a7e85afb1..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatProperties.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.chat; - -import com.alibaba.dashscope.aigc.generation.Generation; -import com.alibaba.dashscope.aigc.generation.GenerationParam; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -@ConfigurationProperties(TongYiChatProperties.CONFIG_PREFIX) -public class TongYiChatProperties { - - /** - * Spring Cloud Alibaba AI configuration prefix. - */ - public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "chat"; - - /** - * Default TongYi Chat model. - */ - public static final String DEFAULT_DEPLOYMENT_NAME = Generation.Models.QWEN_TURBO; - - /** - * Default temperature speed. - */ - private static final Double DEFAULT_TEMPERATURE = 0.8; - - /** - * Enable TongYiQWEN ai chat client. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private TongYiChatOptions options = TongYiChatOptions.builder() - .withModel(DEFAULT_DEPLOYMENT_NAME) - .withTemperature(DEFAULT_TEMPERATURE) - .withEnableSearch(true) - .withResultFormat(GenerationParam.ResultFormat.MESSAGE) - .build(); - - public TongYiChatOptions getOptions() { - - return this.options; - } - - public void setOptions(TongYiChatOptions options) { - - this.options = options; - } - - public boolean isEnabled() { - - return this.enabled; - } - - public void setEnabled(boolean enabled) { - - this.enabled = enabled; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/constants/TongYiConstants.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/constants/TongYiConstants.java deleted file mode 100644 index 6fe77bd309..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/constants/TongYiConstants.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2024-2025 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 com.alibaba.cloud.ai.tongyi.common.constants; - -/** - * @author yuluo - * @author yuluo - */ - -public final class TongYiConstants { - - private TongYiConstants() { - } - - /** - * Spring Cloud Alibaba AI configuration prefix. - */ - public static final String SCA_AI_CONFIGURATION = "spring.cloud.ai.tongyi."; - - /** - * Spring Cloud Alibaba AI constants prefix. - */ - public static final String SCA_AI = "SPRING_CLOUD_ALIBABA_"; - - /** - * TongYi AI apikey env name. - */ - public static final String SCA_AI_TONGYI_API_KEY = SCA_AI + "TONGYI_API_KEY"; - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiException.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiException.java deleted file mode 100644 index 18e14ad764..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiException.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.common.exception; - -/** - * TongYi models runtime exception. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiException extends RuntimeException { - - public TongYiException(String message) { - - super(message); - } - - public TongYiException(String message, Throwable cause) { - - super(message, cause); - } -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiImagesException.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiImagesException.java deleted file mode 100644 index 5e2e577e97..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiImagesException.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.common.exception; - -/** - * TongYi models images exception. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiImagesException extends TongYiException { - - public TongYiImagesException(String message) { - - super(message); - } - - public TongYiImagesException(String message, Throwable cause) { - - super(message, cause); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiEmbeddingOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiEmbeddingOptions.java deleted file mode 100644 index 2ad81258f0..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiEmbeddingOptions.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.embedding; - -import com.alibaba.dashscope.embeddings.TextEmbeddingParam; -import org.springframework.ai.embedding.EmbeddingOptions; - -import java.util.List; - -/** - * @author why_ohh - * @author yuluo - * @author why_ohh - * @since 2023.0.1.0 - */ - -public final class TongYiEmbeddingOptions implements EmbeddingOptions { - - private List texts; - - private TextEmbeddingParam.TextType textType; - - public List getTexts() { - return texts; - } - - public void setTexts(List texts) { - this.texts = texts; - } - - public TextEmbeddingParam.TextType getTextType() { - return textType; - } - - public void setTextType(TextEmbeddingParam.TextType textType) { - this.textType = textType; - } - - public static Builder builder() { - return new Builder(); - } - - public final static class Builder { - - private final TongYiEmbeddingOptions options; - - private Builder() { - this.options = new TongYiEmbeddingOptions(); - } - - public Builder withtexts(List texts) { - - options.setTexts(texts); - return this; - } - - public Builder withtextType(TextEmbeddingParam.TextType textType) { - - options.setTextType(textType); - return this; - } - - public TongYiEmbeddingOptions build() { - - return options; - } - - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java deleted file mode 100644 index 99a356fe83..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.embedding; - -import cn.hutool.core.collection.ListUtil; -import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException; -import com.alibaba.cloud.ai.tongyi.metadata.TongYiTextEmbeddingResponseMetadata; -import com.alibaba.dashscope.embeddings.TextEmbedding; -import com.alibaba.dashscope.embeddings.TextEmbeddingParam; -import com.alibaba.dashscope.embeddings.TextEmbeddingResult; -import com.alibaba.dashscope.embeddings.TextEmbeddingResultItem; -import com.alibaba.dashscope.exception.InputRequiredException; -import com.alibaba.dashscope.exception.NoApiKeyException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.ai.document.Document; -import org.springframework.ai.document.MetadataMode; -import org.springframework.ai.embedding.AbstractEmbeddingModel; -import org.springframework.ai.embedding.Embedding; -import org.springframework.ai.embedding.EmbeddingRequest; -import org.springframework.ai.embedding.EmbeddingResponse; -import org.springframework.util.Assert; - -import java.util.List; -import java.util.stream.Collectors; - -/** - * {@link TongYiTextEmbeddingModel} implementation for {@literal Alibaba DashScope}. - * - * @author why_ohh - * @author yuluo - * @author why_ohh - * @since 2023.0.1.0 - */ - -public class TongYiTextEmbeddingModel extends AbstractEmbeddingModel { - - private final Logger logger = LoggerFactory.getLogger(TongYiTextEmbeddingModel.class); - - /** - * TongYi Text Embedding client. - */ - private final TextEmbedding textEmbedding; - - /** - * {@link MetadataMode}. - */ - private final MetadataMode metadataMode; - - private final TongYiEmbeddingOptions defaultOptions; - - public TongYiTextEmbeddingModel(TextEmbedding textEmbedding) { - - this(textEmbedding, MetadataMode.EMBED); - } - - public TongYiTextEmbeddingModel(TextEmbedding textEmbedding, MetadataMode metadataMode) { - - this(textEmbedding, metadataMode, - TongYiEmbeddingOptions.builder() - .withtextType(TextEmbeddingParam.TextType.DOCUMENT) - .build() - ); - } - - public TongYiTextEmbeddingModel( - TextEmbedding textEmbedding, - MetadataMode metadataMode, - TongYiEmbeddingOptions options - ) { - Assert.notNull(textEmbedding, "textEmbedding must not be null"); - Assert.notNull(metadataMode, "Metadata mode must not be null"); - Assert.notNull(options, "TongYiEmbeddingOptions must not be null"); - - this.metadataMode = metadataMode; - this.textEmbedding = textEmbedding; - this.defaultOptions = options; - } - - public TongYiEmbeddingOptions getDefaultOptions() { - - return this.defaultOptions; - } - - @Override - public List embed(Document document) { - - return this.call( - new EmbeddingRequest( - ListUtil.of(document.getFormattedContent(this.metadataMode)), - null) - ).getResults().stream() - .map(Embedding::getOutput) - .flatMap(List::stream) - .toList(); - } - - @Override - public EmbeddingResponse call(EmbeddingRequest request) { - - TextEmbeddingParam embeddingParams = toEmbeddingParams(request); - logger.debug("Embedding request: {}", embeddingParams); - TextEmbeddingResult resp; - - try { - resp = textEmbedding.call(embeddingParams); - } - catch (NoApiKeyException e) { - throw new TongYiException(e.getMessage()); - } - - return genEmbeddingResp(resp); - } - - private EmbeddingResponse genEmbeddingResp(TextEmbeddingResult result) { - - return new EmbeddingResponse( - genEmbeddingList(result.getOutput().getEmbeddings()), - TongYiTextEmbeddingResponseMetadata.from(result.getUsage()) - ); - } - - private List genEmbeddingList(List embeddings) { - - return embeddings.stream() - .map(embedding -> - new Embedding( - embedding.getEmbedding(), - embedding.getTextIndex() - )) - .collect(Collectors.toList()); - } - - /** - * We recommend setting the model parameters by passing the embedding parameters through the code; - * yml configuration compatibility is not considered here. - * It is not recommended that users set parameters from yml, - * as this reduces the flexibility of the configuration. - * Because the model name keeps changing, strings are used here and constants are undefined: - * Model list: Text Embedding Model List - * @param requestOptions Client params. {@link EmbeddingRequest} - * @return {@link TextEmbeddingParam} - */ - private TextEmbeddingParam toEmbeddingParams(EmbeddingRequest requestOptions) { - - TextEmbeddingParam tongYiEmbeddingParams = TextEmbeddingParam.builder() - .texts(requestOptions.getInstructions()) - .textType(defaultOptions.getTextType() != null ? defaultOptions.getTextType() : TextEmbeddingParam.TextType.DOCUMENT) - .model("text-embedding-v1") - .build(); - - try { - tongYiEmbeddingParams.validate(); - } - catch (InputRequiredException e) { - throw new TongYiException(e.getMessage()); - } - - return tongYiEmbeddingParams; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingProperties.java deleted file mode 100644 index 02196f41d8..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingProperties.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.embedding; - -import org.springframework.boot.context.properties.ConfigurationProperties; - -import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION; - -/** - * @author why_ohh - * @author yuluo - * @author why_ohh - * @since 2023.0.1.0 - */ - -@ConfigurationProperties(TongYiTextEmbeddingProperties.CONFIG_PREFIX) -public class TongYiTextEmbeddingProperties { - - /** - * Prefix of TongYi Text Embedding properties. - */ - public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "embedding"; - - private boolean enabled = true; - - public boolean isEnabled() { - - return this.enabled; - } - - public void setEnabled(boolean enabled) { - - this.enabled = enabled; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesModel.java deleted file mode 100644 index 55dbb339dc..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesModel.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.image; - -import com.alibaba.cloud.ai.tongyi.common.exception.TongYiImagesException; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; -import com.alibaba.dashscope.exception.NoApiKeyException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.ai.image.*; -import org.springframework.util.Assert; - -import java.io.ByteArrayOutputStream; -import java.net.URL; -import java.util.Base64; -import java.util.stream.Collectors; - -import static com.alibaba.cloud.ai.tongyi.metadata.TongYiImagesResponseMetadata.from; - -/** - * TongYiImagesClient is a class that implements the ImageClient interface. It provides a - * client for calling the TongYi AI image generation API. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiImagesModel implements ImageModel { - - private final Logger logger = LoggerFactory.getLogger(TongYiImagesModel.class); - - /** - * Gen Images API. - */ - private final ImageSynthesis imageSynthesis; - - /** - * TongYi Gen images properties. - */ - private TongYiImagesOptions defaultOptions; - - /** - * Adapt TongYi images api size properties. - */ - private final String sizeConnection = "*"; - - /** - * Get default images options. - * - * @return Default TongYiImagesOptions. - */ - public TongYiImagesOptions getDefaultOptions() { - - return this.defaultOptions; - } - - /** - * TongYiImagesClient constructor. - * @param imageSynthesis the image synthesis - * {@link ImageSynthesis} - */ - public TongYiImagesModel(ImageSynthesis imageSynthesis) { - - this(imageSynthesis, TongYiImagesOptions. - builder() - .withModel(ImageSynthesis.Models.WANX_V1) - .withN(1) - .build() - ); - } - - /** - * TongYiImagesClient constructor. - * @param imageSynthesis {@link ImageSynthesis} - * @param imagesOptions {@link TongYiImagesOptions} - */ - public TongYiImagesModel(ImageSynthesis imageSynthesis, TongYiImagesOptions imagesOptions) { - - Assert.notNull(imageSynthesis, "ImageSynthesis must not be null"); - Assert.notNull(imagesOptions, "TongYiImagesOptions must not be null"); - - this.imageSynthesis = imageSynthesis; - this.defaultOptions = imagesOptions; - } - - /** - * Call the TongYi images service. - * @param imagePrompt the image prompt. - * @return the image response. - * {@link ImageSynthesis#call(ImageSynthesisParam)} - */ - @Override - public ImageResponse call(ImagePrompt imagePrompt) { - - ImageSynthesisResult result; - String prompt = imagePrompt.getInstructions().get(0).getText(); - var imgParams = ImageSynthesisParam.builder() - .prompt("") - .model(ImageSynthesis.Models.WANX_V1) - .build(); - - if (this.defaultOptions != null) { - - imgParams = merge(this.defaultOptions); - } - - if (imagePrompt.getOptions() != null) { - - imgParams = merge(toTingYiImageOptions(imagePrompt.getOptions())); - } - imgParams.setPrompt(prompt); - - try { - result = imageSynthesis.call(imgParams); - } - catch (NoApiKeyException e) { - - logger.error("TongYi models gen images failed: {}.", e.getMessage()); - throw new TongYiImagesException(e.getMessage()); - } - - return convert(result); - } - - public ImageSynthesisParam merge(TongYiImagesOptions target) { - - var builder = ImageSynthesisParam.builder(); - - builder.model(this.defaultOptions.getModel() != null ? this.defaultOptions.getModel() : target.getModel()); - builder.n(this.defaultOptions.getN() != null ? this.defaultOptions.getN() : target.getN()); - builder.size((this.defaultOptions.getHeight() != null && this.defaultOptions.getWidth() != null) - ? this.defaultOptions.getHeight() + "*" + this.defaultOptions.getWidth() - : target.getHeight() + "*" + target.getWidth() - ); - - // prompt is marked non-null but is null. - builder.prompt(""); - - return builder.build(); - } - - private ImageResponse convert(ImageSynthesisResult result) { - - return new ImageResponse( - result.getOutput().getResults().stream() - .flatMap(value -> value.entrySet().stream()) - .map(entry -> { - String key = entry.getKey(); - String value = entry.getValue(); - try { - String base64Image = convertImageToBase64(value); - Image image = new Image(value, base64Image); - return new ImageGeneration(image); - } - catch (Exception e) { - throw new RuntimeException(e); - } - }) - .collect(Collectors.toList()), - from(result) - ); - } - - public TongYiImagesOptions toTingYiImageOptions(ImageOptions runtimeImageOptions) { - - var builder = TongYiImagesOptions.builder(); - - if (runtimeImageOptions != null) { - if (runtimeImageOptions.getN() != null) { - - builder.withN(runtimeImageOptions.getN()); - } - if (runtimeImageOptions.getModel() != null) { - - builder.withModel(runtimeImageOptions.getModel()); - } - if (runtimeImageOptions.getHeight() != null) { - - builder.withHeight(runtimeImageOptions.getHeight()); - } - if (runtimeImageOptions.getWidth() != null) { - - builder.withWidth(runtimeImageOptions.getWidth()); - } - - // todo ImagesParams. - } - - return builder.build(); - } - - /** - * Convert image to base64. - * @param imageUrl the image url. - * @return the base64 image. - * @throws Exception the exception. - */ - public String convertImageToBase64(String imageUrl) throws Exception { - - var url = new URL(imageUrl); - var inputStream = url.openStream(); - var outputStream = new ByteArrayOutputStream(); - var buffer = new byte[4096]; - int bytesRead; - - while ((bytesRead = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - } - - var imageBytes = outputStream.toByteArray(); - - String base64Image = Base64.getEncoder().encodeToString(imageBytes); - - inputStream.close(); - outputStream.close(); - - return base64Image; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesOptions.java deleted file mode 100644 index 412b07be04..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesOptions.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.image; - -import com.alibaba.cloud.ai.tongyi.common.exception.TongYiImagesException; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import org.springframework.ai.image.ImageOptions; - -import java.util.Objects; - -/** - * TongYi Image API options. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiImagesOptions implements ImageOptions { - - /** - * Specify the model name, currently only wanx-v1 is supported. - */ - private String model = ImageSynthesis.Models.WANX_V1; - - /** - * Gen images number. - */ - private Integer n; - - /** - * The width of the generated images. - */ - private Integer width = 1024; - - /** - * The height of the generated images. - */ - private Integer height = 1024; - - @Override - public Integer getN() { - - return this.n; - } - - @Override - public String getModel() { - - return this.model; - } - - @Override - public Integer getWidth() { - - return this.width; - } - - @Override - public Integer getHeight() { - - return this.height; - } - - @Override - public String getResponseFormat() { - - throw new TongYiImagesException("unimplemented!"); - } - - public void setModel(String model) { - - this.model = model; - } - - public void setN(Integer n) { - - this.n = n; - } - - public void setWidth(Integer width) { - - this.width = width; - } - - public void setHeight(Integer height) { - - this.height = height; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - - return true; - } - if (o == null || getClass() != o.getClass()) { - - return false; - } - - TongYiImagesOptions that = (TongYiImagesOptions) o; - - return Objects.equals(model, that.model) - && Objects.equals(n, that.n) - && Objects.equals(width, that.width) - && Objects.equals(height, that.height); - } - - @Override - public int hashCode() { - - return Objects.hash(model, n, width, height); - } - - @Override - public String toString() { - - final StringBuilder sb = new StringBuilder("TongYiImagesOptions{"); - - sb.append("model='").append(model).append('\''); - sb.append(", n=").append(n); - sb.append(", width=").append(width); - sb.append(", height=").append(height); - sb.append('}'); - - return sb.toString(); - } - - public static Builder builder() { - return new Builder(); - } - - public final static class Builder { - - private final TongYiImagesOptions options; - - private Builder() { - this.options = new TongYiImagesOptions(); - } - - public Builder withN(Integer n) { - - options.setN(n); - return this; - } - - public Builder withModel(String model) { - - options.setModel(model); - return this; - } - - public Builder withWidth(Integer width) { - - options.setWidth(width); - return this; - } - - public Builder withHeight(Integer height) { - - options.setHeight(height); - return this; - } - - public TongYiImagesOptions build() { - - return options; - } - - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesProperties.java deleted file mode 100644 index 23f4d7f33a..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesProperties.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.image; - -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.NestedConfigurationProperty; - -import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION; - -/** - * TongYi Image API properties. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -@ConfigurationProperties(TongYiImagesProperties.CONFIG_PREFIX) -public class TongYiImagesProperties { - - /** - * Spring Cloud Alibaba AI configuration prefix. - */ - public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "images"; - - /** - * Default TongYi Chat model. - */ - public static final String DEFAULT_IMAGES_MODEL_NAME = ImageSynthesis.Models.WANX_V1; - - /** - * Enable TongYiQWEN ai images client. - */ - private boolean enabled = true; - - @NestedConfigurationProperty - private TongYiImagesOptions options = TongYiImagesOptions.builder() - .withModel(DEFAULT_IMAGES_MODEL_NAME) - .withN(1) - .build(); - - public TongYiImagesOptions getOptions() { - - return this.options; - } - - public void setOptions(TongYiImagesOptions options) { - - this.options = options; - } - - public boolean isEnabled() { - - return this.enabled; - } - - public void setEnabled(boolean enabled) { - - this.enabled = enabled; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiChatResponseMetadata.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiChatResponseMetadata.java deleted file mode 100644 index 3918313c0d..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiChatResponseMetadata.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.metadata; - -import com.alibaba.dashscope.aigc.generation.GenerationResult; -import org.springframework.ai.chat.metadata.ChatResponseMetadata; -import org.springframework.ai.chat.metadata.PromptMetadata; -import org.springframework.ai.chat.metadata.Usage; -import org.springframework.util.Assert; - -import java.util.HashMap; - - - -/** - * {@link ChatResponseMetadata} implementation for {@literal Alibaba DashScope}. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAiChatResponseMetadata extends HashMap implements ChatResponseMetadata { - - protected static final String AI_METADATA_STRING = "{ @type: %1$s, id: %2$s, usage: %3$s, rateLimit: %4$s }"; - - @SuppressWarnings("all") - public static TongYiAiChatResponseMetadata from(GenerationResult chatCompletions, - PromptMetadata promptFilterMetadata) { - - Assert.notNull(chatCompletions, "Alibaba ai ChatCompletions must not be null"); - String id = chatCompletions.getRequestId(); - TongYiAiUsage usage = TongYiAiUsage.from(chatCompletions); - - return new TongYiAiChatResponseMetadata( - id, - usage, - promptFilterMetadata - ); - } - - private final String id; - - private final Usage usage; - - private final PromptMetadata promptMetadata; - - protected TongYiAiChatResponseMetadata(String id, TongYiAiUsage usage, PromptMetadata promptMetadata) { - - this.id = id; - this.usage = usage; - this.promptMetadata = promptMetadata; - } - - public String getId() { - return this.id; - } - - @Override - public Usage getUsage() { - return this.usage; - } - - @Override - public PromptMetadata getPromptMetadata() { - return this.promptMetadata; - } - - @Override - public String toString() { - - return AI_METADATA_STRING.formatted(getClass().getTypeName(), getId(), getUsage(), getRateLimit()); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiUsage.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiUsage.java deleted file mode 100644 index da086e502f..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiUsage.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.metadata; - -import com.alibaba.dashscope.aigc.generation.GenerationResult; -import com.alibaba.dashscope.aigc.generation.GenerationUsage; -import org.springframework.ai.chat.metadata.Usage; -import org.springframework.util.Assert; - -/** - * {@link Usage} implementation for {@literal Alibaba DashScope}. - * - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAiUsage implements Usage { - - private final GenerationUsage usage; - - public TongYiAiUsage(GenerationUsage usage) { - - Assert.notNull(usage, "GenerationUsage must not be null"); - this.usage = usage; - } - - public static TongYiAiUsage from(GenerationResult chatCompletions) { - - Assert.notNull(chatCompletions, "ChatCompletions must not be null"); - return from(chatCompletions.getUsage()); - } - - public static TongYiAiUsage from(GenerationUsage usage) { - - return new TongYiAiUsage(usage); - } - - protected GenerationUsage getUsage() { - - return this.usage; - } - - @Override - public Long getPromptTokens() { - - throw new UnsupportedOperationException("Unimplemented method 'getPromptTokens'"); - } - - @Override - public Long getGenerationTokens() { - - return this.getUsage().getOutputTokens().longValue(); - } - - @Override - public Long getTotalTokens() { - - return this.getUsage().getTotalTokens().longValue(); - } - - @Override - public String toString() { - - return this.getUsage().toString(); - } -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiImagesResponseMetadata.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiImagesResponseMetadata.java deleted file mode 100644 index 11b38f6eb5..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiImagesResponseMetadata.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.metadata; - -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisTaskMetrics; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisUsage; -import org.springframework.ai.image.ImageResponseMetadata; -import org.springframework.util.Assert; - -import java.util.HashMap; -import java.util.Objects; - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiImagesResponseMetadata extends HashMap implements ImageResponseMetadata { - - private final Long created; - - private String taskId; - - private ImageSynthesisTaskMetrics metrics; - - private ImageSynthesisUsage usage; - - public static TongYiImagesResponseMetadata from(ImageSynthesisResult synthesisResult) { - - Assert.notNull(synthesisResult, "TongYiAiImageResponse must not be null"); - - return new TongYiImagesResponseMetadata( - System.currentTimeMillis(), - synthesisResult.getOutput().getTaskMetrics(), - synthesisResult.getOutput().getTaskId(), - synthesisResult.getUsage() - ); - } - - protected TongYiImagesResponseMetadata( - Long created, - ImageSynthesisTaskMetrics metrics, - String taskId, - ImageSynthesisUsage usage - ) { - - this.taskId = taskId; - this.metrics = metrics; - this.created = created; - this.usage = usage; - } - - public ImageSynthesisUsage getUsage() { - return usage; - } - - public void setUsage(ImageSynthesisUsage usage) { - this.usage = usage; - } - - @Override - public Long getCreated() { - return created; - } - - public String getTaskId() { - return taskId; - } - - public void setTaskId(String taskId) { - this.taskId = taskId; - } - - public ImageSynthesisTaskMetrics getMetrics() { - return metrics; - } - - void setMetrics(ImageSynthesisTaskMetrics metrics) { - this.metrics = metrics; - } - - - public Long created() { - return this.created; - } - - @Override - public String toString() { - return "TongYiImagesResponseMetadata {" + "created=" + created + '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - - return true; - } - if (o == null || getClass() != o.getClass()) { - - return false; - } - - TongYiImagesResponseMetadata that = (TongYiImagesResponseMetadata) o; - - return Objects.equals(created, that.created) - && Objects.equals(taskId, that.taskId) - && Objects.equals(metrics, that.metrics); - } - - @Override - public int hashCode() { - return Objects.hash(created, taskId, metrics); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiTextEmbeddingResponseMetadata.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiTextEmbeddingResponseMetadata.java deleted file mode 100644 index 414154bf84..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiTextEmbeddingResponseMetadata.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.metadata; - -import com.alibaba.dashscope.embeddings.TextEmbeddingUsage; -import org.springframework.ai.embedding.EmbeddingResponseMetadata; - -/** - * @author why_ohh - * @author yuluo - * @author why_ohh - * @since 2023.0.1.0 - */ - -public class TongYiTextEmbeddingResponseMetadata extends EmbeddingResponseMetadata { - - private Integer totalTokens; - - protected TongYiTextEmbeddingResponseMetadata(Integer totalTokens) { - - this.totalTokens = totalTokens; - } - - public static TongYiTextEmbeddingResponseMetadata from(TextEmbeddingUsage usage) { - - return new TongYiTextEmbeddingResponseMetadata(usage.getTotalTokens()); - } - - public Integer getTotalTokens() { - - return totalTokens; - } - - public void setTotalTokens(Integer totalTokens) { - - this.totalTokens = totalTokens; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioSpeechResponseMetadata.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioSpeechResponseMetadata.java deleted file mode 100644 index 8647e3f0b8..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioSpeechResponseMetadata.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.metadata.audio; - -import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult; -import com.alibaba.dashscope.audio.tts.SpeechSynthesisUsage; -import com.alibaba.dashscope.audio.tts.timestamp.Sentence; -import org.springframework.ai.chat.metadata.EmptyRateLimit; -import org.springframework.ai.chat.metadata.RateLimit; -import org.springframework.ai.model.ResponseMetadata; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -import java.util.HashMap; - - - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAudioSpeechResponseMetadata extends HashMap implements ResponseMetadata { - - private SpeechSynthesisUsage usage; - - private String requestId; - - private Sentence time; - - protected static final String AI_METADATA_STRING = "{ @type: %1$s, requestsLimit: %2$s }"; - - /** - * NULL objects. - */ - public static final TongYiAudioSpeechResponseMetadata NULL = new TongYiAudioSpeechResponseMetadata() { - }; - - public static TongYiAudioSpeechResponseMetadata from(SpeechSynthesisResult result) { - - Assert.notNull(result, "TongYi AI speech must not be null"); - TongYiAudioSpeechResponseMetadata speechResponseMetadata = new TongYiAudioSpeechResponseMetadata(); - - - - return speechResponseMetadata; - } - - public static TongYiAudioSpeechResponseMetadata from(String result) { - - Assert.notNull(result, "TongYi AI speech must not be null"); - TongYiAudioSpeechResponseMetadata speechResponseMetadata = new TongYiAudioSpeechResponseMetadata(); - - return speechResponseMetadata; - } - - @Nullable - private RateLimit rateLimit; - - public TongYiAudioSpeechResponseMetadata() { - - this(null); - } - - public TongYiAudioSpeechResponseMetadata(@Nullable RateLimit rateLimit) { - - this.rateLimit = rateLimit; - } - - @Nullable - public RateLimit getRateLimit() { - - RateLimit rateLimit = this.rateLimit; - return rateLimit != null ? rateLimit : new EmptyRateLimit(); - } - - public TongYiAudioSpeechResponseMetadata withRateLimit(RateLimit rateLimit) { - - this.rateLimit = rateLimit; - return this; - } - - public TongYiAudioSpeechResponseMetadata withUsage(SpeechSynthesisUsage usage) { - - this.usage = usage; - return this; - } - - public TongYiAudioSpeechResponseMetadata withRequestId(String id) { - - this.requestId = id; - return this; - } - - public TongYiAudioSpeechResponseMetadata withSentence(Sentence sentence) { - - this.time = sentence; - return this; - } - - public SpeechSynthesisUsage getUsage() { - return usage; - } - - public String getRequestId() { - return requestId; - } - - public Sentence getTime() { - return time; - } - - @Override - public String toString() { - return AI_METADATA_STRING.formatted(getClass().getName(), getRateLimit()); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionMetadata.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionMetadata.java deleted file mode 100644 index 32d5d63c26..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionMetadata.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.metadata.audio; - -import org.springframework.ai.model.ResultMetadata; - -/** - * @author xYLiu - * @author yuluo - * @since 2023.0.1.0 - */ - -public interface TongYiAudioTranscriptionMetadata extends ResultMetadata { - - /** - * A constant instance of {@link TongYiAudioTranscriptionMetadata} that represents a null or empty metadata. - */ - TongYiAudioTranscriptionMetadata NULL = TongYiAudioTranscriptionMetadata.create(); - - /** - * Factory method for creating a new instance of {@link TongYiAudioTranscriptionMetadata}. - * @return a new instance of {@link TongYiAudioTranscriptionMetadata} - */ - static TongYiAudioTranscriptionMetadata create() { - return new TongYiAudioTranscriptionMetadata() { - }; - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionResponseMetadata.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionResponseMetadata.java deleted file mode 100644 index 36525d5f56..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionResponseMetadata.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 com.alibaba.cloud.ai.tongyi.metadata.audio; - -import com.alibaba.dashscope.audio.asr.transcription.TranscriptionResult; -import com.google.gson.JsonObject; -import org.springframework.ai.chat.metadata.EmptyRateLimit; -import org.springframework.ai.chat.metadata.RateLimit; -import org.springframework.ai.model.ResponseMetadata; -import org.springframework.util.Assert; - -import javax.annotation.Nullable; -import java.util.HashMap; - - - -/** - * @author yuluo - * @author yuluo - * @since 2023.0.1.0 - */ - -public class TongYiAudioTranscriptionResponseMetadata extends HashMap implements ResponseMetadata { - - @Nullable - private RateLimit rateLimit; - - private JsonObject usage; - - protected static final String AI_METADATA_STRING = "{ @type: %1$s, rateLimit: %4$s }"; - - /** - * NULL objects. - */ - public static final TongYiAudioTranscriptionResponseMetadata NULL = new TongYiAudioTranscriptionResponseMetadata() { - }; - - protected TongYiAudioTranscriptionResponseMetadata() { - - this(null, new JsonObject()); - } - - protected TongYiAudioTranscriptionResponseMetadata(JsonObject usage) { - - this(null, usage); - } - - protected TongYiAudioTranscriptionResponseMetadata(@Nullable RateLimit rateLimit, JsonObject usage) { - - this.rateLimit = rateLimit; - this.usage = usage; - } - - public static TongYiAudioTranscriptionResponseMetadata from(TranscriptionResult result) { - - Assert.notNull(result, "TongYi Transcription must not be null"); - return new TongYiAudioTranscriptionResponseMetadata(result.getUsage()); - } - - @Nullable - public RateLimit getRateLimit() { - - return this.rateLimit != null ? this.rateLimit : new EmptyRateLimit(); - } - - public void setRateLimit(@Nullable RateLimit rateLimit) { - this.rateLimit = rateLimit; - } - - public JsonObject getUsage() { - return usage; - } - - public void setUsage(JsonObject usage) { - this.usage = usage; - } - - @Override - public String toString() { - - return AI_METADATA_STRING.formatted(getClass().getName(), getRateLimit()); - } - -} From 22801802ac24cae2e2d5c7aecf8821952f7e1ce4 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 21 Feb 2025 09:50:28 +0800 Subject: [PATCH 02/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E3=80=91AI=EF=BC=9A=E4=BD=BF=E7=94=A8=20OpenAiApi=20?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E6=98=9F=E7=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/model/xinghuo/XingHuoChatModel.java | 138 ++---------------- .../ai/chat/XingHuoChatModelTests.java | 18 ++- 2 files changed, 25 insertions(+), 131 deletions(-) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java index 501d916db5..330d102a0f 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java @@ -1,163 +1,45 @@ package cn.iocoder.yudao.framework.ai.core.model.xinghuo; -import cn.hutool.core.collection.ListUtil; -import cn.hutool.core.lang.Assert; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.chat.metadata.ChatGenerationMetadata; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.ModelOptionsUtils; -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.ai.openai.metadata.OpenAiChatResponseMetadata; -import org.springframework.ai.retry.RetryUtils; -import org.springframework.http.ResponseEntity; -import org.springframework.retry.support.RetryTemplate; +import org.springframework.ai.openai.OpenAiChatModel; import reactor.core.publisher.Flux; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions.MODEL_DEFAULT; - /** * 讯飞星火 {@link ChatModel} 实现类 * * @author fansili */ @Slf4j +@RequiredArgsConstructor public class XingHuoChatModel implements ChatModel { - private static final String BASE_URL = "https://spark-api-open.xf-yun.com"; + public static final String BASE_URL = "https://spark-api-open.xf-yun.com"; - private final XingHuoChatOptions defaultOptions; - private final RetryTemplate retryTemplate; + public static final String MODEL_DEFAULT = "generalv3.5"; /** - * 星火兼容 OpenAI 的 HTTP 接口,所以复用它的实现,简化接入成本 - * - * 不过要注意,星火没有完全兼容,所以不能使用 {@link org.springframework.ai.openai.OpenAiChatModel} 调用,但是实现会参考它 + * 兼容 OpenAI 接口,进行复用 */ - private final OpenAiApi openAiApi; - - public XingHuoChatModel(String apiKey, String secretKey) { - this(apiKey, secretKey, - XingHuoChatOptions.builder().model(MODEL_DEFAULT).temperature(0.7F).build()); - } - - public XingHuoChatModel(String apiKey, String secretKey, XingHuoChatOptions options) { - this(apiKey, secretKey, options, RetryUtils.DEFAULT_RETRY_TEMPLATE); - } - - public XingHuoChatModel(String apiKey, String secretKey, XingHuoChatOptions options, RetryTemplate retryTemplate) { - Assert.notEmpty(apiKey, "apiKey 不能为空"); - Assert.notEmpty(secretKey, "secretKey 不能为空"); - Assert.notNull(options, "options 不能为空"); - Assert.notNull(retryTemplate, "retryTemplate 不能为空"); - this.openAiApi = new OpenAiApi(BASE_URL, apiKey + ":" + secretKey); - this.defaultOptions = options; - this.retryTemplate = retryTemplate; - } + private final OpenAiChatModel openAiChatModel; @Override public ChatResponse call(Prompt prompt) { - OpenAiApi.ChatCompletionRequest request = createRequest(prompt, false); - return this.retryTemplate.execute(ctx -> { - // 1.1 发起调用 - ResponseEntity completionEntity = openAiApi.chatCompletionEntity(request); - // 1.2 校验结果 - OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody(); - if (chatCompletion == null) { - log.warn("No chat completion returned for prompt: {}", prompt); - return new ChatResponse(ListUtil.of()); - } - List choices = chatCompletion.choices(); - if (choices == null) { - log.warn("No choices returned for prompt: {}", prompt); - return new ChatResponse(ListUtil.of()); - } - - // 2. 转换 ChatResponse 返回 - List generations = choices.stream().map(choice -> { - Generation generation = new Generation(choice.message().content(), toMap(chatCompletion.id(), choice)); - if (choice.finishReason() != null) { - generation.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null)); - } - return generation; - }).toList(); - return new ChatResponse(generations, - OpenAiChatResponseMetadata.from(completionEntity.getBody())); - }); - } - - private Map toMap(String id, OpenAiApi.ChatCompletion.Choice choice) { - Map map = new HashMap<>(); - OpenAiApi.ChatCompletionMessage message = choice.message(); - if (message.role() != null) { - map.put("role", message.role().name()); - } - if (choice.finishReason() != null) { - map.put("finishReason", choice.finishReason().name()); - } - map.put("id", id); - return map; + return openAiChatModel.call(prompt); } @Override public Flux stream(Prompt prompt) { - OpenAiApi.ChatCompletionRequest request = createRequest(prompt, true); - return this.retryTemplate.execute(ctx -> { - // 1. 发起调用 - Flux response = this.openAiApi.chatCompletionStream(request); - return response.map(chatCompletion -> { - String id = chatCompletion.id(); - // 2. 转换 ChatResponse 返回 - List generations = chatCompletion.choices().stream().map(choice -> { - String finish = (choice.finishReason() != null ? choice.finishReason().name() : ""); - Generation generation = new Generation(choice.delta().content(), - Map.of("id", id, "role", choice.delta().role().name(), "finishReason", finish)); - if (choice.finishReason() != null) { - generation = generation.withGenerationMetadata( - ChatGenerationMetadata.from(choice.finishReason().name(), null)); - } - return generation; - }).toList(); - return new ChatResponse(generations); - }); - }); - } - - OpenAiApi.ChatCompletionRequest createRequest(Prompt prompt, boolean stream) { - // 1. 构建 ChatCompletionMessage 对象 - List chatCompletionMessages = prompt.getInstructions().stream().map(m -> - new OpenAiApi.ChatCompletionMessage(m.getContent(), OpenAiApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name()))).toList(); - OpenAiApi.ChatCompletionRequest request = new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, stream); - - // 2.1 补充 prompt 内置的 options - if (prompt.getOptions() != null) { - if (prompt.getOptions() instanceof ChatOptions runtimeOptions) { - OpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, - ChatOptions.class, OpenAiChatOptions.class); - request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, OpenAiApi.ChatCompletionRequest.class); - } else { - throw new IllegalArgumentException("Prompt options are not of type ChatOptions: " - + prompt.getOptions().getClass().getSimpleName()); - } - } - // 2.2 补充默认 options - if (this.defaultOptions != null) { - request = ModelOptionsUtils.merge(request, this.defaultOptions, OpenAiApi.ChatCompletionRequest.class); - } - return request; + return openAiChatModel.stream(prompt); } @Override public ChatOptions getDefaultOptions() { - return XingHuoChatOptions.fromOptions(defaultOptions); + return openAiChatModel.getDefaultOptions(); } } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java index 63f76b96d1..4140b4bfbd 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java @@ -8,6 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; import reactor.core.publisher.Flux; import java.util.ArrayList; @@ -20,9 +23,18 @@ import java.util.List; */ public class XingHuoChatModelTests { - private final XingHuoChatModel chatModel = new XingHuoChatModel( - "cb6415c19d6162cda07b47316fcb0416", - "Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh"); + private static final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(XingHuoChatModel.BASE_URL) + .apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model("generalv3.5") // 模型 + .temperature(0.7) + .build()) + .build(); + + private final XingHuoChatModel chatModel = new XingHuoChatModel(openAiChatModel); @Test @Disabled From f582c9cfa3901921d3b6f75a85ab17bf42cb9fde Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 21 Feb 2025 13:46:54 +0800 Subject: [PATCH 03/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E3=80=91AI=EF=BC=9A=E4=BD=BF=E7=94=A8=20OpenAiApi=20?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=20deepseek?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/deepseek/DeepSeekChatModel.java | 141 ++---------------- .../model/deepseek/DeepSeekChatOptions.java | 55 ------- .../model/xinghuo/XingHuoChatOptions.java | 55 ------- .../ai/chat/DeepSeekChatModelTests.java | 16 +- 4 files changed, 25 insertions(+), 242 deletions(-) delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatOptions.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java index e3097b83a3..a136b5a2b5 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java @@ -1,166 +1,45 @@ package cn.iocoder.yudao.framework.ai.core.model.deepseek; -import cn.hutool.core.collection.ListUtil; -import cn.hutool.core.lang.Assert; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.chat.metadata.ChatGenerationMetadata; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.model.ModelOptionsUtils; -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.ai.openai.metadata.OpenAiChatResponseMetadata; -import org.springframework.ai.retry.RetryUtils; -import org.springframework.http.ResponseEntity; -import org.springframework.retry.support.RetryTemplate; +import org.springframework.ai.openai.OpenAiChatModel; import reactor.core.publisher.Flux; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions.MODEL_DEFAULT; - /** * DeepSeek {@link ChatModel} 实现类 * * @author fansili */ @Slf4j +@RequiredArgsConstructor public class DeepSeekChatModel implements ChatModel { - private static final String BASE_URL = "https://api.deepseek.com"; + public static final String BASE_URL = "https://api.deepseek.com"; - private final DeepSeekChatOptions defaultOptions; - private final RetryTemplate retryTemplate; + public static final String MODEL_DEFAULT = "deepseek-chat"; /** - * DeepSeek 兼容 OpenAI 的 HTTP 接口,所以复用它的实现,简化接入成本 - * - * 不过要注意,DeepSeek 没有完全兼容,所以不能使用 {@link org.springframework.ai.openai.OpenAiChatModel} 调用,但是实现会参考它 + * 兼容 OpenAI 接口,进行复用 */ - private final OpenAiApi openAiApi; - - public DeepSeekChatModel(String apiKey) { - this(apiKey, DeepSeekChatOptions.builder().model(MODEL_DEFAULT).temperature(0.7F).build()); - } - - public DeepSeekChatModel(String apiKey, DeepSeekChatOptions options) { - this(apiKey, options, RetryUtils.DEFAULT_RETRY_TEMPLATE); - } - - public DeepSeekChatModel(String apiKey, DeepSeekChatOptions options, RetryTemplate retryTemplate) { - Assert.notEmpty(apiKey, "apiKey 不能为空"); - Assert.notNull(options, "options 不能为空"); - Assert.notNull(retryTemplate, "retryTemplate 不能为空"); - this.openAiApi = new OpenAiApi(BASE_URL, apiKey); - this.defaultOptions = options; - this.retryTemplate = retryTemplate; - } + private final OpenAiChatModel openAiChatModel; @Override public ChatResponse call(Prompt prompt) { - OpenAiApi.ChatCompletionRequest request = createRequest(prompt, false); - return this.retryTemplate.execute(ctx -> { - // 1.1 发起调用 - ResponseEntity completionEntity = openAiApi.chatCompletionEntity(request); - // 1.2 校验结果 - OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody(); - if (chatCompletion == null) { - log.warn("No chat completion returned for prompt: {}", prompt); - return new ChatResponse(ListUtil.of()); - } - List choices = chatCompletion.choices(); - if (choices == null) { - log.warn("No choices returned for prompt: {}", prompt); - return new ChatResponse(ListUtil.of()); - } - - // 2. 转换 ChatResponse 返回 - List generations = choices.stream().map(choice -> { - Generation generation = new Generation(choice.message().content(), toMap(chatCompletion.id(), choice)); - if (choice.finishReason() != null) { - generation.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null)); - } - return generation; - }).toList(); - return new ChatResponse(generations, - OpenAiChatResponseMetadata.from(completionEntity.getBody())); - }); - } - - private Map toMap(String id, OpenAiApi.ChatCompletion.Choice choice) { - Map map = new HashMap<>(); - OpenAiApi.ChatCompletionMessage message = choice.message(); - if (message.role() != null) { - map.put("role", message.role().name()); - } - if (choice.finishReason() != null) { - map.put("finishReason", choice.finishReason().name()); - } - map.put("id", id); - return map; + return openAiChatModel.call(prompt); } @Override public Flux stream(Prompt prompt) { - OpenAiApi.ChatCompletionRequest request = createRequest(prompt, true); - return this.retryTemplate.execute(ctx -> { - // 1. 发起调用 - Flux response = this.openAiApi.chatCompletionStream(request); - return response.map(chatCompletion -> { - String id = chatCompletion.id(); - // 2. 转换 ChatResponse 返回 - List generations = chatCompletion.choices().stream().map(choice -> { - String finish = (choice.finishReason() != null ? choice.finishReason().name() : ""); - String role = (choice.delta().role() != null ? choice.delta().role().name() : ""); - if (choice.finishReason() == OpenAiApi.ChatCompletionFinishReason.STOP) { - // 兜底处理 DeepSeek 返回 STOP 时,role 为空的情况 - role = OpenAiApi.ChatCompletionMessage.Role.ASSISTANT.name(); - } - Generation generation = new Generation(choice.delta().content(), - Map.of("id", id, "role", role, "finishReason", finish)); - if (choice.finishReason() != null) { - generation = generation.withGenerationMetadata( - ChatGenerationMetadata.from(choice.finishReason().name(), null)); - } - return generation; - }).toList(); - return new ChatResponse(generations); - }); - }); - } - - OpenAiApi.ChatCompletionRequest createRequest(Prompt prompt, boolean stream) { - // 1. 构建 ChatCompletionMessage 对象 - List chatCompletionMessages = prompt.getInstructions().stream().map(m -> - new OpenAiApi.ChatCompletionMessage(m.getContent(), OpenAiApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name()))).toList(); - OpenAiApi.ChatCompletionRequest request = new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, stream); - - // 2.1 补充 prompt 内置的 options - if (prompt.getOptions() != null) { - if (prompt.getOptions() instanceof ChatOptions runtimeOptions) { - OpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, - ChatOptions.class, OpenAiChatOptions.class); - request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, OpenAiApi.ChatCompletionRequest.class); - } else { - throw new IllegalArgumentException("Prompt options are not of type ChatOptions: " - + prompt.getOptions().getClass().getSimpleName()); - } - } - // 2.2 补充默认 options - if (this.defaultOptions != null) { - request = ModelOptionsUtils.merge(request, this.defaultOptions, OpenAiApi.ChatCompletionRequest.class); - } - return request; + return openAiChatModel.stream(prompt); } @Override public ChatOptions getDefaultOptions() { - return DeepSeekChatOptions.fromOptions(defaultOptions); + return openAiChatModel.getDefaultOptions(); } } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatOptions.java deleted file mode 100644 index e07e3f0865..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatOptions.java +++ /dev/null @@ -1,55 +0,0 @@ -package cn.iocoder.yudao.framework.ai.core.model.deepseek; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.ai.chat.prompt.ChatOptions; - -/** - * DeepSeek {@link ChatOptions} 实现类 - * - * 参考文档:快速开始 - * - * @author fansili - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class DeepSeekChatOptions implements ChatOptions { - - public static final String MODEL_DEFAULT = "deepseek-chat"; - - /** - * 模型 - */ - private String model; - /** - * 温度 - */ - private Float temperature; - /** - * 最大 Token - */ - private Integer maxTokens; - /** - * topP - */ - private Float topP; - - @Override - public Integer getTopK() { - return null; - } - - public static DeepSeekChatOptions fromOptions(DeepSeekChatOptions fromOptions) { - return DeepSeekChatOptions.builder() - .model(fromOptions.getModel()) - .temperature(fromOptions.getTemperature()) - .maxTokens(fromOptions.getMaxTokens()) - .topP(fromOptions.getTopP()) - .build(); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java deleted file mode 100644 index e3287b613a..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java +++ /dev/null @@ -1,55 +0,0 @@ -package cn.iocoder.yudao.framework.ai.core.model.xinghuo; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.ai.chat.prompt.ChatOptions; - -/** - * 讯飞星火 {@link ChatOptions} 实现类 - * - * 参考文档:HTTP 调用 - * - * @author fansili - */ -@Data -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class XingHuoChatOptions implements ChatOptions { - - public static final String MODEL_DEFAULT = "generalv3.5"; - - /** - * 模型 - */ - private String model; - /** - * 温度 - */ - private Float temperature; - /** - * 最大 Token - */ - private Integer maxTokens; - /** - * K 个候选 - */ - private Integer topK; - - @Override - public Float getTopP() { - return null; - } - - public static XingHuoChatOptions fromOptions(XingHuoChatOptions fromOptions) { - return XingHuoChatOptions.builder() - .model(fromOptions.getModel()) - .temperature(fromOptions.getTemperature()) - .maxTokens(fromOptions.getMaxTokens()) - .topK(fromOptions.getTopK()) - .build(); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DeepSeekChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DeepSeekChatModelTests.java index f66c548176..b3e12bfd16 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DeepSeekChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DeepSeekChatModelTests.java @@ -8,6 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; import reactor.core.publisher.Flux; import java.util.ArrayList; @@ -20,7 +23,18 @@ import java.util.List; */ public class DeepSeekChatModelTests { - private final DeepSeekChatModel chatModel = new DeepSeekChatModel("sk-e94db327cc7d457d99a8de8810fc6b12"); + private static final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(DeepSeekChatModel.BASE_URL) + .apiKey("sk-e52047409b144d97b791a6a46a2d") // apiKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model("deepseek-chat") // 模型 + .temperature(0.7) + .build()) + .build(); + + private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel); @Test @Disabled From 3b7b81829d4d31d6387068eb48fe01123a94245b Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 22 Feb 2025 09:22:58 +0800 Subject: [PATCH 04/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E3=80=91AI=EF=BC=9A=E4=BD=BF=E7=94=A8=20alibaba=20ai?= =?UTF-8?q?=20=E6=9B=BF=E4=BB=A3=20tongyi=20=E7=9A=84=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/chat/TongYiChatModelTests.java | 28 +++++-------------- .../ai/chat/XingHuoChatModelTests.java | 22 +++++++-------- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java index 00bc2b9001..aa521373c4 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java @@ -1,12 +1,8 @@ package cn.iocoder.yudao.framework.ai.chat; -import cn.hutool.core.util.ReflectUtil; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions; -import com.alibaba.dashscope.aigc.generation.Generation; -import com.alibaba.dashscope.common.MessageManager; -import com.alibaba.dashscope.utils.Constants; -import org.junit.jupiter.api.BeforeEach; +import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; @@ -20,25 +16,15 @@ import java.util.ArrayList; import java.util.List; /** - * {@link TongYiChatModel} 集成测试类 + * {@link DashScopeChatModel} 集成测试类 * * @author fansili */ public class TongYiChatModelTests { - private final Generation generation = new Generation(); - private final TongYiChatModel chatModel = new TongYiChatModel(generation, - TongYiChatOptions.builder().withModel("qwen1.5-72b-chat").build()); - - static { - Constants.apiKey = "sk-Zsd81gZYg7"; - } - - @BeforeEach - public void before() { - // 防止 TongYiChatModel 调用空指针 - ReflectUtil.setFieldValue(chatModel, "msgManager", new MessageManager()); - } + private final DashScopeChatModel chatModel = new DashScopeChatModel( + new DashScopeApi("sk-7d903764249848cfa912733146da12d1"), + DashScopeChatOptions.builder().withModel("qwen1.5-72b-chat").build()); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java index 4140b4bfbd..cbc7f6e166 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java @@ -23,18 +23,18 @@ import java.util.List; */ public class XingHuoChatModelTests { - private static final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() - .openAiApi(OpenAiApi.builder() - .baseUrl(XingHuoChatModel.BASE_URL) - .apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey - .build()) - .defaultOptions(OpenAiChatOptions.builder() - .model("generalv3.5") // 模型 - .temperature(0.7) - .build()) - .build(); + private static final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(XingHuoChatModel.BASE_URL) + .apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model("generalv3.5") // 模型 + .temperature(0.7) + .build()) + .build(); - private final XingHuoChatModel chatModel = new XingHuoChatModel(openAiChatModel); + private final XingHuoChatModel chatModel = new XingHuoChatModel(openAiChatModel); @Test @Disabled From 7ef73b7d0992486c380c49e7f214f48ebf0f29d4 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 22 Feb 2025 10:47:18 +0800 Subject: [PATCH 05/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E3=80=91AI=EF=BC=9Aspring=20ai=20=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=EF=BC=8C=E5=8D=87=E7=BA=A7=E5=88=B0=201.0.0-M6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/framework/web/package-info.java | 4 +- .../ai/service/image/AiImageServiceImpl.java | 3 +- .../yudao-spring-boot-starter-ai/pom.xml | 9 +- .../ai/config/YudaoAiAutoConfiguration.java | 59 ++- .../ai/config/YudaoAiProperties.java | 8 +- .../ai/core/factory/AiModelFactoryImpl.java | 144 +++--- .../model/midjourney/api/MidjourneyApi.java | 14 +- .../yudao/framework/ai/core/util/AiUtils.java | 28 +- .../RedisVectorStoreAutoConfiguration.java | 61 --- .../ai/vectorstore/RedisVectorStore.java | 456 ------------------ .../ai/chat/AzureOpenAIChatModelTests.java | 9 +- .../ai/chat/LlamaChatModelTests.java | 86 ++-- .../ai/chat/OpenAIChatModelTests.java | 2 +- .../ai/chat/YiYanChatModelTests.java | 2 +- .../ai/chat/ZhiPuAiChatModelTests.java | 2 +- .../framework/ai/image/QianFanImageTests.java | 6 +- .../ai/image/TongYiImagesModelTest.java | 18 +- .../ai/image/ZhiPuAiImageModelTests.java | 2 +- 18 files changed, 199 insertions(+), 714 deletions(-) delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java delete mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java index 09de7263c5..e979056d4e 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/web/package-info.java @@ -1,4 +1,4 @@ /** - * crm 模块的 web 拓展封装 + * ai 模块的 web 拓展封装 */ -package cn.iocoder.yudao.module.crm.framework.web; +package cn.iocoder.yudao.module.ai.framework.web; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index e8532a5762..3e8c09f079 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -20,7 +20,6 @@ import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper; import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; import cn.iocoder.yudao.module.infra.api.file.FileApi; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesOptions; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.image.ImageModel; @@ -144,7 +143,7 @@ public class AiImageServiceImpl implements AiImageService { .withClipGuidancePreset(String.valueOf(draw.getOptions().get("clipGuidancePreset"))) .build(); } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) { - return TongYiImagesOptions.builder() + return DashScopeImageOptions.builder() .withModel(draw.getModel()).withN(1) .withHeight(draw.getHeight()).withWidth(draw.getWidth()) .build(); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index a270f76558..156d3f0769 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -14,8 +14,8 @@ ${project.artifactId} AI 大模型拓展,接入国内外大模型 - group.springframework.ai - 1.1.0 + org.springframework.ai + 1.0.0-M6 @@ -90,6 +90,11 @@ spring-ai-qianfan-spring-boot-starter ${spring-ai.version} + + com.alibaba.cloud.ai + spring-ai-alibaba-starter + 1.0.0-M5.1 + diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 0d2620b0cb..3aed3ee1b9 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -1,15 +1,16 @@ package cn.iocoder.yudao.framework.ai.config; +import cn.hutool.core.util.StrUtil; 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.deepseek.DeepSeekChatModel; -import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; 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.ai.core.model.xinghuo.XingHuoChatOptions; -import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; import lombok.extern.slf4j.Slf4j; +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.ai.transformer.splitter.TokenTextSplitter; @@ -17,7 +18,6 @@ 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; -import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Lazy; /** @@ -28,7 +28,6 @@ import org.springframework.context.annotation.Lazy; @AutoConfiguration @EnableConfigurationProperties(YudaoAiProperties.class) @Slf4j -@Import(TongYiAutoConfiguration.class) public class YudaoAiAutoConfiguration { @Bean @@ -43,26 +42,52 @@ public class YudaoAiAutoConfiguration { @ConditionalOnProperty(value = "yudao.ai.deepseek.enable", havingValue = "true") public DeepSeekChatModel deepSeekChatModel(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.DeepSeekProperties properties = yudaoAiProperties.getDeepSeek(); - DeepSeekChatOptions options = DeepSeekChatOptions.builder() - .model(properties.getModel()) - .temperature(properties.getTemperature()) - .maxTokens(properties.getMaxTokens()) - .topP(properties.getTopP()) + 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()) .build(); - return new DeepSeekChatModel(properties.getApiKey(), options); + return new DeepSeekChatModel(openAiChatModel); } @Bean @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo(); - XingHuoChatOptions options = XingHuoChatOptions.builder() - .model(properties.getModel()) - .temperature(properties.getTemperature()) - .maxTokens(properties.getMaxTokens()) - .topK(properties.getTopK()) + 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()) .build(); - return new XingHuoChatModel(properties.getAppKey(), properties.getSecretKey(), options); + return new XingHuoChatModel(openAiChatModel); } @Bean diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java index 82c74b0c64..7a98c4376a 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java @@ -42,9 +42,9 @@ public class YudaoAiProperties { private String secretKey; private String model; - private Float temperature; + private Double temperature; private Integer maxTokens; - private Integer topK; + private Double topP; } @@ -55,9 +55,9 @@ public class YudaoAiProperties { private String apiKey; private String model; - private Float temperature; + private Double temperature; private Integer maxTokens; - private Float topP; + private Double topP; } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index 7acd247691..985d8fd041 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -13,60 +13,45 @@ import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; 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.tongyi.TongYiAutoConfiguration; -import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatProperties; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesProperties; -import com.alibaba.dashscope.aigc.generation.Generation; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import com.alibaba.dashscope.embeddings.TextEmbedding; -import com.azure.ai.openai.OpenAIClient; +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.embedding.DashScopeEmbeddingModel; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; +import com.azure.ai.openai.OpenAIClientBuilder; 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.ollama.OllamaAutoConfiguration; import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration; -import org.springframework.ai.autoconfigure.qianfan.QianFanChatProperties; -import org.springframework.ai.autoconfigure.qianfan.QianFanConnectionProperties; -import org.springframework.ai.autoconfigure.qianfan.QianFanImageProperties; import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration; -import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiChatProperties; import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiConnectionProperties; -import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiImageProperties; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; -import org.springframework.ai.model.function.FunctionCallbackContext; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.api.OllamaApi; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiImageModel; -import org.springframework.ai.openai.api.ApiUtils; 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.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.RedisVectorStore; +import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiImageModel; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; -import org.springframework.boot.autoconfigure.data.redis.RedisProperties; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; -import redis.clients.jedis.JedisPooled; -import redis.clients.jedis.search.Schema; import java.util.List; @@ -110,7 +95,7 @@ public class AiModelFactoryImpl implements AiModelFactory { //noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: - return SpringUtil.getBean(TongYiChatModel.class); + return SpringUtil.getBean(DashScopeChatModel.class); case YI_YAN: return SpringUtil.getBean(QianFanChatModel.class); case DEEP_SEEK: @@ -135,7 +120,7 @@ public class AiModelFactoryImpl implements AiModelFactory { //noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: - return SpringUtil.getBean(TongYiImagesModel.class); + return SpringUtil.getBean(DashScopeImageModel.class); case YI_YAN: return SpringUtil.getBean(QianFanImageModel.class); case ZHI_PU: @@ -202,17 +187,20 @@ public class AiModelFactoryImpl implements AiModelFactory { String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url); return Singleton.get(cacheKey, (Func0) () -> { String prefix = StrUtil.format("{}#{}:", platform.getPlatform(), apiKey); - var config = RedisVectorStore.RedisVectorStoreConfig.builder() - .withIndexName(cacheKey) - .withPrefix(prefix) - .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC)) - .build(); - RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); - RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel, - new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), - true); - redisVectorStore.afterPropertiesSet(); - return redisVectorStore; + // TODO @芋艿:先临时使用 store + return SimpleVectorStore.builder(embeddingModel).build(); + // TODO @芋艿:@xin:后续看看,是不是切到阿里云之类的 +// var config = RedisVectorStore.RedisVectorStoreConfig.builder() +// .withIndexName(cacheKey) +// .withPrefix(prefix) +// .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC)) +// .build(); +// RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); +// RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel, +// new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), +// true); +// redisVectorStore.afterPropertiesSet(); +// return redisVectorStore; }); } @@ -226,29 +214,23 @@ public class AiModelFactoryImpl implements AiModelFactory { // ========== 各种创建 spring-ai 客户端的方法 ========== /** - * 可参考 {@link TongYiAutoConfiguration#tongYiChatClient(Generation, TongYiChatProperties, TongYiConnectionProperties)} + * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeChatModel 方法 */ - private static TongYiChatModel buildTongYiChatModel(String key) { - com.alibaba.dashscope.aigc.generation.Generation generation = SpringUtil.getBean(Generation.class); - TongYiChatProperties chatOptions = SpringUtil.getBean(TongYiChatProperties.class); - // TODO @芋艿:貌似 apiKey 是全局唯一的???得测试下 - // TODO @芋艿:貌似阿里云不是增量返回的 - // 该 issue 进行跟进中 https://github.com/alibaba/spring-cloud-alibaba/issues/3790 - TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); - connectionProperties.setApiKey(key); - return new TongYiAutoConfiguration().tongYiChatClient(generation, chatOptions, connectionProperties); - } - - private static TongYiImagesModel buildTongYiImagesModel(String key) { - ImageSynthesis imageSynthesis = SpringUtil.getBean(ImageSynthesis.class); - TongYiImagesProperties imagesOptions = SpringUtil.getBean(TongYiImagesProperties.class); - TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); - connectionProperties.setApiKey(key); - return new TongYiAutoConfiguration().tongYiImagesClient(imageSynthesis, imagesOptions, connectionProperties); + private static DashScopeChatModel buildTongYiChatModel(String key) { + DashScopeApi dashScopeApi = new DashScopeApi(key); + return new DashScopeChatModel(dashScopeApi); } /** - * 可参考 {@link QianFanAutoConfiguration#qianFanChatModel(QianFanConnectionProperties, QianFanChatProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + * 可参考 {@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 keys = StrUtil.split(key, '|'); @@ -260,7 +242,7 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link QianFanAutoConfiguration#qianFanImageModel(QianFanConnectionProperties, QianFanImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + * 可参考 {@link QianFanAutoConfiguration} 的 qianFanImageModel 方法 */ private QianFanImageModel buildQianFanImageModel(String key) { List keys = StrUtil.split(key, '|'); @@ -275,11 +257,13 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link YudaoAiAutoConfiguration#deepSeekChatModel(YudaoAiProperties)} */ private static DeepSeekChatModel buildDeepSeekChatModel(String apiKey) { - return new DeepSeekChatModel(apiKey); + YudaoAiProperties.DeepSeekProperties properties = new YudaoAiProperties.DeepSeekProperties() + .setApiKey(apiKey); + return new YudaoAiAutoConfiguration().buildDeepSeekChatModel(properties); } /** - * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiChatModel(ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)} + * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiChatModel 方法 */ private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); @@ -288,7 +272,7 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiImageModel(ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiImageModel 方法 */ private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); @@ -301,21 +285,22 @@ public class AiModelFactoryImpl implements AiModelFactory { */ private static XingHuoChatModel buildXingHuoChatModel(String key) { List keys = StrUtil.split(key, '|'); - Assert.equals(keys.size(), 3, "XingHuoChatClient 的密钥需要 (appid|appKey|secretKey) 格式"); - String appKey = keys.get(1); - String secretKey = keys.get(2); - return new XingHuoChatModel(appKey, secretKey); + 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 OpenAiAutoConfiguration} + * 可参考 {@link OpenAiAutoConfiguration} 的 openAiChatModel 方法 */ private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) { - url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL); - OpenAiApi openAiApi = new OpenAiApi(url, openAiToken); - return new OpenAiChatModel(openAiApi); + url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); + OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build(); + return OpenAiChatModel.builder().openAiApi(openAiApi).build(); } + // TODO @芋艿:手头暂时没密钥,使用建议再测试下 /** * 可参考 {@link AzureOpenAiAutoConfiguration} */ @@ -325,27 +310,28 @@ public class AiModelFactoryImpl implements AiModelFactory { AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties(); connectionProperties.setApiKey(apiKey); connectionProperties.setEndpoint(url); - OpenAIClient openAIClient = azureOpenAiAutoConfiguration.openAIClient(connectionProperties); + OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null); // 获取 AzureOpenAiChatProperties 对象 AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class); - return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, null, null); + return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, + null, null, null); } /** - * 可参考 {@link OpenAiAutoConfiguration} + * 可参考 {@link OpenAiAutoConfiguration} 的 openAiImageModel 方法 */ private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) { - url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL); - OpenAiImageApi openAiApi = new OpenAiImageApi(url, openAiToken, RestClient.builder()); + url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); + OpenAiImageApi openAiApi = OpenAiImageApi.builder().baseUrl(url).apiKey(openAiToken).build(); return new OpenAiImageModel(openAiApi); } /** - * 可参考 {@link OllamaAutoConfiguration} + * 可参考 {@link OllamaAutoConfiguration} 的 ollamaApi 方法 */ private static OllamaChatModel buildOllamaChatModel(String url) { OllamaApi ollamaApi = new OllamaApi(url); - return new OllamaChatModel(ollamaApi); + return OllamaChatModel.builder().ollamaApi(ollamaApi).build(); } private StabilityAiImageModel buildStabilityAiImageModel(String apiKey, String url) { @@ -356,13 +342,13 @@ public class AiModelFactoryImpl implements AiModelFactory { // ========== 各种创建 EmbeddingModel 的方法 ========== + // TODO @芋艿:需要测试下 /** - * 可参考 {@link TongYiAutoConfiguration#tongYiTextEmbeddingClient(TextEmbedding, TongYiConnectionProperties)} + * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeEmbeddingModel 方法 */ private EmbeddingModel buildTongYiEmbeddingModel(String apiKey) { - TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); - connectionProperties.setApiKey(apiKey); - return new TongYiAutoConfiguration().tongYiTextEmbeddingClient(SpringUtil.getBean(TextEmbedding.class), connectionProperties); + DashScopeApi dashScopeApi = new DashScopeApi(apiKey); + return new DashScopeEmbeddingModel(dashScopeApi); } } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java index 55091c78d4..a03a0c844f 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java @@ -8,9 +8,10 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.openai.api.ApiUtils; +import org.springframework.http.HttpHeaders; 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; @@ -18,6 +19,7 @@ import reactor.core.publisher.Mono; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -50,11 +52,19 @@ public class MidjourneyApi { public MidjourneyApi(String baseUrl, String apiKey, String notifyUrl) { this.webClient = WebClient.builder() .baseUrl(baseUrl) - .defaultHeaders(ApiUtils.getJsonContentHeaders(apiKey)) + .defaultHeaders(getJsonContentHeaders(apiKey)) .build(); this.notifyUrl = notifyUrl; } + // TODO @芋艿:这里,看看怎么调整下???https://github.com/spring-projects/spring-ai/issues/741 + public static Consumer getJsonContentHeaders(String apiKey) { + return (headers) -> { + headers.setBearerAuth(apiKey); + headers.setContentType(MediaType.APPLICATION_JSON); + }; + }; + /** * imagine - 根据提示词提交绘画任务 * diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index e18f100156..7203db4593 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -2,9 +2,7 @@ package cn.iocoder.yudao.framework.ai.core.util; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; -import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions; -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions; -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions; +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; @@ -21,26 +19,24 @@ import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; public class AiUtils { public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) { - Float temperatureF = temperature != null ? temperature.floatValue() : null; //noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: - return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build(); + // TODO @芋艿:tongyi 暂时没 maxTokens 选项 + return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).build(); case YI_YAN: - return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); - case DEEP_SEEK: - return DeepSeekChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build(); + return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case ZHI_PU: - return ZhiPuAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); - case XING_HUO: - return XingHuoChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build(); + return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case OPENAI: - return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); + case DEEP_SEEK: // 复用 OpenAI 客户端 + case XING_HUO: // 复用 OpenAI 客户端 + return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case AZURE_OPENAI: // TODO 芋艿:貌似没 model 字段???! - return AzureOpenAiChatOptions.builder().withDeploymentName(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build(); + return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens).build(); case OLLAMA: - return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens); + return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens).build(); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } @@ -56,8 +52,8 @@ public class AiUtils { if (MessageType.SYSTEM.getValue().equals(type)) { return new SystemMessage(content); } - if (MessageType.FUNCTION.getValue().equals(type)) { - return new FunctionMessage(content); + if (MessageType.TOOL.getValue().equals(type)) { + throw new UnsupportedOperationException("暂不支持 tool 消息:" + content); } throw new IllegalArgumentException(StrUtil.format("未知消息类型({})", type)); } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java deleted file mode 100644 index a72d50c4a8..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 org.springframework.ai.autoconfigure.vectorstore.redis; - -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.vectorstore.RedisVectorStore; -import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; -import redis.clients.jedis.JedisPooled; - -/** - * TODO @xin 先拿 spring-ai 最新代码覆盖,1.0.0-M1 跟 redis 自动配置会冲突 - * - * TODO 这个官方,有说啥时候 fix 哇? - * TODO 看着是列在1.0.0-M2版本 - * - * @author Christian Tzolov - * @author Eddú Meléndez - */ -@AutoConfiguration(after = RedisAutoConfiguration.class) -@ConditionalOnClass({JedisPooled.class, JedisConnectionFactory.class, RedisVectorStore.class, EmbeddingModel.class}) -@ConditionalOnBean(JedisConnectionFactory.class) -@EnableConfigurationProperties(RedisVectorStoreProperties.class) -public class RedisVectorStoreAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public RedisVectorStore vectorStore(EmbeddingModel embeddingModel, RedisVectorStoreProperties properties, - JedisConnectionFactory jedisConnectionFactory) { - - var config = RedisVectorStoreConfig.builder() - .withIndexName(properties.getIndex()) - .withPrefix(properties.getPrefix()) - .build(); - - return new RedisVectorStore(config, embeddingModel, - new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()), - properties.isInitializeSchema()); - } - -} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java deleted file mode 100644 index de80401ed1..0000000000 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java +++ /dev/null @@ -1,456 +0,0 @@ -/* - * 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 org.springframework.ai.vectorstore; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.ai.document.Document; -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.vectorstore.filter.FilterExpressionConverter; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import redis.clients.jedis.JedisPooled; -import redis.clients.jedis.Pipeline; -import redis.clients.jedis.json.Path2; -import redis.clients.jedis.search.*; -import redis.clients.jedis.search.Schema.FieldType; -import redis.clients.jedis.search.schemafields.*; -import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm; - -import java.text.MessageFormat; -import java.util.*; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -/** - * The RedisVectorStore is for managing and querying vector data in a Redis database. It - * offers functionalities like adding, deleting, and performing similarity searches on - * documents. - * - * The store utilizes RedisJSON and RedisSearch to handle JSON documents and to index and - * search vector data. It supports various vector algorithms (e.g., FLAT, HSNW) for - * efficient similarity searches. Additionally, it allows for custom metadata fields in - * the documents to be stored alongside the vector and content data. - * - * This class requires a RedisVectorStoreConfig configuration object for initialization, - * which includes settings like Redis URI, index name, field names, and vector algorithms. - * It also requires an EmbeddingModel to convert documents into embeddings before storing - * them. - * - * @author Julien Ruaux - * @author Christian Tzolov - * @author Eddú Meléndez - * @see VectorStore - * @see RedisVectorStoreConfig - * @see EmbeddingModel - */ -public class RedisVectorStore implements VectorStore, InitializingBean { - - public enum Algorithm { - - FLAT, HSNW - - } - - public record MetadataField(String name, FieldType fieldType) { - - public static MetadataField text(String name) { - return new MetadataField(name, FieldType.TEXT); - } - - public static MetadataField numeric(String name) { - return new MetadataField(name, FieldType.NUMERIC); - } - - public static MetadataField tag(String name) { - return new MetadataField(name, FieldType.TAG); - } - - } - - /** - * Configuration for the Redis vector store. - */ - public static final class RedisVectorStoreConfig { - - private final String indexName; - - private final String prefix; - - private final String contentFieldName; - - private final String embeddingFieldName; - - private final Algorithm vectorAlgorithm; - - private final List metadataFields; - - private RedisVectorStoreConfig() { - this(builder()); - } - - private RedisVectorStoreConfig(Builder builder) { - this.indexName = builder.indexName; - this.prefix = builder.prefix; - this.contentFieldName = builder.contentFieldName; - this.embeddingFieldName = builder.embeddingFieldName; - this.vectorAlgorithm = builder.vectorAlgorithm; - this.metadataFields = builder.metadataFields; - } - - /** - * Start building a new configuration. - * @return The entry point for creating a new configuration. - */ - public static Builder builder() { - - return new Builder(); - } - - /** - * {@return the default config} - */ - public static RedisVectorStoreConfig defaultConfig() { - - return builder().build(); - } - - public static class Builder { - - private String indexName = DEFAULT_INDEX_NAME; - - private String prefix = DEFAULT_PREFIX; - - private String contentFieldName = DEFAULT_CONTENT_FIELD_NAME; - - private String embeddingFieldName = DEFAULT_EMBEDDING_FIELD_NAME; - - private Algorithm vectorAlgorithm = DEFAULT_VECTOR_ALGORITHM; - - private List metadataFields = new ArrayList<>(); - - private Builder() { - } - - /** - * Configures the Redis index name to use. - * @param name the index name to use - * @return this builder - */ - public Builder withIndexName(String name) { - this.indexName = name; - return this; - } - - /** - * Configures the Redis key prefix to use (default: "embedding:"). - * @param prefix the prefix to use - * @return this builder - */ - public Builder withPrefix(String prefix) { - this.prefix = prefix; - return this; - } - - /** - * Configures the Redis content field name to use. - * @param name the content field name to use - * @return this builder - */ - public Builder withContentFieldName(String name) { - this.contentFieldName = name; - return this; - } - - /** - * Configures the Redis embedding field name to use. - * @param name the embedding field name to use - * @return this builder - */ - public Builder withEmbeddingFieldName(String name) { - this.embeddingFieldName = name; - return this; - } - - /** - * Configures the Redis vector algorithmto use. - * @param algorithm the vector algorithm to use - * @return this builder - */ - public Builder withVectorAlgorithm(Algorithm algorithm) { - this.vectorAlgorithm = algorithm; - return this; - } - - public Builder withMetadataFields(MetadataField... fields) { - return withMetadataFields(Arrays.asList(fields)); - } - - public Builder withMetadataFields(List fields) { - this.metadataFields = fields; - return this; - } - - /** - * {@return the immutable configuration} - */ - public RedisVectorStoreConfig build() { - - return new RedisVectorStoreConfig(this); - } - - } - - } - - private final boolean initializeSchema; - - public static final String DEFAULT_INDEX_NAME = "spring-ai-index"; - - public static final String DEFAULT_CONTENT_FIELD_NAME = "content"; - - public static final String DEFAULT_EMBEDDING_FIELD_NAME = "embedding"; - - public static final String DEFAULT_PREFIX = "embedding:"; - - public static final Algorithm DEFAULT_VECTOR_ALGORITHM = Algorithm.HSNW; - - private static final String QUERY_FORMAT = "%s=>[KNN %s @%s $%s AS %s]"; - - private static final Path2 JSON_SET_PATH = Path2.of("$"); - - private static final String JSON_PATH_PREFIX = "$."; - - private static final Logger logger = LoggerFactory.getLogger(RedisVectorStore.class); - - private static final Predicate RESPONSE_OK = Predicate.isEqual("OK"); - - private static final Predicate RESPONSE_DEL_OK = Predicate.isEqual(1l); - - private static final String VECTOR_TYPE_FLOAT32 = "FLOAT32"; - - private static final String EMBEDDING_PARAM_NAME = "BLOB"; - - public static final String DISTANCE_FIELD_NAME = "vector_score"; - - private static final String DEFAULT_DISTANCE_METRIC = "COSINE"; - - private final JedisPooled jedis; - - private final EmbeddingModel embeddingModel; - - private final RedisVectorStoreConfig config; - - private FilterExpressionConverter filterExpressionConverter; - - public RedisVectorStore(RedisVectorStoreConfig config, EmbeddingModel embeddingModel, JedisPooled jedis, - boolean initializeSchema) { - - Assert.notNull(config, "Config must not be null"); - Assert.notNull(embeddingModel, "Embedding model must not be null"); - this.initializeSchema = initializeSchema; - - this.jedis = jedis; - this.embeddingModel = embeddingModel; - this.config = config; - this.filterExpressionConverter = new RedisFilterExpressionConverter(this.config.metadataFields); - } - - public JedisPooled getJedis() { - return this.jedis; - } - - @Override - public void add(List documents) { - try (Pipeline pipeline = this.jedis.pipelined()) { - for (Document document : documents) { - var embedding = this.embeddingModel.embed(document); - document.setEmbedding(embedding); - - var fields = new HashMap(); - fields.put(this.config.embeddingFieldName, embedding); - fields.put(this.config.contentFieldName, document.getContent()); - fields.putAll(document.getMetadata()); - pipeline.jsonSetWithEscape(key(document.getId()), JSON_SET_PATH, fields); - } - List responses = pipeline.syncAndReturnAll(); - Optional errResponse = responses.stream().filter(Predicate.not(RESPONSE_OK)).findAny(); - if (errResponse.isPresent()) { - String message = MessageFormat.format("Could not add document: {0}", errResponse.get()); - if (logger.isErrorEnabled()) { - logger.error(message); - } - throw new RuntimeException(message); - } - } - } - - private String key(String id) { - return this.config.prefix + id; - } - - @Override - public Optional delete(List idList) { - try (Pipeline pipeline = this.jedis.pipelined()) { - for (String id : idList) { - pipeline.jsonDel(key(id)); - } - List responses = pipeline.syncAndReturnAll(); - Optional errResponse = responses.stream().filter(Predicate.not(RESPONSE_DEL_OK)).findAny(); - if (errResponse.isPresent()) { - if (logger.isErrorEnabled()) { - logger.error("Could not delete document: {}", errResponse.get()); - } - return Optional.of(false); - } - return Optional.of(true); - } - } - - @Override - public List similaritySearch(SearchRequest request) { - - Assert.isTrue(request.getTopK() > 0, "The number of documents to returned must be greater than zero"); - Assert.isTrue(request.getSimilarityThreshold() >= 0 && request.getSimilarityThreshold() <= 1, - "The similarity score is bounded between 0 and 1; least to most similar respectively."); - - String filter = nativeExpressionFilter(request); - - String queryString = String.format(QUERY_FORMAT, filter, request.getTopK(), this.config.embeddingFieldName, - EMBEDDING_PARAM_NAME, DISTANCE_FIELD_NAME); - - List returnFields = new ArrayList<>(); - this.config.metadataFields.stream().map(MetadataField::name).forEach(returnFields::add); - returnFields.add(this.config.embeddingFieldName); - returnFields.add(this.config.contentFieldName); - returnFields.add(DISTANCE_FIELD_NAME); - var embedding = toFloatArray(this.embeddingModel.embed(request.getQuery())); - Query query = new Query(queryString).addParam(EMBEDDING_PARAM_NAME, RediSearchUtil.toByteArray(embedding)) - .returnFields(returnFields.toArray(new String[0])) - .setSortBy(DISTANCE_FIELD_NAME, true) - .dialect(2); - - SearchResult result = this.jedis.ftSearch(this.config.indexName, query); - return result.getDocuments() - .stream() - .filter(d -> similarityScore(d) >= request.getSimilarityThreshold()) - .map(this::toDocument) - .toList(); - } - - private Document toDocument(redis.clients.jedis.search.Document doc) { - var id = doc.getId().substring(this.config.prefix.length()); - var content = doc.hasProperty(this.config.contentFieldName) ? doc.getString(this.config.contentFieldName) - : null; - Map metadata = this.config.metadataFields.stream() - .map(MetadataField::name) - .filter(doc::hasProperty) - .collect(Collectors.toMap(Function.identity(), doc::getString)); - metadata.put(DISTANCE_FIELD_NAME, 1 - similarityScore(doc)); - return new Document(id, content, metadata); - } - - private float similarityScore(redis.clients.jedis.search.Document doc) { - return (2 - Float.parseFloat(doc.getString(DISTANCE_FIELD_NAME))) / 2; - } - - private String nativeExpressionFilter(SearchRequest request) { - if (request.getFilterExpression() == null) { - return "*"; - } - return "(" + this.filterExpressionConverter.convertExpression(request.getFilterExpression()) + ")"; - } - - @Override - public void afterPropertiesSet() { - - if (!this.initializeSchema) { - return; - } - - // If index already exists don't do anything - if (this.jedis.ftList().contains(this.config.indexName)) { - return; - } - - String response = this.jedis.ftCreate(this.config.indexName, - FTCreateParams.createParams().on(IndexDataType.JSON).addPrefix(this.config.prefix), schemaFields()); - if (!RESPONSE_OK.test(response)) { - String message = MessageFormat.format("Could not create index: {0}", response); - throw new RuntimeException(message); - } - } - - private Iterable schemaFields() { - Map vectorAttrs = new HashMap<>(); - vectorAttrs.put("DIM", this.embeddingModel.dimensions()); - vectorAttrs.put("DISTANCE_METRIC", DEFAULT_DISTANCE_METRIC); - vectorAttrs.put("TYPE", VECTOR_TYPE_FLOAT32); - List fields = new ArrayList<>(); - fields.add(TextField.of(jsonPath(this.config.contentFieldName)).as(this.config.contentFieldName).weight(1.0)); - fields.add(VectorField.builder() - .fieldName(jsonPath(this.config.embeddingFieldName)) - .algorithm(vectorAlgorithm()) - .attributes(vectorAttrs) - .as(this.config.embeddingFieldName) - .build()); - - if (!CollectionUtils.isEmpty(this.config.metadataFields)) { - for (MetadataField field : this.config.metadataFields) { - fields.add(schemaField(field)); - } - } - return fields; - } - - private SchemaField schemaField(MetadataField field) { - String fieldName = jsonPath(field.name); - switch (field.fieldType) { - case NUMERIC: - return NumericField.of(fieldName).as(field.name); - case TAG: - return TagField.of(fieldName).as(field.name); - case TEXT: - return TextField.of(fieldName).as(field.name); - default: - throw new IllegalArgumentException( - MessageFormat.format("Field {0} has unsupported type {1}", field.name, field.fieldType)); - } - } - - private VectorAlgorithm vectorAlgorithm() { - if (config.vectorAlgorithm == Algorithm.HSNW) { - return VectorAlgorithm.HNSW; - } - return VectorAlgorithm.FLAT; - } - - private String jsonPath(String field) { - return JSON_PATH_PREFIX + field; - } - - private static float[] toFloatArray(List embeddingDouble) { - float[] embeddingFloat = new float[embeddingDouble.size()]; - int i = 0; - for (Double d : embeddingDouble) { - embeddingFloat[i++] = d.floatValue(); - } - return embeddingFloat; - } - -} \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java index c859587799..7713ec4688 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.framework.ai.chat; -import com.azure.ai.openai.OpenAIClient; import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.core.credential.AzureKeyCredential; import com.azure.core.util.ClientOptions; @@ -27,13 +26,13 @@ import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatP */ public class AzureOpenAIChatModelTests { - private final OpenAIClient openAiApi = (new OpenAIClientBuilder()) + // TODO @芋艿:晚点在调整 + private final OpenAIClientBuilder openAiApi = new OpenAIClientBuilder() .endpoint("https://eastusprejade.openai.azure.com") .credential(new AzureKeyCredential("xxx")) - .clientOptions((new ClientOptions()).setApplicationId("spring-ai")) - .buildClient(); + .clientOptions((new ClientOptions()).setApplicationId("spring-ai")); private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi, - AzureOpenAiChatOptions.builder().withDeploymentName(DEFAULT_DEPLOYMENT_NAME).build()); + AzureOpenAiChatOptions.builder().deploymentName(DEFAULT_DEPLOYMENT_NAME).build()); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java index c6b99f287b..a72b556fcd 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java @@ -1,20 +1,6 @@ package cn.iocoder.yudao.framework.ai.chat; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.SystemMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.ollama.OllamaChatModel; -import org.springframework.ai.ollama.api.OllamaApi; -import org.springframework.ai.ollama.api.OllamaModel; -import org.springframework.ai.ollama.api.OllamaOptions; -import reactor.core.publisher.Flux; - -import java.util.ArrayList; -import java.util.List; /** * {@link OllamaChatModel} 集成测试 @@ -23,41 +9,41 @@ import java.util.List; */ public class LlamaChatModelTests { - private final OllamaApi ollamaApi = new OllamaApi( - "http://127.0.0.1:11434"); - private final OllamaChatModel chatModel = new OllamaChatModel(ollamaApi, - OllamaOptions.create().withModel(OllamaModel.LLAMA3.getModelName())); - - @Test - @Disabled - public void testCall() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("1 + 1 = ?")); - - // 调用 - ChatResponse response = chatModel.call(new Prompt(messages)); - // 打印结果 - System.out.println(response); - System.out.println(response.getResult().getOutput()); - } - - @Test - @Disabled - public void testStream() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("1 + 1 = ?")); - - // 调用 - Flux flux = chatModel.stream(new Prompt(messages)); - // 打印结果 - flux.doOnNext(response -> { -// System.out.println(response); - System.out.println(response.getResult().getOutput()); - }).then().block(); - } +// private final OllamaApi ollamaApi = new OllamaApi( +// "http://127.0.0.1:11434"); +// private final OllamaChatModel chatModel = new OllamaChatModel(ollamaApi, +// OllamaOptions.create().withModel(OllamaModel.LLAMA3.getModelName())); +// +// @Test +// @Disabled +// public void testCall() { +// // 准备参数 +// List messages = new ArrayList<>(); +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); +// messages.add(new UserMessage("1 + 1 = ?")); +// +// // 调用 +// ChatResponse response = chatModel.call(new Prompt(messages)); +// // 打印结果 +// System.out.println(response); +// System.out.println(response.getResult().getOutput()); +// } +// +// @Test +// @Disabled +// public void testStream() { +// // 准备参数 +// List messages = new ArrayList<>(); +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); +// messages.add(new UserMessage("1 + 1 = ?")); +// +// // 调用 +// Flux flux = chatModel.stream(new Prompt(messages)); +// // 打印结果 +// flux.doOnNext(response -> { +//// System.out.println(response); +// System.out.println(response.getResult().getOutput()); +// }).then().block(); +// } } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java index 6768325462..b6653b1b15 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java @@ -26,7 +26,7 @@ public class OpenAIChatModelTests { "https://api.holdai.top", "sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf"); private final OpenAiChatModel chatModel = new OpenAiChatModel(openAiApi, - OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_O).build()); + OpenAiChatOptions.builder().model(OpenAiApi.ChatModel.GPT_4_O).build()); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java index d10a04677f..e271decbc5 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java @@ -25,7 +25,7 @@ public class YiYanChatModelTests { "qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"); private final QianFanChatModel chatModel = new QianFanChatModel(qianFanApi, - QianFanChatOptions.builder().withModel(QianFanApi.ChatModel.ERNIE_Tiny_8K.getValue()).build() + QianFanChatOptions.builder().model(QianFanApi.ChatModel.ERNIE_Tiny_8K.getValue()).build() ); @Test diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java index 20b73bd8e7..101be34b7b 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java @@ -24,7 +24,7 @@ public class ZhiPuAiChatModelTests { private final ZhiPuAiApi zhiPuAiApi = new ZhiPuAiApi("32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs"); private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(zhiPuAiApi, - ZhiPuAiChatOptions.builder().withModel(ZhiPuAiApi.ChatModel.GLM_4.getModelName()).build()); + ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.ChatModel.GLM_4.getName()).build()); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java index 22bf6614eb..4c0a9eb36d 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java @@ -25,9 +25,9 @@ public class QianFanImageTests { // 准备参数 // 只支持 1024x1024、768x768、768x1024、1024x768、576x1024、1024x576 QianFanImageOptions imageOptions = QianFanImageOptions.builder() - .withModel(QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue()) - .withWidth(1024).withHeight(1024) - .withN(1) + .model(QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue()) + .width(1024).height(1024) + .N(1) .build(); ImagePrompt prompt = new ImagePrompt("good", imageOptions); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java index 41d7859c4d..cc62f02fb6 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java @@ -1,34 +1,30 @@ package cn.iocoder.yudao.framework.ai.image; -import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel; +import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; -import com.alibaba.dashscope.utils.Constants; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.image.ImageOptions; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; -import org.springframework.ai.openai.OpenAiImageOptions; /** - * {@link com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel} 集成测试类 + * {@link DashScopeImageModel} 集成测试类 * * @author fansili */ public class TongYiImagesModelTest { - private final ImageSynthesis imageApi = new ImageSynthesis(); - private final TongYiImagesModel imageModel = new TongYiImagesModel(imageApi); - - static { - Constants.apiKey = "sk-Zsd81gZYg7"; - } + private final DashScopeImageModel imageModel = new DashScopeImageModel( + new DashScopeImageApi("sk-7d903764249848cfa912733146da12d1")); @Test @Disabled public void imageCallTest() { // 准备参数 - ImageOptions options = OpenAiImageOptions.builder() + ImageOptions options = DashScopeImageOptions.builder() .withModel(ImageSynthesis.Models.WANX_V1) .withHeight(256).withWidth(256) .build(); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java index f9338995f3..f4802d4d21 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java @@ -22,7 +22,7 @@ public class ZhiPuAiImageModelTests { public void testCall() { // 准备参数 ZhiPuAiImageOptions imageOptions = ZhiPuAiImageOptions.builder() - .withModel(ZhiPuAiImageApi.ImageModel.CogView_3.getValue()) + .model(ZhiPuAiImageApi.ImageModel.CogView_3.getValue()) .build(); ImagePrompt prompt = new ImagePrompt("万里长城", imageOptions); From 5655ae925ccda0492d5b0c5d34efb901145a29e1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 23 Feb 2025 17:19:19 +0800 Subject: [PATCH 06/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91AI=EF=BC=9A=E9=80=82=E9=85=8D=20Spring=20AI?= =?UTF-8?q?=201.0.6=20=E5=AF=B9=20OpenAI=20=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/service/chat/AiChatMessageServiceImpl.java | 4 ++-- .../knowledge/AiKnowledgeDocumentServiceImpl.java | 6 +++--- .../knowledge/AiKnowledgeSegmentServiceImpl.java | 10 ++++++---- .../ai/service/mindmap/AiMindMapServiceImpl.java | 2 +- .../ai/service/write/AiWriteServiceImpl.java | 2 +- .../framework/ai/chat/OpenAIChatModelTests.java | 15 ++++++++++----- .../framework/ai/image/OpenAiImageModelTests.java | 12 +++++++----- yudao-server/src/main/resources/application.yaml | 9 +++------ 8 files changed, 33 insertions(+), 27 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java index d332fbf1a6..8c4a08e1d0 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -98,7 +98,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { ChatResponse chatResponse = chatModel.call(prompt); // 3.4 段式返回 - String newContent = chatResponse.getResult().getOutput().getContent(); + String newContent = chatResponse.getResult().getOutput().getText(); chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId)).setContent(newContent)); return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent)); @@ -136,7 +136,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { // TODO 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题 StringBuffer contentBuffer = new StringBuffer(); return streamResponse.map(chunk -> { - String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null; + String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 contentBuffer.append(newContent); // 响应结果 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index ff475f92ca..f5855db7d9 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -60,7 +60,7 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic List documents = loader.get(); Document document = CollUtil.getFirst(documents); // 1.2 文档记录入库 - String content = document.getContent(); + String content = document.getText(); AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class) .setTokens(tokenCountEstimator.estimate(content)).setWordCount(content.length()) .setStatus(CommonStatusEnum.ENABLE.getStatus()).setSliceStatus(AiKnowledgeDocumentStatusEnum.SUCCESS.getStatus()); @@ -77,9 +77,9 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic List segments = tokenTextSplitter.apply(documents); // 2.2 分段内容入库 List segmentDOList = CollectionUtils.convertList(segments, - segment -> new AiKnowledgeSegmentDO().setContent(segment.getContent()).setDocumentId(documentId) + segment -> new AiKnowledgeSegmentDO().setContent(segment.getText()).setDocumentId(documentId) .setKnowledgeId(createReqVO.getKnowledgeId()).setVectorId(segment.getId()) - .setTokens(tokenCountEstimator.estimate(segment.getContent())).setWordCount(segment.getContent().length()) + .setTokens(tokenCountEstimator.estimate(segment.getText())).setWordCount(segment.getText().length()) .setStatus(CommonStatusEnum.ENABLE.getStatus())); segmentMapper.insertBatch(segmentDOList); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index 5523fe2783..afb9d755f0 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -106,10 +106,12 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService VectorStore vectorStore = apiKeyService.getOrCreateVectorStore(model.getKeyId()); // 3.1 向量检索 - List documentList = vectorStore.similaritySearch(SearchRequest.query(reqVO.getContent()) - .withTopK(knowledge.getTopK()) - .withSimilarityThreshold(knowledge.getSimilarityThreshold()) - .withFilterExpression(new FilterExpressionBuilder().eq(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, reqVO.getKnowledgeId()).build())); + List documentList = vectorStore.similaritySearch(SearchRequest.builder() + .query(reqVO.getContent()) + .topK(knowledge.getTopK()) + .similarityThreshold(knowledge.getSimilarityThreshold()) + .filterExpression(new FilterExpressionBuilder().eq(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, reqVO.getKnowledgeId()).build()) + .build()); if (CollUtil.isEmpty(documentList)) { return ListUtil.empty(); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java index b34bd63481..5536831469 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java @@ -85,7 +85,7 @@ public class AiMindMapServiceImpl implements AiMindMapService { // 3.2 流式返回 StringBuffer contentBuffer = new StringBuffer(); return streamResponse.map(chunk -> { - String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null; + String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 contentBuffer.append(newContent); // 响应结果 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java index b8f22a2fa6..f238bd3e30 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java @@ -92,7 +92,7 @@ public class AiWriteServiceImpl implements AiWriteService { // 3.2 流式返回 StringBuffer contentBuffer = new StringBuffer(); return streamResponse.map(chunk -> { - String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null; + String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 contentBuffer.append(newContent); // 响应结果 diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java index b6653b1b15..eb52c2bfb8 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java @@ -22,11 +22,16 @@ import java.util.List; */ public class OpenAIChatModelTests { - private final OpenAiApi openAiApi = new OpenAiApi( - "https://api.holdai.top", - "sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf"); - private final OpenAiChatModel chatModel = new OpenAiChatModel(openAiApi, - OpenAiChatOptions.builder().model(OpenAiApi.ChatModel.GPT_4_O).build()); + private static final OpenAiChatModel chatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl("https://api.holdai.top") + .apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") // apiKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(OpenAiApi.ChatModel.GPT_4_O) // 模型 + .temperature(0.7) + .build()) + .build(); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageModelTests.java index c9b07d9ff2..f7a64e6a6a 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageModelTests.java @@ -5,8 +5,11 @@ import org.junit.jupiter.api.Test; import org.springframework.ai.image.ImageOptions; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.OpenAiImageModel; import org.springframework.ai.openai.OpenAiImageOptions; +import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiImageApi; import org.springframework.web.client.RestClient; @@ -17,11 +20,10 @@ import org.springframework.web.client.RestClient; */ public class OpenAiImageModelTests { - private final OpenAiImageApi imageApi = new OpenAiImageApi( - "https://api.holdai.top", - "sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf", - RestClient.builder()); - private final OpenAiImageModel imageModel = new OpenAiImageModel(imageApi); + private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder() + .baseUrl("https://api.holdai.top") // apiKey + .apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") + .build()); @Test @Disabled diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index e0e135c7be..223fffa7c7 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -163,7 +163,7 @@ spring: zhipuai: # 智谱 AI api-key: 32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs openai: # OpenAI 官方 - api-key: sk-yzKea6d8e8212c3bdd99f9f44ced1cae37c097e5aa3BTS7z + api-key: sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17 base-url: https://api.gptsapi.net azure: # OpenAI 微软 openai: @@ -175,11 +175,8 @@ spring: model: llama3 stabilityai: api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx - cloud: - ai: - tongyi: # 通义千问 - tongyi: - api-key: sk-Zsd81gZYg7 + dashscope: # 通义千问 + api-key: sk-71800982914041848008480000000000 yudao: ai: From d05a7bd59a83ab5a0102d7380022c6f0eeedff85 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 23 Feb 2025 17:33:53 +0800 Subject: [PATCH 07/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91AI=EF=BC=9A=E9=80=82=E9=85=8D=20Spring=20AI?= =?UTF-8?q?=201.0.6=20=E5=AF=B9=20Ollama=20=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/service/image/AiImageServiceImpl.java | 23 ++--- .../ai/config/YudaoAiAutoConfiguration.java | 1 - .../ai/chat/LlamaChatModelTests.java | 88 +++++++++++-------- 3 files changed, 64 insertions(+), 48 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index 3e8c09f079..523014d530 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -20,6 +20,7 @@ import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper; import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; import cn.iocoder.yudao.module.infra.api.file.FileApi; +import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.image.ImageModel; @@ -133,14 +134,14 @@ public class AiImageServiceImpl implements AiImageService { } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.STABLE_DIFFUSION.getPlatform())) { // https://platform.stability.ai/docs/api-reference#tag/SDXL-and-SD1.6/operation/textToImage // https://platform.stability.ai/docs/api-reference#tag/Text-to-Image/operation/textToImage - return StabilityAiImageOptions.builder().withModel(draw.getModel()) - .withHeight(draw.getHeight()).withWidth(draw.getWidth()) - .withSeed(Long.valueOf(draw.getOptions().get("seed"))) - .withCfgScale(Float.valueOf(draw.getOptions().get("scale"))) - .withSteps(Integer.valueOf(draw.getOptions().get("steps"))) - .withSampler(String.valueOf(draw.getOptions().get("sampler"))) - .withStylePreset(String.valueOf(draw.getOptions().get("stylePreset"))) - .withClipGuidancePreset(String.valueOf(draw.getOptions().get("clipGuidancePreset"))) + return StabilityAiImageOptions.builder().model(draw.getModel()) + .height(draw.getHeight()).width(draw.getWidth()) + .seed(Long.valueOf(draw.getOptions().get("seed"))) + .cfgScale(Float.valueOf(draw.getOptions().get("scale"))) + .steps(Integer.valueOf(draw.getOptions().get("steps"))) + .sampler(String.valueOf(draw.getOptions().get("sampler"))) + .stylePreset(String.valueOf(draw.getOptions().get("stylePreset"))) + .clipGuidancePreset(String.valueOf(draw.getOptions().get("clipGuidancePreset"))) .build(); } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) { return DashScopeImageOptions.builder() @@ -149,12 +150,12 @@ public class AiImageServiceImpl implements AiImageService { .build(); } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) { return QianFanImageOptions.builder() - .withModel(draw.getModel()).withN(1) - .withHeight(draw.getHeight()).withWidth(draw.getWidth()) + .model(draw.getModel()).N(1) + .height(draw.getHeight()).width(draw.getWidth()) .build(); } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.ZHI_PU.getPlatform())) { return ZhiPuAiImageOptions.builder() - .withModel(draw.getModel()) + .model(draw.getModel()) .build(); } throw new IllegalArgumentException("不支持的 AI 平台:" + draw.getPlatform()); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 3aed3ee1b9..d9b143bb04 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -35,7 +35,6 @@ public class YudaoAiAutoConfiguration { return new AiModelFactoryImpl(); } - // ========== 各种 AI Client 创建 ========== @Bean diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java index a72b556fcd..497a6fe9a9 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java @@ -1,6 +1,20 @@ package cn.iocoder.yudao.framework.ai.chat; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.ollama.OllamaChatModel; +import org.springframework.ai.ollama.api.OllamaApi; +import org.springframework.ai.ollama.api.OllamaModel; +import org.springframework.ai.ollama.api.OllamaOptions; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; /** * {@link OllamaChatModel} 集成测试 @@ -9,41 +23,43 @@ import org.springframework.ai.ollama.OllamaChatModel; */ public class LlamaChatModelTests { -// private final OllamaApi ollamaApi = new OllamaApi( -// "http://127.0.0.1:11434"); -// private final OllamaChatModel chatModel = new OllamaChatModel(ollamaApi, -// OllamaOptions.create().withModel(OllamaModel.LLAMA3.getModelName())); -// -// @Test -// @Disabled -// public void testCall() { -// // 准备参数 -// List messages = new ArrayList<>(); -// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); -// messages.add(new UserMessage("1 + 1 = ?")); -// -// // 调用 -// ChatResponse response = chatModel.call(new Prompt(messages)); -// // 打印结果 -// System.out.println(response); -// System.out.println(response.getResult().getOutput()); -// } -// -// @Test -// @Disabled -// public void testStream() { -// // 准备参数 -// List messages = new ArrayList<>(); -// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); -// messages.add(new UserMessage("1 + 1 = ?")); -// -// // 调用 -// Flux flux = chatModel.stream(new Prompt(messages)); -// // 打印结果 -// flux.doOnNext(response -> { -//// System.out.println(response); -// System.out.println(response.getResult().getOutput()); -// }).then().block(); -// } + private final OllamaChatModel chatModel = OllamaChatModel.builder() + .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 + .defaultOptions(OllamaOptions.builder() + .model(OllamaModel.LLAMA3.getName()) // 模型 + .build()) + .build(); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(response.getResult().getOutput()); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(response.getResult().getOutput()); + }).then().block(); + } } From c96d966a41622bd41d3dbbfad6062e66689d64c4 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 23 Feb 2025 17:57:59 +0800 Subject: [PATCH 08/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91AI=EF=BC=9A=E6=97=A0=E6=B3=95=E9=80=82?= =?UTF-8?q?=E9=85=8D=20Spring=20AI=201.0.6=20=E5=AF=B9=E7=99=BE=E5=BA=A6?= =?UTF-8?q?=20QianFan=20=E7=9A=84=E9=80=BB=E8=BE=91=EF=BC=9A=E6=97=A0?= =?UTF-8?q?=E8=AF=AD=EF=BC=81=EF=BC=81=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/framework/ai/chat/YiYanChatModelTests.java | 11 ++++++----- .../yudao/framework/ai/image/QianFanImageTests.java | 7 ++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java index e271decbc5..baa36d86e8 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java @@ -14,6 +14,7 @@ import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; +// TODO @芋艿:百度千帆 API 提供了 V2 版本,目前 Spring AI 不兼容,可关键 进展 /** * {@link QianFanChatModel} 的集成测试 * @@ -21,11 +22,11 @@ import java.util.List; */ public class YiYanChatModelTests { - private final QianFanApi qianFanApi = new QianFanApi( - "qS8k8dYr2nXunagK4SSU8Xjj", - "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"); - private final QianFanChatModel chatModel = new QianFanChatModel(qianFanApi, - QianFanChatOptions.builder().model(QianFanApi.ChatModel.ERNIE_Tiny_8K.getValue()).build() + private final QianFanChatModel chatModel = new QianFanChatModel( + new QianFanApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"), // 密钥 + QianFanChatOptions.builder() + .model(QianFanApi.ChatModel.ERNIE_4_0_8K_Preview.getValue()) + .build() ); @Test diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java index 4c0a9eb36d..c284d8c76d 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java @@ -10,14 +10,15 @@ import org.springframework.ai.qianfan.api.QianFanImageApi; import static cn.iocoder.yudao.framework.ai.image.StabilityAiImageModelTests.viewImage; +// TODO @芋艿:百度千帆 API 提供了 V2 版本,目前 Spring AI 不兼容,可关键 进展 + /** * {@link QianFanImageModel} 集成测试类 */ public class QianFanImageTests { - private final QianFanImageApi imageApi = new QianFanImageApi( - "qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"); - private final QianFanImageModel imageModel = new QianFanImageModel(imageApi); + private final QianFanImageModel imageModel = new QianFanImageModel( + new QianFanImageApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e")); // 密钥 @Test @Disabled From 430dc99d124a695e00af55bbc9ec6fdee324a026 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 23 Feb 2025 18:29:33 +0800 Subject: [PATCH 09/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91AI=EF=BC=9A=E9=80=82=E9=85=8D=20Spring=20AI?= =?UTF-8?q?=201.0.6=20=E5=AF=B9=20ZhiPu=20=E7=9A=84=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/framework/ai/chat/ZhiPuAiChatModelTests.java | 9 ++++++--- .../yudao/framework/ai/image/ZhiPuAiImageModelTests.java | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java index 101be34b7b..4517482a06 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java @@ -22,9 +22,12 @@ import java.util.List; */ public class ZhiPuAiChatModelTests { - private final ZhiPuAiApi zhiPuAiApi = new ZhiPuAiApi("32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs"); - private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(zhiPuAiApi, - ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.ChatModel.GLM_4.getName()).build()); + private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel( + new ZhiPuAiApi("32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs"), // 密钥 + ZhiPuAiChatOptions.builder() + .model(ZhiPuAiApi.ChatModel.GLM_4.getName()) // 模型 + .build() + ); @Test @Disabled diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java index f4802d4d21..cb0b4efb78 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java @@ -13,9 +13,9 @@ import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; */ public class ZhiPuAiImageModelTests { - private final ZhiPuAiImageApi imageApi = new ZhiPuAiImageApi( - "78d3228c1d9e5e342a3e1ab349e2dd7b.VXLoq5vrwK2ofboy"); - private final ZhiPuAiImageModel imageModel = new ZhiPuAiImageModel(imageApi); + private final ZhiPuAiImageModel imageModel = new ZhiPuAiImageModel( + new ZhiPuAiImageApi("78d3228c1d9e5e342a3e1ab349e2dd7b.VXLoq5vrwK2ofboy") // 密钥 + ); @Test @Disabled From 4ee638db87e7e7344be2dba888f06cb65d72a579 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 23 Feb 2025 18:47:35 +0800 Subject: [PATCH 10/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91AI=EF=BC=9A=E9=80=82=E9=85=8D=20Spring=20AI?= =?UTF-8?q?=201.0.6=20=E5=AF=B9=20sd=E3=80=81mj=20=E7=9A=84=E5=85=BC?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/model/midjourney/api/MidjourneyApi.java | 15 ++++----------- .../framework/ai/image/MidjourneyApiTests.java | 4 ++-- .../ai/image/StabilityAiImageModelTests.java | 8 ++++---- yudao-server/src/main/resources/application.yaml | 5 ++--- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java index a03a0c844f..fe784d86b7 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java @@ -8,7 +8,6 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; @@ -19,7 +18,6 @@ import reactor.core.publisher.Mono; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -52,19 +50,14 @@ public class MidjourneyApi { public MidjourneyApi(String baseUrl, String apiKey, String notifyUrl) { this.webClient = WebClient.builder() .baseUrl(baseUrl) - .defaultHeaders(getJsonContentHeaders(apiKey)) + .defaultHeaders(httpHeaders -> { + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + httpHeaders.setBearerAuth(apiKey); + }) .build(); this.notifyUrl = notifyUrl; } - // TODO @芋艿:这里,看看怎么调整下???https://github.com/spring-projects/spring-ai/issues/741 - public static Consumer getJsonContentHeaders(String apiKey) { - return (headers) -> { - headers.setBearerAuth(apiKey); - headers.setContentType(MediaType.APPLICATION_JSON); - }; - }; - /** * imagine - 根据提示词提交绘画任务 * diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/MidjourneyApiTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/MidjourneyApiTests.java index c03cebb2bf..383b1e5c4b 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/MidjourneyApiTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/MidjourneyApiTests.java @@ -15,8 +15,8 @@ import java.util.List; public class MidjourneyApiTests { private final MidjourneyApi midjourneyApi = new MidjourneyApi( - "https://api.holdai.top/mj", - "sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf", + "https://api.holdai.top/mj", // 链接 + "sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17", // 密钥 null); @Test diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/StabilityAiImageModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/StabilityAiImageModelTests.java index 7ee7e6044e..5c3aa1f411 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/StabilityAiImageModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/StabilityAiImageModelTests.java @@ -22,9 +22,9 @@ import java.util.concurrent.TimeUnit; */ public class StabilityAiImageModelTests { - private final StabilityAiApi imageApi = new StabilityAiApi( - "sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx"); - private final StabilityAiImageModel imageModel = new StabilityAiImageModel(imageApi); + private final StabilityAiImageModel imageModel = new StabilityAiImageModel( + new StabilityAiApi("sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx") // 密钥 + ); @Test @Disabled @@ -32,7 +32,7 @@ public class StabilityAiImageModelTests { // 准备参数 ImageOptions options = OpenAiImageOptions.builder() .withModel("stable-diffusion-v1-6") - .withHeight(256).withWidth(256) + .withHeight(320).withWidth(320) .build(); ImagePrompt prompt = new ImagePrompt("great wall", options); diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 223fffa7c7..e5e863f59f 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -186,9 +186,8 @@ yudao: model: deepseek-chat xinghuo: # 讯飞星火 enable: true - appId: 13c8cca6 - appKey: cb6415c19d6162cda07b47316fcb0416 - secretKey: Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh + appKey: 75b161ed2aef4719b275d6e7f2a4d4cd + secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz model: generalv3.5 midjourney: enable: true From 875cfa10c6fbbd4ab224fe87b973daee84b7d7e1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 23 Feb 2025 19:22:48 +0800 Subject: [PATCH 11/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E9=80=82=E9=85=8D=20=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=20OllamaChatModelTests=EF=BC=8C=E7=94=A8=E4=BA=8E?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=20ollma=20=E6=A8=A1=E5=9E=8B=E7=9A=84?= =?UTF-8?q?=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/chat/OllamaChatModelTests.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OllamaChatModelTests.java diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OllamaChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OllamaChatModelTests.java new file mode 100644 index 0000000000..6bb08f7010 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OllamaChatModelTests.java @@ -0,0 +1,65 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.ollama.OllamaChatModel; +import org.springframework.ai.ollama.api.OllamaApi; +import org.springframework.ai.ollama.api.OllamaOptions; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link OllamaChatModel} 集成测试 + * + * @author 芋道源码 + */ +public class OllamaChatModelTests { + + private final OllamaChatModel chatModel = OllamaChatModel.builder() + .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 + .defaultOptions(OllamaOptions.builder() +// .model("qwen") // 模型(https://ollama.com/library/qwen) + .model("deepseek-r1") // 模型(https://ollama.com/library/deepseek-r1) + .build()) + .build(); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(response.getResult().getOutput()); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(response.getResult().getOutput()); + }).then().block(); + } + +} From 46726fe67c0feddcdebb6b87c1b855e1fd30d7dd Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 23 Feb 2025 20:00:54 +0800 Subject: [PATCH 12/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E9=80=82=E9=85=8D=E5=AD=97?= =?UTF-8?q?=E8=8A=82=E8=B1=86=E5=8C=85=20doubao=E3=80=81deepseek?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/config/YudaoAiAutoConfiguration.java | 29 +++++++- .../ai/config/YudaoAiProperties.java | 24 ++++++- .../ai/core/enums/AiPlatformEnum.java | 1 + .../ai/core/factory/AiModelFactoryImpl.java | 14 ++++ .../ai/core/model/doubao/DouBaoChatModel.java | 45 ++++++++++++ .../yudao/framework/ai/core/util/AiUtils.java | 1 + .../ai/chat/DouBaoChatModelTests.java | 70 +++++++++++++++++++ .../src/main/resources/application.yaml | 4 ++ 8 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/doubao/DouBaoChatModel.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index d9b143bb04..1a8b374174 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -4,6 +4,7 @@ import cn.hutool.core.util.StrUtil; 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.deepseek.DeepSeekChatModel; +import cn.iocoder.yudao.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; @@ -40,7 +41,7 @@ public class YudaoAiAutoConfiguration { @Bean @ConditionalOnProperty(value = "yudao.ai.deepseek.enable", havingValue = "true") public DeepSeekChatModel deepSeekChatModel(YudaoAiProperties yudaoAiProperties) { - YudaoAiProperties.DeepSeekProperties properties = yudaoAiProperties.getDeepSeek(); + YudaoAiProperties.DeepSeekProperties properties = yudaoAiProperties.getDeepseek(); return buildDeepSeekChatModel(properties); } @@ -89,6 +90,32 @@ public class YudaoAiAutoConfiguration { return new XingHuoChatModel(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()) + .build(); + return new DouBaoChatModel(openAiChatModel); + } + @Bean @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true") public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) { diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java index 7a98c4376a..8e36355e6d 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java @@ -16,11 +16,19 @@ public class YudaoAiProperties { /** * DeepSeek */ - private DeepSeekProperties deepSeek; + @SuppressWarnings("SpellCheckingInspection") + private DeepSeekProperties deepseek; + + /** + * 字节豆包 + */ + @SuppressWarnings("SpellCheckingInspection") + private DouBaoProperties doubao; /** * 讯飞星火 */ + @SuppressWarnings("SpellCheckingInspection") private XingHuoProperties xinghuo; /** @@ -31,6 +39,7 @@ public class YudaoAiProperties { /** * Suno 音乐 */ + @SuppressWarnings("SpellCheckingInspection") private SunoProperties suno; @Data @@ -61,6 +70,19 @@ public class YudaoAiProperties { } + @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 MidjourneyProperties { diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java index 1922e9a2cf..f5d8bd655a 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java @@ -19,6 +19,7 @@ public enum AiPlatformEnum { DEEP_SEEK("DeepSeek", "DeepSeek"), // DeepSeek ZHI_PU("ZhiPu", "智谱"), // 智谱 AI XING_HUO("XingHuo", "星火"), // 讯飞 + DOU_BAO("DouBao", "豆包"), // 字节 // ========== 国外平台 ========== diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index 985d8fd041..8928407a52 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -10,6 +10,7 @@ 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.deepseek.DeepSeekChatModel; +import cn.iocoder.yudao.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; @@ -74,6 +75,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return buildYiYanChatModel(apiKey); case DEEP_SEEK: return buildDeepSeekChatModel(apiKey); + case DOU_BAO: + return buildDouBaoChatModel(apiKey); case ZHI_PU: return buildZhiPuChatModel(apiKey, url); case XING_HUO: @@ -100,6 +103,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return SpringUtil.getBean(QianFanChatModel.class); case DEEP_SEEK: return SpringUtil.getBean(DeepSeekChatModel.class); + case DOU_BAO: + return SpringUtil.getBean(DouBaoChatModel.class); case ZHI_PU: return SpringUtil.getBean(ZhiPuAiChatModel.class); case XING_HUO: @@ -262,6 +267,15 @@ public class AiModelFactoryImpl implements AiModelFactory { 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 ZhiPuAiAutoConfiguration} 的 zhiPuAiChatModel 方法 */ diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/doubao/DouBaoChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/doubao/DouBaoChatModel.java new file mode 100644 index 0000000000..b6b17effee --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/doubao/DouBaoChatModel.java @@ -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 stream(Prompt prompt) { + return openAiChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return openAiChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index 7203db4593..bb9d530a31 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -30,6 +30,7 @@ public class AiUtils { return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case OPENAI: case DEEP_SEEK: // 复用 OpenAI 客户端 + case DOU_BAO: // 复用 OpenAI 客户端 case XING_HUO: // 复用 OpenAI 客户端 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case AZURE_OPENAI: diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java new file mode 100644 index 0000000000..f3de2a6300 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; +import cn.iocoder.yudao.framework.ai.core.model.doubao.DouBaoChatModel; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link DeepSeekChatModel} 集成测试 + * + * @author 芋道源码 + */ +public class DouBaoChatModelTests { + + private static final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(DouBaoChatModel.BASE_URL) + .apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model("doubao-1-5-lite-32k-250115") // 模型(doubao) +// .model("deepseek-r1-250120") // 模型(deepseek) + .temperature(0.7) + .build()) + .build(); + + private final DouBaoChatModel chatModel = new DouBaoChatModel(openAiChatModel); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + } + + // TODO @芋艿:因为使用的是 v1 api,导致 deepseek-r1-250120 不返回 think 过程,后续需要优化 + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(System.out::println).then().block(); + } + +} diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index e5e863f59f..cac5e5a142 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -184,6 +184,10 @@ yudao: enable: true api-key: sk-e94db327cc7d457d99a8de8810fc6b12 model: deepseek-chat + doubao: # 字节豆包 + enable: true + api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 + model: doubao-1-5-lite-32k-250115 xinghuo: # 讯飞星火 enable: true appKey: 75b161ed2aef4719b275d6e7f2a4d4cd From a1d5602c404ca6483045a85541e663c2269aa316 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 23 Feb 2025 21:14:53 +0800 Subject: [PATCH 13/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E9=80=82=E9=85=8D=E8=85=BE?= =?UTF-8?q?=E8=AE=AF=E6=B7=B7=E5=85=83=E5=A4=A7=E6=A8=A1=E5=9E=8B=E3=80=81?= =?UTF-8?q?=E7=9F=A5=E8=AF=86=E5=BC=95=E6=93=8E=EF=BC=88deepseek=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/config/YudaoAiAutoConfiguration.java | 85 ++++++++++---- .../ai/config/YudaoAiProperties.java | 50 +++++--- .../ai/core/enums/AiPlatformEnum.java | 1 + .../ai/core/factory/AiModelFactoryImpl.java | 16 ++- .../core/model/hunyuan/HunYuanChatModel.java | 52 ++++++++ .../yudao/framework/ai/core/util/AiUtils.java | 1 + .../ai/chat/DeepSeekChatModelTests.java | 2 +- .../ai/chat/DouBaoChatModelTests.java | 2 +- .../ai/chat/HunYuanChatModelTests.java | 111 ++++++++++++++++++ .../ai/chat/OpenAIChatModelTests.java | 2 +- .../ai/chat/XingHuoChatModelTests.java | 2 +- .../src/main/resources/application.yaml | 4 + 12 files changed, 282 insertions(+), 46 deletions(-) create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/hunyuan/HunYuanChatModel.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/HunYuanChatModelTests.java diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 1a8b374174..b74b7600ec 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -5,6 +5,7 @@ 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.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.suno.api.SunoApi; import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; @@ -64,32 +65,6 @@ public class YudaoAiAutoConfiguration { return new DeepSeekChatModel(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()) - .build(); - return new XingHuoChatModel(openAiChatModel); - } - @Bean @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { @@ -116,6 +91,64 @@ public class YudaoAiAutoConfiguration { return new DouBaoChatModel(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()) + .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()) + .build(); + return new XingHuoChatModel(openAiChatModel); + } + @Bean @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true") public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) { diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java index 8e36355e6d..563451ccc1 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java @@ -25,6 +25,12 @@ public class YudaoAiProperties { @SuppressWarnings("SpellCheckingInspection") private DouBaoProperties doubao; + /** + * 腾讯混元 + */ + @SuppressWarnings("SpellCheckingInspection") + private HunYuanProperties hunyuan; + /** * 讯飞星火 */ @@ -42,21 +48,6 @@ public class YudaoAiProperties { @SuppressWarnings("SpellCheckingInspection") private SunoProperties suno; - @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 DeepSeekProperties { @@ -83,6 +74,35 @@ public class YudaoAiProperties { } + @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 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 MidjourneyProperties { diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java index f5d8bd655a..2288eda6ef 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java @@ -20,6 +20,7 @@ public enum AiPlatformEnum { ZHI_PU("ZhiPu", "智谱"), // 智谱 AI XING_HUO("XingHuo", "星火"), // 讯飞 DOU_BAO("DouBao", "豆包"), // 字节 + HUN_YUAN("HunYuan", "混元"), // 腾讯 // ========== 国外平台 ========== diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index 8928407a52..e3f225334e 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -11,6 +11,7 @@ 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.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.suno.api.SunoApi; import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; @@ -77,6 +78,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return buildDeepSeekChatModel(apiKey); case DOU_BAO: return buildDouBaoChatModel(apiKey); + case HUN_YUAN: + return buildHunYuanChatModel(apiKey, url); case ZHI_PU: return buildZhiPuChatModel(apiKey, url); case XING_HUO: @@ -105,6 +108,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return SpringUtil.getBean(DeepSeekChatModel.class); case DOU_BAO: return SpringUtil.getBean(DouBaoChatModel.class); + case HUN_YUAN: + return SpringUtil.getBean(HunYuanChatModel.class); case ZHI_PU: return SpringUtil.getBean(ZhiPuAiChatModel.class); case XING_HUO: @@ -272,10 +277,19 @@ public class AiModelFactoryImpl implements AiModelFactory { */ private ChatModel buildDouBaoChatModel(String apiKey) { YudaoAiProperties.DouBaoProperties properties = new YudaoAiProperties.DouBaoProperties() - .setApiKey(apiKey); + .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 ZhiPuAiAutoConfiguration} 的 zhiPuAiChatModel 方法 */ diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/hunyuan/HunYuanChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/hunyuan/HunYuanChatModel.java new file mode 100644 index 0000000000..f6f598d0af --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/hunyuan/HunYuanChatModel.java @@ -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. 混元大模型:基于 知识引擎原子能力 实现 + * 2. 知识引擎原子能力:基于 知识引擎原子能力 实现 + * + * @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 stream(Prompt prompt) { + return openAiChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return openAiChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index bb9d530a31..51c300c27a 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -31,6 +31,7 @@ public class AiUtils { case OPENAI: case DEEP_SEEK: // 复用 OpenAI 客户端 case DOU_BAO: // 复用 OpenAI 客户端 + case HUN_YUAN: // 复用 OpenAI 客户端 case XING_HUO: // 复用 OpenAI 客户端 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case AZURE_OPENAI: diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DeepSeekChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DeepSeekChatModelTests.java index b3e12bfd16..bc6a367ec0 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DeepSeekChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DeepSeekChatModelTests.java @@ -23,7 +23,7 @@ import java.util.List; */ public class DeepSeekChatModelTests { - private static final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl(DeepSeekChatModel.BASE_URL) .apiKey("sk-e52047409b144d97b791a6a46a2d") // apiKey diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java index f3de2a6300..2a6ef7fd21 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java @@ -24,7 +24,7 @@ import java.util.List; */ public class DouBaoChatModelTests { - private static final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl(DouBaoChatModel.BASE_URL) .apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/HunYuanChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/HunYuanChatModelTests.java new file mode 100644 index 0000000000..886c9dc1de --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/HunYuanChatModelTests.java @@ -0,0 +1,111 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; +import cn.iocoder.yudao.framework.ai.core.model.hunyuan.HunYuanChatModel; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link DeepSeekChatModel} 集成测试 + * + * @author 芋道源码 + */ +public class HunYuanChatModelTests { + + private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(HunYuanChatModel.BASE_URL) + .apiKey("sk-bcd") // apiKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(HunYuanChatModel.MODEL_DEFAULT) // 模型 + .temperature(0.7) + .build()) + .build(); + + private final HunYuanChatModel chatModel = new HunYuanChatModel(openAiChatModel); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(System.out::println).then().block(); + } + + private final OpenAiChatModel deepSeekOpenAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(HunYuanChatModel.DEEP_SEEK_BASE_URL) + .apiKey("sk-abc") // apiKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() +// .model(HunYuanChatModel.DEEP_SEEK_MODEL_DEFAULT) // 模型("deepseek-v3") + .model("deepseek-r1") // 模型("deepseek-r1") + .temperature(0.7) + .build()) + .build(); + + private final HunYuanChatModel deepSeekChatModel = new HunYuanChatModel(deepSeekOpenAiChatModel); + + @Test + @Disabled + public void testCall_deepseek() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = deepSeekChatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + } + + @Test + @Disabled + public void testStream_deekseek() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = deepSeekChatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(System.out::println).then().block(); + } + + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java index eb52c2bfb8..735f0a9415 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java @@ -22,7 +22,7 @@ import java.util.List; */ public class OpenAIChatModelTests { - private static final OpenAiChatModel chatModel = OpenAiChatModel.builder() + private final OpenAiChatModel chatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl("https://api.holdai.top") .apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") // apiKey diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java index cbc7f6e166..791e75688e 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java @@ -23,7 +23,7 @@ import java.util.List; */ public class XingHuoChatModelTests { - private static final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl(XingHuoChatModel.BASE_URL) .apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index cac5e5a142..2093e5b50d 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -188,6 +188,10 @@ yudao: enable: true api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 model: doubao-1-5-lite-32k-250115 + hunyuan: # 腾讯混元 + enable: true + api-key: sk-abc + model: hunyuan-turbo xinghuo: # 讯飞星火 enable: true appKey: 75b161ed2aef4719b275d6e7f2a4d4cd From 117470998ea5bfc234543a6d82b837626ac44028 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 24 Feb 2025 12:41:14 +0800 Subject: [PATCH 14/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E5=A2=9E=E5=8A=A0=E6=B7=B7?= =?UTF-8?q?=E5=85=83=E3=80=81=E9=80=9A=E4=B9=89=E7=9A=84=20deepseek=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=A1=88=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/framework/ai/chat/TongYiChatModelTests.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java index aa521373c4..c632acf20f 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java @@ -24,7 +24,11 @@ public class TongYiChatModelTests { private final DashScopeChatModel chatModel = new DashScopeChatModel( new DashScopeApi("sk-7d903764249848cfa912733146da12d1"), - DashScopeChatOptions.builder().withModel("qwen1.5-72b-chat").build()); + DashScopeChatOptions.builder() + .withModel("qwen1.5-72b-chat") // 模型 +// .withModel("deepseek-r1") // 模型(deepseek-r1) +// .withModel("deepseek-v3") // 模型(deepseek-v3) + .build()); @Test @Disabled From a7e5aaec3bf155a8c451fc031dbdaa5ff4d283d2 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 24 Feb 2025 21:19:44 +0800 Subject: [PATCH 15/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E8=BD=A8=E8=BF=B9=E6=B5=81?= =?UTF-8?q?=E5=8A=A8=E7=9A=84=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .image/common/ai-feature.png | Bin 21239 -> 21387 bytes .../ai/config/YudaoAiAutoConfiguration.java | 27 +++++++ .../ai/config/YudaoAiProperties.java | 19 +++++ .../ai/core/enums/AiPlatformEnum.java | 1 + .../ai/core/factory/AiModelFactoryImpl.java | 14 ++++ .../siliconflow/SiliconFlowChatModel.java | 47 ++++++++++++ .../yudao/framework/ai/core/util/AiUtils.java | 1 + .../ai/chat/DouBaoChatModelTests.java | 3 +- .../ai/chat/HunYuanChatModelTests.java | 3 +- .../ai/chat/SiliconFlowChatModelTests.java | 69 ++++++++++++++++++ .../src/main/resources/application.yaml | 4 + 11 files changed, 184 insertions(+), 4 deletions(-) create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/siliconflow/SiliconFlowChatModel.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/SiliconFlowChatModelTests.java diff --git a/.image/common/ai-feature.png b/.image/common/ai-feature.png index b4a55f547c85650d1b34e0bf0852a40634ecc99f..fadc1cc5d660ef1e3a03fcdb472cba4187ff33b3 100644 GIT binary patch literal 21387 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 z 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 mKhyoTgm1@ZDcM_`%s-~;|KNUAPI 文档 + * + * @author fansili + */ +@Slf4j +@RequiredArgsConstructor +public class SiliconFlowChatModel implements ChatModel { + + public static final String BASE_URL = "https://api.siliconflow.cn"; + + public static final String MODEL_DEFAULT = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"; + + /** + * 兼容 OpenAI 接口,进行复用 + */ + private final OpenAiChatModel openAiChatModel; + + @Override + public ChatResponse call(Prompt prompt) { + return openAiChatModel.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return openAiChatModel.stream(prompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return openAiChatModel.getDefaultOptions(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index 51c300c27a..b1a3a3b507 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -33,6 +33,7 @@ public class AiUtils { case DOU_BAO: // 复用 OpenAI 客户端 case HUN_YUAN: // 复用 OpenAI 客户端 case XING_HUO: // 复用 OpenAI 客户端 + case SILICON_FLOW: // 复用 OpenAI 客户端 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case AZURE_OPENAI: // TODO 芋艿:貌似没 model 字段???! diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java index 2a6ef7fd21..fc5dc3a274 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DouBaoChatModelTests.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.framework.ai.chat; -import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; import cn.iocoder.yudao.framework.ai.core.model.doubao.DouBaoChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -18,7 +17,7 @@ import java.util.ArrayList; import java.util.List; /** - * {@link DeepSeekChatModel} 集成测试 + * {@link DouBaoChatModel} 集成测试 * * @author 芋道源码 */ diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/HunYuanChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/HunYuanChatModelTests.java index 886c9dc1de..e083e6be2d 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/HunYuanChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/HunYuanChatModelTests.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.framework.ai.chat; -import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; import cn.iocoder.yudao.framework.ai.core.model.hunyuan.HunYuanChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -18,7 +17,7 @@ import java.util.ArrayList; import java.util.List; /** - * {@link DeepSeekChatModel} 集成测试 + * {@link HunYuanChatModel} 集成测试 * * @author 芋道源码 */ diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/SiliconFlowChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/SiliconFlowChatModelTests.java new file mode 100644 index 0000000000..880795fe96 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/SiliconFlowChatModelTests.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import cn.iocoder.yudao.framework.ai.core.model.siliconflow.SiliconFlowChatModel; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link SiliconFlowChatModel} 集成测试 + * + * @author 芋道源码 + */ +public class SiliconFlowChatModelTests { + + private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl(SiliconFlowChatModel.BASE_URL) + .apiKey("sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz") // apiKey + .build()) + .defaultOptions(OpenAiChatOptions.builder() + .model(SiliconFlowChatModel.MODEL_DEFAULT) // 模型 +// .model("deepseek-ai/DeepSeek-R1") // 模型(deepseek-ai/DeepSeek-R1)可用赠费 +// .model("Pro/deepseek-ai/DeepSeek-R1") // 模型(Pro/deepseek-ai/DeepSeek-R1)需要付费 + .temperature(0.7) + .build()) + .build(); + + private final SiliconFlowChatModel chatModel = new SiliconFlowChatModel(openAiChatModel); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(System.out::println).then().block(); + } + +} diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 2093e5b50d..b3792c4c16 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -192,6 +192,10 @@ yudao: enable: true api-key: sk-abc model: hunyuan-turbo + siliconflow: # 硅基流动 + enable: true + api-key: sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz + model: deepseek-ai/DeepSeek-R1-Distill-Qwen-7B xinghuo: # 讯飞星火 enable: true appKey: 75b161ed2aef4719b275d6e7f2a4d4cd From deca69ada661636e7a724f77c3c72f745433f722 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 24 Feb 2025 21:28:15 +0800 Subject: [PATCH 16/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91AI=EF=BC=9ADispatcherType.ASYNC=20=E6=97=B6?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E8=BF=9B=E8=A1=8C=E8=AE=A4=E8=AF=81=EF=BC=8C?= =?UTF-8?q?=E8=A7=A3=E5=86=B3=20Spring=20AI=20SSE=20=E7=9A=84=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/YunaiV/yudao-cloud/issues/197 --- .../security/config/YudaoWebSecurityConfigurerAdapter.java | 5 ++++- .../ai/controller/admin/chat/AiChatMessageController.java | 2 -- .../ai/controller/admin/mindmap/AiMindMapController.java | 2 -- .../module/ai/controller/admin/write/AiWriteController.java | 2 -- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java index 68ae1b30c5..8c2d93b22f 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java @@ -7,6 +7,7 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import jakarta.annotation.Resource; import jakarta.annotation.security.PermitAll; +import jakarta.servlet.DispatcherType; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.context.ApplicationContext; @@ -142,7 +143,9 @@ public class YudaoWebSecurityConfigurerAdapter { // ②:每个项目的自定义规则 .authorizeHttpRequests(c -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(c))) // ③:兜底规则,必须认证 - .authorizeHttpRequests(c -> c.anyRequest().authenticated()); + .authorizeHttpRequests(c -> c + .dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() // WebFlux 异步请求,无需认证,目的:SSE 场景 + .anyRequest().authenticated()); // 添加 Token Filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java index 357dbec5ed..43ae9a40db 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java @@ -20,7 +20,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; -import jakarta.annotation.security.PermitAll; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; @@ -57,7 +56,6 @@ public class AiChatMessageController { @Operation(summary = "发送消息(流式)", description = "流式返回,响应较快") @PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题 public Flux> sendChatMessageStream(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) { return chatMessageService.sendChatMessageStream(sendReqVO, getLoginUserId()); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java index f1c59b964c..db015e5149 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java @@ -12,7 +12,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; -import jakarta.annotation.security.PermitAll; import jakarta.validation.Valid; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; @@ -32,7 +31,6 @@ public class AiMindMapController { @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @Operation(summary = "导图生成(流式)", description = "流式返回,响应较快") - @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题 public Flux> generateMindMap(@RequestBody @Valid AiMindMapGenerateReqVO generateReqVO) { return mindMapService.generateMindMap(generateReqVO, getLoginUserId()); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java index d27204d21e..7ef208b9aa 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java @@ -12,7 +12,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; -import jakarta.annotation.security.PermitAll; import jakarta.validation.Valid; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; @@ -32,7 +31,6 @@ public class AiWriteController { @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @Operation(summary = "写作生成(流式)", description = "流式返回,响应较快") - @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题 public Flux> generateWriteContent(@RequestBody @Valid AiWriteGenerateReqVO generateReqVO) { return writeService.generateWriteContent(generateReqVO, getLoginUserId()); } From 0a8c75625a2501a21efcbea14a79784a5b03a10f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 28 Feb 2025 07:43:43 +0800 Subject: [PATCH 17/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E3=80=91AI=EF=BC=9A=E7=9F=A5=E8=AF=86=E5=BA=93?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E7=9A=84=E8=A1=A8=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/enums/ErrorCodeConstants.java | 11 +- .../AiKnowledgeDocumentStatusEnum.java | 39 ----- .../knowledge/AiKnowledgeController.http | 33 ++++ .../knowledge/AiKnowledgeController.java | 16 +- .../AiKnowledgeDocumentController.http | 12 ++ .../AiKnowledgeDocumentController.java | 17 +- .../AiKnowledgeDocumentUpdateReqVO.java | 7 +- .../AiKnowledgeDocumentCreateReqVO.java | 22 +-- ...teReqVO.java => AiKnowledgeSaveReqVO.java} | 28 ++-- .../vo/knowledge/AiKnowledgeUpdateReqVO.java | 32 ---- .../dataobject/chat/AiChatConversationDO.java | 2 + .../dal/dataobject/chat/AiChatMessageDO.java | 2 + .../ai/dal/dataobject/image/AiImageDO.java | 1 + .../dataobject/knowledge/AiKnowledgeDO.java | 28 +--- .../knowledge/AiKnowledgeDocumentDO.java | 44 +---- .../knowledge/AiKnowledgeSegmentDO.java | 19 ++- .../dal/dataobject/mindmap/AiMindMapDO.java | 1 + .../dal/dataobject/model/AiChatModelDO.java | 1 + .../ai/dal/dataobject/music/AiMusicDO.java | 1 + .../ai/dal/dataobject/write/AiWriteDO.java | 1 + .../mysql/knowledge/AiKnowledgeMapper.java | 6 +- .../knowledge/AiKnowledgeDocumentService.java | 10 +- .../AiKnowledgeDocumentServiceImpl.java | 79 ++++----- .../knowledge/AiKnowledgeSegmentService.java | 8 + .../AiKnowledgeSegmentServiceImpl.java | 155 +++++++++++++----- .../service/knowledge/AiKnowledgeService.java | 21 +-- .../knowledge/AiKnowledgeServiceImpl.java | 45 ++--- .../ai/service/model/AiApiKeyService.java | 13 +- .../ai/service/model/AiApiKeyServiceImpl.java | 25 ++- .../ai/core/factory/AiModelFactory.java | 10 +- .../ai/core/factory/AiModelFactoryImpl.java | 32 +++- 31 files changed, 356 insertions(+), 365 deletions(-) delete mode 100644 yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/knowledge/AiKnowledgeDocumentStatusEnum.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/{AiKnowledgeCreateReqVO.java => AiKnowledgeSaveReqVO.java} (69%) delete mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateReqVO.java diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index e1dd1a9568..48913e91fa 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -21,7 +21,7 @@ public interface ErrorCodeConstants { ErrorCode CHAT_MODEL_DISABLE = new ErrorCode(1_040_001_001, "模型({})已禁用!"); ErrorCode CHAT_MODEL_DEFAULT_NOT_EXISTS = new ErrorCode(1_040_001_002, "操作失败,找不到默认聊天模型"); - // ========== API 聊天模型 1-040-002-000 ========== + // ========== API 聊天角色 1-040-002-000 ========== ErrorCode CHAT_ROLE_NOT_EXISTS = new ErrorCode(1_040_002_000, "聊天角色不存在"); ErrorCode CHAT_ROLE_DISABLE = new ErrorCode(1_040_001_001, "聊天角色({})已禁用!"); @@ -40,7 +40,6 @@ public interface ErrorCodeConstants { ErrorCode IMAGE_NOT_EXISTS = new ErrorCode(1_022_005_000, "图片不存在!"); ErrorCode IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_022_005_001, "Midjourney 提交失败!原因:{}"); ErrorCode IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_022_005_002, "Midjourney 按钮 customId 不存在! {}"); - ErrorCode IMAGE_FAIL = new ErrorCode(1_022_005_002, "图片绘画失败! {}"); // ========== API 音乐 1-040-006-000 ========== ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!"); @@ -54,7 +53,11 @@ public interface ErrorCodeConstants { // ========== API 知识库 1-022-008-000 ========== ErrorCode KNOWLEDGE_NOT_EXISTS = new ErrorCode(1_022_008_000, "知识库不存在!"); - ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_022_008_001, "文档不存在!"); - ErrorCode KNOWLEDGE_SEGMENT_NOT_EXISTS = new ErrorCode(1_022_008_002, "段落不存在!"); + + ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_022_008_101, "文档不存在!"); + ErrorCode KNOWLEDGE_DOCUMENT_FILE_EMPTY = new ErrorCode(1_022_008_102, "文档内容为空!"); + ErrorCode KNOWLEDGE_DOCUMENT_FILE_READ_FAIL = new ErrorCode(1_022_008_102, "文档加载失败!"); + + ErrorCode KNOWLEDGE_SEGMENT_NOT_EXISTS = new ErrorCode(1_022_008_202, "段落不存在!"); } diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/knowledge/AiKnowledgeDocumentStatusEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/knowledge/AiKnowledgeDocumentStatusEnum.java deleted file mode 100644 index 6ded3f6de8..0000000000 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/knowledge/AiKnowledgeDocumentStatusEnum.java +++ /dev/null @@ -1,39 +0,0 @@ -package cn.iocoder.yudao.module.ai.enums.knowledge; - -import cn.iocoder.yudao.framework.common.core.ArrayValuable; -import lombok.AllArgsConstructor; -import lombok.Getter; - -import java.util.Arrays; - -/** - * AI 知识库-文档状态的枚举 - * - * @author xiaoxin - */ -@AllArgsConstructor -@Getter -public enum AiKnowledgeDocumentStatusEnum implements ArrayValuable { - - IN_PROGRESS(10, "索引中"), - SUCCESS(20, "可用"), - FAIL(30, "失败"); - - /** - * 状态 - */ - private final Integer status; - - /** - * 状态名 - */ - private final String name; - - public static final Integer[] ARRAYS = Arrays.stream(values()).map(AiKnowledgeDocumentStatusEnum::getStatus).toArray(Integer[]::new); - - @Override - public Integer[] array() { - return ARRAYS; - } - -} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http new file mode 100644 index 0000000000..235e880343 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http @@ -0,0 +1,33 @@ +### 创建知识库 +POST {{baseUrl}}/ai/knowledge/create +Content-Type: application/json +Authorization: {{token}} +tenant-id: {{adminTenantId}} + +{ + "name": "测试标题", + "description": "测试描述", + "embeddingModelId": 30, + "topK": 3, + "similarityThreshold": 0.5 +} + +### 更新知识库 +PUT {{baseUrl}}/ai/knowledge/update +Content-Type: application/json +Authorization: {{token}} +tenant-id: {{adminTenantId}} + +{ + "id": 1, + "name": "测试标题(更新)", + "description": "测试描述", + "embeddingModelId": 30, + "topK": 5, + "similarityThreshold": 0.6 +} + +### 获取知识库分页 +GET {{baseUrl}}/ai/knowledge/page?pageNo=1&pageSize=10 +Authorization: {{token}} +tenant-id: {{adminTenantId}} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java index 3ffea5e802..5c27810260 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java @@ -3,10 +3,9 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeRespVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService; import io.swagger.v3.oas.annotations.Operation; @@ -17,7 +16,6 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - AI 知识库") @RestController @@ -31,20 +29,22 @@ public class AiKnowledgeController { @GetMapping("/page") @Operation(summary = "获取知识库分页") public CommonResult> getKnowledgePage(@Valid AiKnowledgePageReqVO pageReqVO) { - PageResult pageResult = knowledgeService.getKnowledgePage(getLoginUserId(), pageReqVO); + + PageResult pageResult = knowledgeService.getKnowledgePage(pageReqVO); return success(BeanUtils.toBean(pageResult, AiKnowledgeRespVO.class)); } @PostMapping("/create") @Operation(summary = "创建知识库") - public CommonResult createKnowledge(@RequestBody @Valid AiKnowledgeCreateReqVO createReqVO) { - return success(knowledgeService.createKnowledge(createReqVO, getLoginUserId())); + public CommonResult createKnowledge(@RequestBody @Valid AiKnowledgeSaveReqVO createReqVO) { + return success(knowledgeService.createKnowledge(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新知识库") - public CommonResult updateKnowledge(@RequestBody @Valid AiKnowledgeUpdateReqVO updateReqVO) { - knowledgeService.updateKnowledge(updateReqVO, getLoginUserId()); + public CommonResult updateKnowledge(@RequestBody @Valid AiKnowledgeSaveReqVO updateReqVO) { + knowledgeService.updateKnowledge(updateReqVO); return success(true); } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http new file mode 100644 index 0000000000..7af4780c3c --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http @@ -0,0 +1,12 @@ +### 创建知识文档 +POST {{baseUrl}}/ai/knowledge/document/create +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +{ + "knowledgeId": 1, + "name": "测试文档", + "url": "https://static.iocoder.cn/README.md", + "segmentMaxTokens": 800 +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java index 75c4d805b6..cd6feb3056 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java @@ -27,20 +27,21 @@ public class AiKnowledgeDocumentController { @Resource private AiKnowledgeDocumentService documentService; - @PostMapping("/create") - @Operation(summary = "新建文档") - public CommonResult createKnowledgeDocument(@Valid AiKnowledgeDocumentCreateReqVO reqVO) { - Long knowledgeDocumentId = documentService.createKnowledgeDocument(reqVO); - return success(knowledgeDocumentId); - } - @GetMapping("/page") @Operation(summary = "获取文档分页") - public CommonResult> getKnowledgeDocumentPage(@Valid AiKnowledgeDocumentPageReqVO pageReqVO) { + public CommonResult> getKnowledgeDocumentPage( + @Valid AiKnowledgeDocumentPageReqVO pageReqVO) { PageResult pageResult = documentService.getKnowledgeDocumentPage(pageReqVO); return success(BeanUtils.toBean(pageResult, AiKnowledgeDocumentRespVO.class)); } + @PostMapping("/create") + @Operation(summary = "新建文档") + public CommonResult createKnowledgeDocument(@RequestBody @Valid AiKnowledgeDocumentCreateReqVO reqVO) { + Long knowledgeDocumentId = documentService.createKnowledgeDocument(reqVO); + return success(knowledgeDocumentId); + } + @PutMapping("/update") @Operation(summary = "更新文档") public CommonResult updateKnowledgeDocument(@Valid @RequestBody AiKnowledgeDocumentUpdateReqVO reqVO) { diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java index 2cc6a32f3c..13f871f6d4 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java @@ -11,16 +11,15 @@ import lombok.Data; @Data public class AiKnowledgeDocumentUpdateReqVO { - @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583") @NotNull(message = "编号不能为空") private Long id; + @Schema(description = "名称", example = "Java 开发手册") + private String name; + @Schema(description = "是否启用", example = "1") @InEnum(CommonStatusEnum.class) private Integer status; - @Schema(description = "名称", example = "Java 开发手册") - private String name; - } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java index df6b6821d8..1d2e49307a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateReqVO.java @@ -23,24 +23,8 @@ public class AiKnowledgeDocumentCreateReqVO { @URL(message = "文档 URL 格式不正确") private String url; - @Schema(description = "每个段落的目标 token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "800") - @NotNull(message = "每个段落的目标 token 数不能为空") - private Integer defaultSegmentTokens; - - @Schema(description = "每个段落的最小字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "350") - @NotNull(message = "每个段落的最小字符数不能为空") - private Integer minSegmentWordCount; - - @Schema(description = "丢弃阈值:低于此阈值的段落会被丢弃", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") - @NotNull(message = "丢弃阈值不能为空") - private Integer minChunkLengthToEmbed; - - @Schema(description = "最大段落数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000") - @NotNull(message = "最大段落数不能为空") - private Integer maxNumSegments; - - @Schema(description = "分块是否保留分隔符", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") - @NotNull(message = "分块是否保留分隔符不能为空") - private Boolean keepSeparator; + @Schema(description = "分段的最大 Token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "800") + @NotNull(message = "分段的最大 Token 数不能为空") + private Integer segmentMaxTokens; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeSaveReqVO.java similarity index 69% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateReqVO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeSaveReqVO.java index 00843665c4..f7c6a245ca 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeCreateReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeSaveReqVO.java @@ -5,11 +5,12 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; -import java.util.List; - -@Schema(description = "管理后台 - AI 知识库创建 Request VO") +@Schema(description = "管理后台 - AI 知识库新增/修改 Request VO") @Data -public class AiKnowledgeCreateReqVO { +public class AiKnowledgeSaveReqVO { + + @Schema(description = "对话编号", example = "1204") + private Long id; @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro 用户指南") @NotBlank(message = "知识库名称不能为空") @@ -18,19 +19,18 @@ public class AiKnowledgeCreateReqVO { @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "存储 ruoyi-vue-pro 操作文档") private String description; - @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2,3]") - private List visibilityPermissions; - - @Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "嵌入模型不能为空") - private Long modelId; - - @Schema(description = "相似性阈值", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.5") - @NotNull(message = "相似性阈值不能为空") - private Double similarityThreshold; + @Schema(description = "向量模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "向量模型不能为空") + private Long embeddingModelId; @Schema(description = "topK", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") @NotNull(message = "topK 不能为空") private Integer topK; + @Schema(description = "相似性阈值", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.5") + @NotNull(message = "相似性阈值不能为空") + private Double similarityThreshold; + + // TODO @芋艿:status + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateReqVO.java deleted file mode 100644 index ba98bf0c72..0000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeUpdateReqVO.java +++ /dev/null @@ -1,32 +0,0 @@ -package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import lombok.Data; - -import java.util.List; - -@Schema(description = "管理后台 - AI 知识库更新【我的】 Request VO") -@Data -public class AiKnowledgeUpdateReqVO { - - @Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204") - @NotNull(message = "知识库编号不能为空") - private Long id; - - @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "") - @NotBlank(message = "知识库名称不能为空") - private String name; - - @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "") - private String description; - - @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") - private List visibilityPermissions; - - @Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotNull(message = "嵌入模型不能为空") - private Long modelId; - -} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java index 7d9625f58f..cc8ae7d42a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java @@ -80,6 +80,8 @@ public class AiChatConversationDO extends BaseDO { private Long modelId; /** * 模型标志 + * + * 冗余 {@link AiChatModelDO#getModel()} 字段 */ private String model; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java index ecd10609f5..c65360ccbb 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java @@ -82,6 +82,8 @@ public class AiChatMessageDO extends BaseDO { /** * 模型标志 + * + * 冗余 {@link AiChatModelDO#getModel()} */ private String model; /** diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java index 56749a1d00..5b43bc91e9 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java @@ -52,6 +52,7 @@ public class AiImageDO extends BaseDO { * 枚举 {@link cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum} */ private String platform; + // TODO @芋艿:modelId? /** * 模型 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java index 638a8ba50b..732d54e24a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java @@ -2,15 +2,12 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import com.baomidou.mybatisplus.annotation.KeySequence; -import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; -import java.util.List; - /** * AI 知识库 DO * @@ -26,12 +23,6 @@ public class AiKnowledgeDO extends BaseDO { */ @TableId private Long id; - /** - * 用户编号 - *

- * 关联 AdminUserDO 的 userId 字段 - */ - private Long userId; /** * 知识库名称 */ @@ -42,20 +33,17 @@ public class AiKnowledgeDO extends BaseDO { private String description; /** - * 可见权限,选择哪些人可见 - *

- * -1 所有人可见,其他为各自用户编号 + * 向量模型编号 + * + * 关联 {@link AiChatModelDO#getId()} */ - @TableField(typeHandler = LongListTypeHandler.class) - private List visibilityPermissions; - /** - * 嵌入模型编号 - */ - private Long modelId; + private Long embeddingModelId; /** * 模型标识 + * + * 冗余 {@link AiChatModelDO#getModel()} */ - private String model; + private String embeddingModel; /** * topK diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java index ee8bfd5aab..56de374f7e 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -33,54 +32,27 @@ public class AiKnowledgeDocumentDO extends BaseDO { * 文件名称 */ private String name; + /** + * 文件 URL + */ + private String url; /** * 内容 */ private String content; /** - * 文件 URL + * 文档长度 */ - private String url; + private Integer contentLength; + /** * 文档 token 数量 */ private Integer tokens; - /** - * 文档字符数 - */ - private Integer wordCount; - - - // ========== 自定义分段所用参数 ========== - // TODO @新:3)defaultChunkSize、defaultChunkSize、minChunkSizeChars、maxNumChunks 这几个字段的命名,可能要微信一起讨论下。尽量命名保持风格统一哈。 /** * 每个文本块的目标 token 数 */ - private Integer defaultSegmentTokens; - /** - * 每个文本块的最小字符数 - */ - private Integer minSegmentWordCount; - /** - * 低于此值的块会被丢弃 - */ - private Integer minChunkLengthToEmbed; - /** - * 最大块数 - */ - private Integer maxNumSegments; - /** - * 分块是否保留分隔符 - */ - private Boolean keepSeparator; - // =================================== - - /** - * 切片状态 - *

- * 枚举 {@link AiKnowledgeDocumentStatusEnum} - */ - private Integer sliceStatus; + private Integer segmentMaxTokens; /** * 状态 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java index b08e960d14..5ab18e7d7a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java @@ -17,17 +17,16 @@ import lombok.Data; @Data public class AiKnowledgeSegmentDO extends BaseDO { - public static final String FIELD_KNOWLEDGE_ID = "knowledgeId"; + /** + * 向量库的编号 - 空值 + */ + public static final String VECTOR_ID_EMPTY = ""; /** * 编号 */ @TableId private Long id; - /** - * 向量库的编号 - */ - private String vectorId; /** * 知识库编号 *

@@ -45,13 +44,19 @@ public class AiKnowledgeSegmentDO extends BaseDO { */ private String content; /** - * 字符数 + * 切片内容长度 */ - private Integer wordCount; + private Integer contentLength; + + /** + * 向量库的编号 + */ + private String vectorId; /** * token 数量 */ private Integer tokens; + /** * 状态 *

diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java index b9768529f1..47aa0ce4a5 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java @@ -36,6 +36,7 @@ public class AiMindMapDO extends BaseDO { * 枚举 {@link AiPlatformEnum} */ private String platform; + // TODO @芋艿:modelId? /** * 模型 */ diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatModelDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatModelDO.java index 7197f8b58f..c3800bd4ca 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatModelDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatModelDO.java @@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; +// TODO @芋艿,需要改造,增加 type /** * AI 聊天模型 DO * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java index e03d62c162..bfa7394ddd 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java @@ -84,6 +84,7 @@ public class AiMusicDO extends BaseDO { * 枚举 {@link AiPlatformEnum} */ private String platform; + // TODO @芋艿:modelId? /** * 模型 */ diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java index 0d6f9c5e64..a99f94e82d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java @@ -44,6 +44,7 @@ public class AiWriteDO extends BaseDO { * 枚举 {@link AiPlatformEnum} */ private String platform; + // TODO @芋艿:modelId? /** * 模型 */ diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java index f07a9a2afa..735332beb9 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java @@ -16,11 +16,9 @@ import org.apache.ibatis.annotations.Mapper; @Mapper public interface AiKnowledgeMapper extends BaseMapperX { - default PageResult selectPage(Long userId, AiKnowledgePageReqVO pageReqVO) { + default PageResult selectPage(AiKnowledgePageReqVO pageReqVO) { return selectPage(pageReqVO, new LambdaQueryWrapperX() .eq(AiKnowledgeDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) - .likeIfPresent(AiKnowledgeDO::getName, pageReqVO.getName()) - .and(e -> e.apply("FIND_IN_SET(" + userId + ",visibility_permissions)").or(m -> m.apply("FIND_IN_SET(-1,visibility_permissions)"))) - .orderByDesc(AiKnowledgeDO::getId)); + .likeIfPresent(AiKnowledgeDO::getName, pageReqVO.getName())); } } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java index 3de0ac01de..9cb7c5a827 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -21,7 +21,6 @@ public interface AiKnowledgeDocumentService { */ Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO); - /** * 获取文档分页 * @@ -36,4 +35,13 @@ public interface AiKnowledgeDocumentService { * @param reqVO 更新信息 */ void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO); + + /** + * 校验文档是否存在 + * + * @param id 文档编号 + * @return 文档信息 + */ + AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index f5855db7d9..0f48d5a992 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -1,26 +1,22 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper; -import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper; -import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.document.Document; import org.springframework.ai.reader.tika.TikaDocumentReader; import org.springframework.ai.tokenizer.TokenCountEstimator; -import org.springframework.ai.transformer.splitter.TokenTextSplitter; -import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.context.annotation.Lazy; import org.springframework.core.io.ByteArrayResource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,7 +24,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_DOCUMENT_NOT_EXISTS; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; /** * AI 知识库文档 Service 实现类 @@ -40,58 +36,46 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_DOCU public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentService { @Resource - private AiKnowledgeDocumentMapper documentMapper; - @Resource - private AiKnowledgeSegmentMapper segmentMapper; + private AiKnowledgeDocumentMapper knowledgeDocumentMapper; @Resource private TokenCountEstimator tokenCountEstimator; + @Resource + private AiKnowledgeSegmentService knowledgeSegmentService; + @Resource + @Lazy // 延迟加载,避免循环依赖 private AiKnowledgeService knowledgeService; @Override @Transactional(rollbackFor = Exception.class) public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) { - // 0. 校验并获取向量存储实例 - VectorStore vectorStore = knowledgeService.getVectorStoreById(createReqVO.getKnowledgeId()); + // 1. 校验参数 + knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId()); - // 1.1 下载文档 + // 2. 下载文档 TikaDocumentReader loader = new TikaDocumentReader(downloadFile(createReqVO.getUrl())); List documents = loader.get(); Document document = CollUtil.getFirst(documents); - // 1.2 文档记录入库 - String content = document.getText(); - AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class) - .setTokens(tokenCountEstimator.estimate(content)).setWordCount(content.length()) - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setSliceStatus(AiKnowledgeDocumentStatusEnum.SUCCESS.getStatus()); - documentMapper.insert(documentDO); - Long documentId = documentDO.getId(); - if (CollUtil.isEmpty(documents)) { - return documentId; + if (document == null || StrUtil.isEmpty(document.getText())) { + throw exception(KNOWLEDGE_DOCUMENT_FILE_READ_FAIL); } - // 2 构造文本分段器 - TokenTextSplitter tokenTextSplitter = new TokenTextSplitter(createReqVO.getDefaultSegmentTokens(), createReqVO.getMinSegmentWordCount(), createReqVO.getMinChunkLengthToEmbed(), - createReqVO.getMaxNumSegments(), createReqVO.getKeepSeparator()); - // 2.1 文档分段 - List segments = tokenTextSplitter.apply(documents); - // 2.2 分段内容入库 - List segmentDOList = CollectionUtils.convertList(segments, - segment -> new AiKnowledgeSegmentDO().setContent(segment.getText()).setDocumentId(documentId) - .setKnowledgeId(createReqVO.getKnowledgeId()).setVectorId(segment.getId()) - .setTokens(tokenCountEstimator.estimate(segment.getText())).setWordCount(segment.getText().length()) - .setStatus(CommonStatusEnum.ENABLE.getStatus())); - segmentMapper.insertBatch(segmentDOList); + // 3. 文档记录入库 + String content = document.getText(); + AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class) + .setContent(content).setContentLength(content.length()).setTokens(tokenCountEstimator.estimate(content)) + .setStatus(CommonStatusEnum.ENABLE.getStatus()); + knowledgeDocumentMapper.insert(documentDO); - // 3. 向量化并存储 - segments.forEach(segment -> segment.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, createReqVO.getKnowledgeId())); - vectorStore.add(segments); - return documentId; + // 4. 文档切片入库 + knowledgeSegmentService.createKnowledgeSegmentBySplitContent(documentDO.getId(), document.getText()); + return documentDO.getId(); } @Override public PageResult getKnowledgeDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO) { - return documentMapper.selectPage(pageReqVO); + return knowledgeDocumentMapper.selectPage(pageReqVO); } @Override @@ -100,17 +84,13 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic validateKnowledgeDocumentExists(reqVO.getId()); // 2. 更新文档 AiKnowledgeDocumentDO document = BeanUtils.toBean(reqVO, AiKnowledgeDocumentDO.class); - documentMapper.updateById(document); + knowledgeDocumentMapper.updateById(document); + // TODO @芋艿:这里要处理状态的变更 } - /** - * 校验文档是否存在 - * - * @param id 文档编号 - * @return 文档信息 - */ - private AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id) { - AiKnowledgeDocumentDO knowledgeDocument = documentMapper.selectById(id); + @Override + public AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id) { + AiKnowledgeDocumentDO knowledgeDocument = knowledgeDocumentMapper.selectById(id); if (knowledgeDocument == null) { throw exception(KNOWLEDGE_DOCUMENT_NOT_EXISTS); } @@ -120,6 +100,9 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic private org.springframework.core.io.Resource downloadFile(String url) { try { byte[] bytes = HttpUtil.downloadBytes(url); + if (bytes.length == 0) { + throw exception(KNOWLEDGE_DOCUMENT_FILE_EMPTY); + } return new ByteArrayResource(bytes); } catch (Exception e) { log.error("[downloadFile][url({}) 下载失败]", url, e); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java index 91bffc2761..2a8d9afd87 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java @@ -24,6 +24,14 @@ public interface AiKnowledgeSegmentService { */ PageResult getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO); + /** + * 基于 content 内容,切片创建多个段落 + * + * @param documentId 知识库文档编号 + * @param content 文档内容 + */ + void createKnowledgeSegmentBySplitContent(Long documentId, String content); + /** * 更新段落的内容 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index afb9d755f0..9b15135c95 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -10,23 +11,28 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowle import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper; import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; -import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.document.Document; +import org.springframework.ai.tokenizer.TokenCountEstimator; +import org.springframework.ai.transformer.splitter.TextSplitter; +import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; +import java.util.Collections; import java.util.List; import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS; /** @@ -38,85 +44,138 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGM @Slf4j public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService { + public static final String VECTOR_STORE_METADATA_KNOWLEDGE_ID = "knowledgeId"; + public static final String VECTOR_STORE_METADATA_DOCUMENT_ID = "documentId"; + public static final String VECTOR_STORE_METADATA_SEGMENT_ID = "segmentId"; + @Resource private AiKnowledgeSegmentMapper segmentMapper; @Resource private AiKnowledgeService knowledgeService; @Resource - private AiChatModelService chatModelService; + @Lazy // 延迟加载,避免循环依赖 + private AiKnowledgeDocumentService knowledgeDocumentService; @Resource private AiApiKeyService apiKeyService; + @Resource + private TokenCountEstimator tokenCountEstimator; + @Override public PageResult getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) { return segmentMapper.selectPage(pageReqVO); } + @Override + public void createKnowledgeSegmentBySplitContent(Long documentId, String content) { + // 1. 校验 + AiKnowledgeDocumentDO documentDO = knowledgeDocumentService.validateKnowledgeDocumentExists(documentId); + AiKnowledgeDO knowledgeDO = knowledgeService.validateKnowledgeExists(documentDO.getKnowledgeId()); + VectorStore vectorStore = getVectorStoreById(knowledgeDO); + + // 2. 文档切片 + Document document = new Document(content); + TextSplitter textSplitter = buildTokenTextSplitter(documentDO.getSegmentMaxTokens()); + List documentSegments = textSplitter.apply(Collections.singletonList(document)); + + // 3.1 存储切片 + List segmentDOs = convertList(documentSegments, segment -> { + if (StrUtil.isEmpty(segment.getText())) { + return null; + } + return new AiKnowledgeSegmentDO().setKnowledgeId(documentDO.getKnowledgeId()).setDocumentId(documentId) + .setContent(segment.getText()).setContentLength(segment.getText().length()) + .setVectorId(AiKnowledgeSegmentDO.VECTOR_ID_EMPTY).setTokens(tokenCountEstimator.estimate(segment.getText())) + .setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + segmentMapper.insertBatch(segmentDOs); + // 3.2 切片向量化 + for (int i = 0; i < documentSegments.size(); i++) { + Document segment = documentSegments.get(i); + AiKnowledgeSegmentDO segmentDO = segmentDOs.get(i); + writeVectorStore(vectorStore, segmentDO, segment); + } + } + @Override public void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO) { // 1. 校验 - AiKnowledgeSegmentDO oldKnowledgeSegment = validateKnowledgeSegmentExists(reqVO.getId()); + AiKnowledgeSegmentDO segment = validateKnowledgeSegmentExists(reqVO.getId()); - // 2.1 获取知识库向量实例 - VectorStore vectorStore = knowledgeService.getVectorStoreById(oldKnowledgeSegment.getKnowledgeId()); - // 2.2 删除原向量 - vectorStore.delete(List.of(oldKnowledgeSegment.getVectorId())); - // 2.3 重新向量化 - Document document = new Document(reqVO.getContent()); - document.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, oldKnowledgeSegment.getKnowledgeId()); - vectorStore.add(List.of(document)); + // 2. 删除向量 + VectorStore vectorStore = getVectorStoreById(segment.getKnowledgeId()); + deleteVectorStore(vectorStore, segment); - // 3. 更新段落内容 - AiKnowledgeSegmentDO knowledgeSegment = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class); - knowledgeSegment.setVectorId(document.getId()); - segmentMapper.updateById(knowledgeSegment); + // 3.1 更新切片 + AiKnowledgeSegmentDO segmentDO = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class); + segmentMapper.updateById(segmentDO); + // 3.2 重新向量化 + writeVectorStore(vectorStore, segmentDO, new Document(segmentDO.getContent())); } @Override public void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO) { - // 0 校验 - AiKnowledgeSegmentDO oldKnowledgeSegment = validateKnowledgeSegmentExists(reqVO.getId()); - // 1 获取知识库向量实例 - VectorStore vectorStore = knowledgeService.getVectorStoreById(oldKnowledgeSegment.getKnowledgeId()); - AiKnowledgeSegmentDO knowledgeSegment = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class); + // 1. 校验 + AiKnowledgeSegmentDO segment = validateKnowledgeSegmentExists(reqVO.getId()); + // 2. 获取知识库向量实例 + VectorStore vectorStore = getVectorStoreById(segment.getKnowledgeId()); + + // 3. 更新状态 + segmentMapper.updateById(new AiKnowledgeSegmentDO().setId(reqVO.getId()).setStatus(reqVO.getStatus())); + + // 4. 更新向量 if (Objects.equals(reqVO.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { - // 2.1 启用重新向量化 - Document document = new Document(oldKnowledgeSegment.getContent()); - document.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, oldKnowledgeSegment.getKnowledgeId()); - vectorStore.add(List.of(document)); - knowledgeSegment.setVectorId(document.getId()); + writeVectorStore(vectorStore, segment, new Document(segment.getContent())); } else { - // 2.2 禁用删除向量 - vectorStore.delete(List.of(oldKnowledgeSegment.getVectorId())); - knowledgeSegment.setVectorId(""); + deleteVectorStore(vectorStore, segment); } - // 3 更新段落状态 - segmentMapper.updateById(knowledgeSegment); + } + + private void writeVectorStore(VectorStore vectorStore, AiKnowledgeSegmentDO segmentDO, Document segment) { + // 1. 向量存储 + segment.getMetadata().put(VECTOR_STORE_METADATA_KNOWLEDGE_ID, segmentDO.getKnowledgeId()); + segment.getMetadata().put(VECTOR_STORE_METADATA_DOCUMENT_ID, segmentDO.getDocumentId()); + segment.getMetadata().put(VECTOR_STORE_METADATA_SEGMENT_ID, segmentDO.getId()); + vectorStore.add(List.of(segment)); + + // 2. 更新向量 ID + segmentMapper.updateById(new AiKnowledgeSegmentDO().setId(segmentDO.getId()).setVectorId(segment.getId())); + } + + private void deleteVectorStore(VectorStore vectorStore, AiKnowledgeSegmentDO segmentDO) { + // 1. 更新向量 ID + if (StrUtil.isEmpty(segmentDO.getVectorId())) { + return; + } + segmentMapper.updateById(new AiKnowledgeSegmentDO().setId(segmentDO.getId()) + .setVectorId(AiKnowledgeSegmentDO.VECTOR_ID_EMPTY)); + + // 2. 删除向量 + vectorStore.delete(List.of(segmentDO.getVectorId())); } @Override public List similaritySearch(AiKnowledgeSegmentSearchReqVO reqVO) { // 1. 校验 AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqVO.getKnowledgeId()); - AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId()); // 2. 获取向量存储实例 - VectorStore vectorStore = apiKeyService.getOrCreateVectorStore(model.getKeyId()); + VectorStore vectorStore = apiKeyService.getOrCreateVectorStoreByModelId(knowledge.getEmbeddingModelId()); // 3.1 向量检索 - List documentList = vectorStore.similaritySearch(SearchRequest.builder() + List documents = vectorStore.similaritySearch(SearchRequest.builder() .query(reqVO.getContent()) - .topK(knowledge.getTopK()) - .similarityThreshold(knowledge.getSimilarityThreshold()) - .filterExpression(new FilterExpressionBuilder().eq(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, reqVO.getKnowledgeId()).build()) + .topK(knowledge.getTopK()).similarityThreshold(knowledge.getSimilarityThreshold()) + .filterExpression(new FilterExpressionBuilder() + .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqVO.getKnowledgeId()).build()) .build()); - if (CollUtil.isEmpty(documentList)) { + if (CollUtil.isEmpty(documents)) { return ListUtil.empty(); } // 3.2 段落召回 - return segmentMapper.selectListByVectorIds(CollUtil.getFieldValues(documentList, "id", String.class)); + return segmentMapper.selectListByVectorIds(convertList(documents, Document::getId)); } /** @@ -133,4 +192,22 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService return knowledgeSegment; } + private VectorStore getVectorStoreById(AiKnowledgeDO knowledge) { + return apiKeyService.getOrCreateVectorStoreByModelId(knowledge.getEmbeddingModelId()); + } + + private VectorStore getVectorStoreById(Long knowledgeId) { + return getVectorStoreById(knowledgeService.validateKnowledgeExists(knowledgeId)); + } + + private static TextSplitter buildTokenTextSplitter(Integer segmentMaxTokens) { + return TokenTextSplitter.builder() + .withChunkSize(segmentMaxTokens) + .withMinChunkSizeChars(Integer.MAX_VALUE) // 忽略字符的截断 + .withMinChunkLengthToEmbed(1) // 允许的最小有效分段长度 + .withMaxNumChunks(Integer.MAX_VALUE) + .withKeepSeparator(true) // 保留分隔符 + .build(); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java index 7060076a42..5c454383f9 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java @@ -1,11 +1,9 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; -import org.springframework.ai.vectorstore.VectorStore; /** * AI 知识库-基础信息 Service 接口 @@ -18,18 +16,16 @@ public interface AiKnowledgeService { * 创建知识库 * * @param createReqVO 创建信息 - * @param userId 用户编号 * @return 编号 */ - Long createKnowledge(AiKnowledgeCreateReqVO createReqVO, Long userId); + Long createKnowledge(AiKnowledgeSaveReqVO createReqVO); /** * 更新知识库 * * @param updateReqVO 更新信息 - * @param userId 用户编号 */ - void updateKnowledge(AiKnowledgeUpdateReqVO updateReqVO, Long userId); + void updateKnowledge(AiKnowledgeSaveReqVO updateReqVO); /** * 校验知识库是否存在 @@ -41,18 +37,9 @@ public interface AiKnowledgeService { /** * 获得知识库分页 * - * @param userId 用户编号 * @param pageReqVO 分页查询 * @return 知识库分页 */ - PageResult getKnowledgePage(Long userId, AiKnowledgePageReqVO pageReqVO); - - /** - * 根据知识库编号获取向量存储实例 - * - * @param id 知识库编号 - * @return 向量存储实例 - */ - VectorStore getVectorStoreById(Long id); + PageResult getKnowledgePage(AiKnowledgePageReqVO pageReqVO); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java index 1a000c19d1..583eabaeaa 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java @@ -1,12 +1,10 @@ package cn.iocoder.yudao.module.ai.service.knowledge; -import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper; @@ -34,35 +32,30 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { @Resource private AiChatModelService chatModelService; - @Resource - private AiApiKeyService apiKeyService; @Override - public Long createKnowledge(AiKnowledgeCreateReqVO createReqVO, Long userId) { + public Long createKnowledge(AiKnowledgeSaveReqVO createReqVO) { // 1. 校验模型配置 - AiChatModelDO model = chatModelService.validateChatModel(createReqVO.getModelId()); + AiChatModelDO model = chatModelService.validateChatModel(createReqVO.getEmbeddingModelId()); // 2. 插入知识库 - AiKnowledgeDO knowledgeBase = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class) - .setModel(model.getModel()).setUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus()); - knowledgeMapper.insert(knowledgeBase); - return knowledgeBase.getId(); + AiKnowledgeDO knowledge = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class) + .setEmbeddingModel(model.getModel()).setStatus(CommonStatusEnum.ENABLE.getStatus()); + knowledgeMapper.insert(knowledge); + return knowledge.getId(); } @Override - public void updateKnowledge(AiKnowledgeUpdateReqVO updateReqVO, Long userId) { + public void updateKnowledge(AiKnowledgeSaveReqVO updateReqVO) { // 1.1 校验知识库存在 - AiKnowledgeDO knowledgeBaseDO = validateKnowledgeExists(updateReqVO.getId()); - if (ObjUtil.notEqual(knowledgeBaseDO.getUserId(), userId)) { - throw exception(KNOWLEDGE_NOT_EXISTS); - } + validateKnowledgeExists(updateReqVO.getId()); // 1.2 校验模型配置 - AiChatModelDO model = chatModelService.validateChatModel(updateReqVO.getModelId()); + AiChatModelDO model = chatModelService.validateChatModel(updateReqVO.getEmbeddingModelId()); // 2. 更新知识库 - AiKnowledgeDO updateDO = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class); - updateDO.setModel(model.getModel()); - knowledgeMapper.updateById(updateDO); + AiKnowledgeDO updateObj = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class) + .setEmbeddingModel(model.getModel()); + knowledgeMapper.updateById(updateObj); } @Override @@ -75,16 +68,8 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { } @Override - public PageResult getKnowledgePage(Long userId, AiKnowledgePageReqVO pageReqVO) { - return knowledgeMapper.selectPage(userId, pageReqVO); - } - - @Override - public VectorStore getVectorStoreById(Long id) { - AiKnowledgeDO knowledge = validateKnowledgeExists(id); - AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId()); - // 创建或获取 VectorStore 对象 - return apiKeyService.getOrCreateVectorStore(model.getKeyId()); + public PageResult getKnowledgePage(AiKnowledgePageReqVO pageReqVO) { + return knowledgeMapper.selectPage(pageReqVO); } } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java index f5f8813492..29d9e3e07a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java @@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveR import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; import jakarta.validation.Valid; 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; @@ -113,20 +112,12 @@ public interface AiApiKeyService { */ SunoApi getSunoApi(); - /** - * 获得 EmbeddingModel 对象 - * - * @param id 编号 - * @return EmbeddingModel 对象 - */ - EmbeddingModel getEmbeddingModel(Long id); - /** * 获得 VectorStore 对象 * - * @param id 编号 + * @param modelId 编号 * @return VectorStore 对象 */ - VectorStore getOrCreateVectorStore(Long id); + VectorStore getOrCreateVectorStoreByModelId(Long modelId); } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java index 50e1fbd7ac..1ef7640f29 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java @@ -10,12 +10,14 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper; import jakarta.annotation.Resource; 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 org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -36,6 +38,11 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { @Resource private AiApiKeyMapper apiKeyMapper; + // TODO @芋艿:后续要不要改? + @Resource + @Lazy // 延迟加载,解决渲染依赖 + private AiChatModelService chatModelService; + @Resource private AiModelFactory modelFactory; @@ -136,18 +143,18 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { } @Override - public EmbeddingModel getEmbeddingModel(Long id) { - AiApiKeyDO apiKey = validateApiKey(id); + public VectorStore getOrCreateVectorStoreByModelId(Long modelId) { + // 获取模型 + 密钥 + AiChatModelDO chatModel = chatModelService.validateChatModel(modelId); + AiApiKeyDO apiKey = validateApiKey(chatModel.getKeyId()); AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); - return modelFactory.getOrCreateEmbeddingModel(platform, apiKey.getApiKey(), apiKey.getUrl()); - } - @Override - public VectorStore getOrCreateVectorStore(Long id) { - AiApiKeyDO apiKey = validateApiKey(id); - AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); + // 创建或获取 EmbeddingModel 对象 + EmbeddingModel embeddingModel = modelFactory.getOrCreateEmbeddingModel(platform, apiKey.getApiKey(), + apiKey.getUrl(), chatModel.getModel()); + // 创建或获取 VectorStore 对象 - return modelFactory.getOrCreateVectorStore(getEmbeddingModel(id), platform, apiKey.getApiKey(), apiKey.getUrl()); + return modelFactory.getOrCreateVectorStore(embeddingModel); } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java index 243c4ae4bc..d4275d5e5d 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java @@ -89,21 +89,19 @@ public interface AiModelFactory { * @param platform 平台 * @param apiKey API KEY * @param url API URL + * @param model 模型 * @return ChatModel 对象 */ - EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url); + EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url, String model); /** * 基于指定配置,获得 VectorStore 对象 *

* 如果不存在,则进行创建 * - * @param embeddingModel 嵌入模型 - * @param platform 平台 - * @param apiKey API KEY - * @param url API URL + * @param embeddingModel 向量模型 * @return VectorStore 对象 */ - VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url); + VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel); } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index e07347334b..779bd8e2fb 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -21,6 +21,7 @@ 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.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 org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration; @@ -33,10 +34,13 @@ import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration; import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiConnectionProperties; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.document.MetadataMode; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; 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.OpenAiImageModel; import org.springframework.ai.openai.api.OpenAiApi; @@ -184,13 +188,15 @@ public class AiModelFactoryImpl implements AiModelFactory { } @Override - public EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url) { - String cacheKey = buildClientCacheKey(EmbeddingModel.class, platform, apiKey, url); + 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) () -> { - // TODO @xin 先测试一个 switch (platform) { case TONG_YI: - return buildTongYiEmbeddingModel(apiKey); + return buildTongYiEmbeddingModel(apiKey, model); + case OLLAMA: + return buildOllamaEmbeddingModel(url, model); + // TODO @芋艿:各个平台的向量化能力; default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } @@ -198,13 +204,14 @@ public class AiModelFactoryImpl implements AiModelFactory { } @Override - public VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url) { - String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url); + public VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel) { +// String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url); + String cacheKey = buildClientCacheKey(VectorStore.class, embeddingModel); return Singleton.get(cacheKey, (Func0) () -> { - String prefix = StrUtil.format("{}#{}:", platform.getPlatform(), apiKey); // TODO @芋艿:先临时使用 store return SimpleVectorStore.builder(embeddingModel).build(); // TODO @芋艿:@xin:后续看看,是不是切到阿里云之类的 +// String prefix = StrUtil.format("{}#{}:", platform.getPlatform(), apiKey); // var config = RedisVectorStore.RedisVectorStoreConfig.builder() // .withIndexName(cacheKey) // .withPrefix(prefix) @@ -388,9 +395,16 @@ public class AiModelFactoryImpl implements AiModelFactory { /** * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeEmbeddingModel 方法 */ - private EmbeddingModel buildTongYiEmbeddingModel(String apiKey) { + private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) { DashScopeApi dashScopeApi = new DashScopeApi(apiKey); - return new DashScopeEmbeddingModel(dashScopeApi); + DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model).build(); + return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions); + } + + 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(); } } From 7918ba7d29698a39dc0141e20284d4946d13fa9a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 28 Feb 2025 08:16:44 +0800 Subject: [PATCH 18/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=96=B0=E5=A2=9E=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E5=BA=93=E6=96=87=E6=A1=A3=E7=9A=84=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/enums/ErrorCodeConstants.java | 1 + .../AiKnowledgeDocumentController.http | 25 +++++++- .../AiKnowledgeDocumentController.java | 15 ++++- .../AiKnowledgeDocumentCreateListReqVO.java | 42 +++++++++++++ .../knowledge/AiKnowledgeDocumentService.java | 11 ++++ .../AiKnowledgeDocumentServiceImpl.java | 60 +++++++++++++++---- .../knowledge/AiKnowledgeSegmentService.java | 12 ++++ .../AiKnowledgeSegmentServiceImpl.java | 6 +- 8 files changed, 154 insertions(+), 18 deletions(-) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateListReqVO.java diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index 48913e91fa..5ac6d2ae9e 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -56,6 +56,7 @@ public interface ErrorCodeConstants { ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_022_008_101, "文档不存在!"); ErrorCode KNOWLEDGE_DOCUMENT_FILE_EMPTY = new ErrorCode(1_022_008_102, "文档内容为空!"); + ErrorCode KNOWLEDGE_DOCUMENT_FILE_DOWNLOAD_FAIL = new ErrorCode(1_022_008_102, "文件下载失败!"); ErrorCode KNOWLEDGE_DOCUMENT_FILE_READ_FAIL = new ErrorCode(1_022_008_102, "文档加载失败!"); ErrorCode KNOWLEDGE_SEGMENT_NOT_EXISTS = new ErrorCode(1_022_008_202, "段落不存在!"); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http index 7af4780c3c..22c1d91115 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http @@ -9,4 +9,27 @@ tenant-id: {{adminTenantId}} "name": "测试文档", "url": "https://static.iocoder.cn/README.md", "segmentMaxTokens": 800 -} \ No newline at end of file +} + +### 批量创建知识文档 +POST {{baseUrl}}/ai/knowledge/document/create-list +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + +{ + "knowledgeId": 1, + "list": [ + { + "name": "测试文档1", + "url": "https://static.iocoder.cn/README.md", + "segmentMaxTokens": 800 + }, + { + "name": "测试文档2", + "url": "https://static.iocoder.cn/README_yudao.md", + "segmentMaxTokens": 400 + } + ] +} + diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java index cd6feb3056..8d61d0c1e4 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentRespVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; @@ -16,6 +17,8 @@ import jakarta.validation.Valid; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.List; + import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; @Tag(name = "管理后台 - AI 知识库文档") @@ -38,8 +41,16 @@ public class AiKnowledgeDocumentController { @PostMapping("/create") @Operation(summary = "新建文档") public CommonResult createKnowledgeDocument(@RequestBody @Valid AiKnowledgeDocumentCreateReqVO reqVO) { - Long knowledgeDocumentId = documentService.createKnowledgeDocument(reqVO); - return success(knowledgeDocumentId); + Long id = documentService.createKnowledgeDocument(reqVO); + return success(id); + } + + @PostMapping("/create-list") + @Operation(summary = "批量新建文档") + public CommonResult> createKnowledgeDocumentList( + @RequestBody @Valid AiKnowledgeDocumentCreateListReqVO reqVO) { + List ids = documentService.createKnowledgeDocumentList(reqVO); + return success(ids); } @PutMapping("/update") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateListReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateListReqVO.java new file mode 100644 index 0000000000..4bd817aa4c --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateListReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import java.util.List; + +@Schema(description = "管理后台 - AI 知识库文档批量创建 Request VO") +@Data +public class AiKnowledgeDocumentCreateListReqVO { + + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204") + @NotNull(message = "知识库编号不能为空") + private Long knowledgeId; + + @Schema(description = "文档列表", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "文档列表不能为空") + private List list; + + @Schema(description = "文档") + @Data + public static class Document { + + @Schema(description = "文档名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "三方登陆") + @NotBlank(message = "文档名称不能为空") + private String name; + + @Schema(description = "文档 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn") + @URL(message = "文档 URL 格式不正确") + private String url; + + @Schema(description = "分段的最大 Token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "800") + @NotNull(message = "分段的最大 Token 数不能为空") + private Integer segmentMaxTokens; + + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java index 9cb7c5a827..41fa41527d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -4,8 +4,11 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; +import java.util.List; + /** * AI 知识库-文档 Service 接口 * @@ -21,6 +24,14 @@ public interface AiKnowledgeDocumentService { */ Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO); + /** + * 批量创建文档 + * + * @param createListReqVO 批量创建 Request VO + * @return 文档编号列表 + */ + List createKnowledgeDocumentList(AiKnowledgeDocumentCreateListReqVO createListReqVO); + /** * 获取文档分页 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 0f48d5a992..fd45b97062 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -8,6 +8,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper; @@ -21,9 +22,11 @@ import org.springframework.core.io.ByteArrayResource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; /** @@ -54,25 +57,45 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId()); // 2. 下载文档 - TikaDocumentReader loader = new TikaDocumentReader(downloadFile(createReqVO.getUrl())); - List documents = loader.get(); - Document document = CollUtil.getFirst(documents); - if (document == null || StrUtil.isEmpty(document.getText())) { - throw exception(KNOWLEDGE_DOCUMENT_FILE_READ_FAIL); - } + String content = readUrl(createReqVO.getUrl()); // 3. 文档记录入库 - String content = document.getText(); AiKnowledgeDocumentDO documentDO = BeanUtils.toBean(createReqVO, AiKnowledgeDocumentDO.class) .setContent(content).setContentLength(content.length()).setTokens(tokenCountEstimator.estimate(content)) .setStatus(CommonStatusEnum.ENABLE.getStatus()); knowledgeDocumentMapper.insert(documentDO); - // 4. 文档切片入库 - knowledgeSegmentService.createKnowledgeSegmentBySplitContent(documentDO.getId(), document.getText()); + // 4. 文档切片入库(同步) + knowledgeSegmentService.createKnowledgeSegmentBySplitContent(documentDO.getId(), content); return documentDO.getId(); } + @Override + @Transactional(rollbackFor = Exception.class) + public List createKnowledgeDocumentList(AiKnowledgeDocumentCreateListReqVO createListReqVO) { + // 1. 校验参数 + knowledgeService.validateKnowledgeExists(createListReqVO.getKnowledgeId()); + + // 2. 下载文档 + List contents = convertList(createListReqVO.getList(), document -> readUrl(document.getUrl())); + + // 3. 文档记录入库 + List documentDOs = new ArrayList<>(createListReqVO.getList().size()); + for (int i = 0; i < createListReqVO.getList().size(); i++) { + AiKnowledgeDocumentCreateListReqVO.Document documentVO = createListReqVO.getList().get(i); + String content = contents.get(i); + documentDOs.add(BeanUtils.toBean(documentVO, AiKnowledgeDocumentDO.class).setKnowledgeId(createListReqVO.getKnowledgeId()) + .setContent(content).setContentLength(content.length()).setTokens(tokenCountEstimator.estimate(content)) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + } + knowledgeDocumentMapper.insertBatch(documentDOs); + + // 4. 批量创建文档切片(异步) + documentDOs.forEach(documentDO -> + knowledgeSegmentService.createKnowledgeSegmentBySplitContentAsync(documentDO.getId(), documentDO.getContent())); + return convertList(documentDOs, AiKnowledgeDocumentDO::getId); + } + @Override public PageResult getKnowledgeDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO) { return knowledgeDocumentMapper.selectPage(pageReqVO); @@ -97,17 +120,28 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic return knowledgeDocument; } - private org.springframework.core.io.Resource downloadFile(String url) { + private static String readUrl(String url) { + // 下载文件 + ByteArrayResource resource = null; try { byte[] bytes = HttpUtil.downloadBytes(url); if (bytes.length == 0) { throw exception(KNOWLEDGE_DOCUMENT_FILE_EMPTY); } - return new ByteArrayResource(bytes); + resource = new ByteArrayResource(bytes); } catch (Exception e) { - log.error("[downloadFile][url({}) 下载失败]", url, e); - throw new RuntimeException(e); + log.error("[readUrl][url({}) 读取失败]", url, e); + throw exception(KNOWLEDGE_DOCUMENT_FILE_DOWNLOAD_FAIL); } + + // 读取文件 + TikaDocumentReader loader = new TikaDocumentReader(resource); + List documents = loader.get(); + Document document = CollUtil.getFirst(documents); + if (document == null || StrUtil.isEmpty(document.getText())) { + throw exception(KNOWLEDGE_DOCUMENT_FILE_READ_FAIL); + } + return document.getText(); } } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java index 2a8d9afd87..064d373b73 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowle import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; +import org.springframework.scheduling.annotation.Async; import java.util.List; @@ -32,6 +33,17 @@ public interface AiKnowledgeSegmentService { */ void createKnowledgeSegmentBySplitContent(Long documentId, String content); + /** + * 【异步】基于 content 内容,切片创建多个段落 + * + * @param documentId 知识库文档编号 + * @param content 文档内容 + */ + @Async + default void createKnowledgeSegmentBySplitContentAsync(Long documentId, String content) { + createKnowledgeSegmentBySplitContent(documentId, content); + } + /** * 更新段落的内容 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index 9b15135c95..615299b2db 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -110,8 +110,10 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService // 3.1 更新切片 AiKnowledgeSegmentDO segmentDO = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class); segmentMapper.updateById(segmentDO); - // 3.2 重新向量化 - writeVectorStore(vectorStore, segmentDO, new Document(segmentDO.getContent())); + // 3.2 重新向量化,必须开启状态 + if (CommonStatusEnum.isEnable(segmentDO.getStatus())) { + writeVectorStore(vectorStore, segmentDO, new Document(segmentDO.getContent())); + } } @Override From e5cc9d2ad857939a42a8ddabd37c9c445981d734 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 28 Feb 2025 18:57:17 +0800 Subject: [PATCH 19/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91AI=EF=BC=9A=E7=9F=A5=E8=AF=86=E5=BA=93?= =?UTF-8?q?=E7=9A=84=E7=B4=A2=E5=BC=95=E8=BF=87=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../knowledge/AiKnowledgeController.java | 12 +++++- .../AiKnowledgeDocumentController.java | 10 ++++- .../AiKnowledgeSegmentController.java | 1 + .../AiKnowledgeDocumentCreateListReqVO.java | 10 ++--- .../AiKnowledgeDocumentPageReqVO.java | 3 ++ .../document/AiKnowledgeDocumentRespVO.java | 39 +++++++++++-------- .../AiKnowledgeDocumentUpdateReqVO.java | 4 +- .../vo/knowledge/AiKnowledgePageReqVO.java | 17 +++++++- .../vo/knowledge/AiKnowledgeRespVO.java | 21 ++++++++-- .../vo/knowledge/AiKnowledgeSaveReqVO.java | 7 +++- .../knowledge/AiKnowledgeDocumentDO.java | 9 ++++- .../knowledge/AiKnowledgeSegmentDO.java | 5 +++ .../knowledge/AiKnowledgeDocumentMapper.java | 3 +- .../mysql/knowledge/AiKnowledgeMapper.java | 10 +++-- .../knowledge/AiKnowledgeSegmentMapper.java | 2 +- .../knowledge/AiKnowledgeDocumentService.java | 10 ++++- .../AiKnowledgeDocumentServiceImpl.java | 17 +++++--- .../service/knowledge/AiKnowledgeService.java | 8 ++++ .../knowledge/AiKnowledgeServiceImpl.java | 12 ++++-- 19 files changed, 151 insertions(+), 49 deletions(-) rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/{knowledge => document}/AiKnowledgeDocumentCreateListReqVO.java (84%) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java index 5c27810260..df49395750 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java @@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnow import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; @@ -17,6 +18,7 @@ import org.springframework.web.bind.annotation.*; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +// TODO @芋艿:增加权限标识 @Tag(name = "管理后台 - AI 知识库") @RestController @RequestMapping("/ai/knowledge") @@ -29,11 +31,19 @@ public class AiKnowledgeController { @GetMapping("/page") @Operation(summary = "获取知识库分页") public CommonResult> getKnowledgePage(@Valid AiKnowledgePageReqVO pageReqVO) { - + PageResult pageResult = knowledgeService.getKnowledgePage(pageReqVO); return success(BeanUtils.toBean(pageResult, AiKnowledgeRespVO.class)); } + @GetMapping("/get") + @Operation(summary = "获得知识库") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult getKnowledge(@RequestParam("id") Long id) { + AiKnowledgeDO knowledge = knowledgeService.getKnowledge(id); + return success(BeanUtils.toBean(knowledge, AiKnowledgeRespVO.class)); + } + @PostMapping("/create") @Operation(summary = "创建知识库") public CommonResult createKnowledge(@RequestBody @Valid AiKnowledgeSaveReqVO createReqVO) { diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java index 8d61d0c1e4..12e8ad1fcd 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java @@ -6,7 +6,7 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentRespVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateListReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; @@ -21,6 +21,7 @@ import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +// TODO @芋艿:增加权限标识 @Tag(name = "管理后台 - AI 知识库文档") @RestController @RequestMapping("/ai/knowledge/document") @@ -38,6 +39,13 @@ public class AiKnowledgeDocumentController { return success(BeanUtils.toBean(pageResult, AiKnowledgeDocumentRespVO.class)); } + @GetMapping("/get") + @Operation(summary = "获取文档详情") + public CommonResult getKnowledgeDocument(@RequestParam("id") Long id) { + AiKnowledgeDocumentDO document = documentService.getKnowledgeDocument(id); + return success(BeanUtils.toBean(document, AiKnowledgeDocumentRespVO.class)); + } + @PostMapping("/create") @Operation(summary = "新建文档") public CommonResult createKnowledgeDocument(@RequestBody @Valid AiKnowledgeDocumentCreateReqVO reqVO) { diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java index d4ca7ca499..267f0021d2 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java @@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.*; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +// TODO @芋艿:增加权限标识 @Tag(name = "管理后台 - AI 知识库段落") @RestController @RequestMapping("/ai/knowledge/segment") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateListReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentCreateListReqVO.java similarity index 84% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateListReqVO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentCreateListReqVO.java index 4bd817aa4c..6545c0bc1c 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeDocumentCreateListReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentCreateListReqVO.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge; +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; @@ -17,6 +17,10 @@ public class AiKnowledgeDocumentCreateListReqVO { @NotNull(message = "知识库编号不能为空") private Long knowledgeId; + @Schema(description = "分段的最大 Token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "800") + @NotNull(message = "分段的最大 Token 数不能为空") + private Integer segmentMaxTokens; + @Schema(description = "文档列表", requiredMode = Schema.RequiredMode.REQUIRED) @NotEmpty(message = "文档列表不能为空") private List list; @@ -33,10 +37,6 @@ public class AiKnowledgeDocumentCreateListReqVO { @URL(message = "文档 URL 格式不正确") private String url; - @Schema(description = "分段的最大 Token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "800") - @NotNull(message = "分段的最大 Token 数不能为空") - private Integer segmentMaxTokens; - } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java index 76c001bd35..15bb603c21 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentPageReqVO.java @@ -8,6 +8,9 @@ import lombok.Data; @Data public class AiKnowledgeDocumentPageReqVO extends PageParam { + @Schema(description = "知识库编号", example = "1") + private Long knowledgeId; + @Schema(description = "文档名称", example = "Java 开发手册") private String name; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java index 96ca61b3d2..7aef94a3f4 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentRespVO.java @@ -1,38 +1,45 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document; -import cn.iocoder.yudao.framework.common.pojo.PageParam; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -@Schema(description = "管理后台 - AI 知识库-文档 Response VO") -@Data -public class AiKnowledgeDocumentRespVO extends PageParam { +import java.time.LocalDateTime; - @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") +@Schema(description = "管理后台 - AI 知识库文档 Response VO") +@Data +public class AiKnowledgeDocumentRespVO { + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") private Long id; @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") private Long knowledgeId; - @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") + @Schema(description = "文档名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") private String name; - @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 是一门面向对象的语言.....") - private String content; - - @Schema(description = "文档 url", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn") + @Schema(description = "文档 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://doc.iocoder.cn") private String url; - @Schema(description = "token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @Schema(description = "文档内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 是一门面向对象的语言.....") + private String content; + + @Schema(description = "文档内容长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Integer contentLength; + + @Schema(description = "文档 Token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Integer tokens; - @Schema(description = "字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1008") - private Integer wordCount; + @Schema(description = "分片最大 Token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "512") + private Integer segmentMaxTokens; - @Schema(description = "切片状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - private Integer sliceStatus; + @Schema(description = "召回次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer retrievalCount; - @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") private Integer status; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java index 13f871f6d4..1097f40606 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java @@ -6,8 +6,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; - -@Schema(description = "管理后台 - AI 更新 知识库-文档 Request VO") +// TODO @芋艿:稍后优化 +@Schema(description = "管理后台 - AI 知识库文档更新 Request VO") @Data public class AiKnowledgeDocumentUpdateReqVO { diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgePageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgePageReqVO.java index 941732f1ad..dc7943cf25 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgePageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgePageReqVO.java @@ -1,14 +1,29 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; @Schema(description = "管理后台 - AI 知识库的分页 Request VO") @Data public class AiKnowledgePageReqVO extends PageParam { - @Schema(description = "知识库名称", example = "Java 开发手册") + @Schema(description = "知识库名称", example = "芋艿") private String name; + @Schema(description = "是否启用", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java index 3ff8a1c757..5e83b85a72 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeRespVO.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.time.LocalDateTime; @Schema(description = "管理后台 - AI 知识库 Response VO") @Data @@ -17,10 +18,22 @@ public class AiKnowledgeRespVO { @Schema(description = "知识库描述", example = "帮助你快速构建系统") private String description; - @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "14") - private Long modelId; + @Schema(description = "向量模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "14") + private Long embeddingModelId; - @Schema(description = "模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "qwen-72b-chat") - private String model; + @Schema(description = "向量模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "qwen-72b-chat") + private String embeddingModel; + + @Schema(description = "topK", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer topK; + + @Schema(description = "相似度阈值", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.7") + private Double similarityThreshold; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeSaveReqVO.java index f7c6a245ca..774b7234a7 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeSaveReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/knowledge/AiKnowledgeSaveReqVO.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -31,6 +33,9 @@ public class AiKnowledgeSaveReqVO { @NotNull(message = "相似性阈值不能为空") private Double similarityThreshold; - // TODO @芋艿:status + @Schema(description = "是否启用", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "是否启用不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java index 56de374f7e..ac014e926b 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDocumentDO.java @@ -29,7 +29,7 @@ public class AiKnowledgeDocumentDO extends BaseDO { */ private Long knowledgeId; /** - * 文件名称 + * 文档名称 */ private String name; /** @@ -50,10 +50,15 @@ public class AiKnowledgeDocumentDO extends BaseDO { */ private Integer tokens; /** - * 每个文本块的目标 token 数 + * 分片最大 Token 数 */ private Integer segmentMaxTokens; + /** + * 召回次数 + */ + private Integer retrievalCount; + /** * 状态 *

diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java index 5ab18e7d7a..cccbd6846b 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeSegmentDO.java @@ -57,6 +57,11 @@ public class AiKnowledgeSegmentDO extends BaseDO { */ private Integer tokens; + /** + * 召回次数 + */ + private Integer retrievalCount; + /** * 状态 *

diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java index 7692d1cede..8e6bb4d2b7 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java @@ -8,7 +8,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO import org.apache.ibatis.annotations.Mapper; /** - * AI 知识库-文档 Mapper + * AI 知识库文档 Mapper * * @author xiaoxin */ @@ -17,6 +17,7 @@ public interface AiKnowledgeDocumentMapper extends BaseMapperX selectPage(AiKnowledgeDocumentPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(AiKnowledgeDocumentDO::getKnowledgeId, reqVO.getKnowledgeId()) .likeIfPresent(AiKnowledgeDocumentDO::getName, reqVO.getName()) .orderByDesc(AiKnowledgeDocumentDO::getId)); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java index 735332beb9..0d6022ed07 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; @@ -9,7 +8,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import org.apache.ibatis.annotations.Mapper; /** - * AI 知识库基础信息 Mapper + * AI 知识库 Mapper * * @author xiaoxin */ @@ -18,7 +17,10 @@ public interface AiKnowledgeMapper extends BaseMapperX { default PageResult selectPage(AiKnowledgePageReqVO pageReqVO) { return selectPage(pageReqVO, new LambdaQueryWrapperX() - .eq(AiKnowledgeDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) - .likeIfPresent(AiKnowledgeDO::getName, pageReqVO.getName())); + .likeIfPresent(AiKnowledgeDO::getName, pageReqVO.getName()) + .eqIfPresent(AiKnowledgeDO::getStatus, pageReqVO.getStatus()) + .betweenIfPresent(AiKnowledgeDO::getCreateTime, pageReqVO.getCreateTime()) + .orderByDesc(AiKnowledgeDO::getId)); } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java index 094f19b52e..30b38f6c8c 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java @@ -10,7 +10,7 @@ import org.apache.ibatis.annotations.Mapper; import java.util.List; /** - * AI 知识库-分片 Mapper + * AI 知识库分片 Mapper * * @author xiaoxin */ diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java index 41fa41527d..5d5c811bb2 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateListReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import java.util.List; @@ -40,6 +40,14 @@ public interface AiKnowledgeDocumentService { */ PageResult getKnowledgeDocumentPage(AiKnowledgeDocumentPageReqVO pageReqVO); + /** + * 获取文档详情 + * + * @param id 文档编号 + * @return 文档详情 + */ + AiKnowledgeDocumentDO getKnowledgeDocument(Long id); + /** * 更新文档 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index fd45b97062..c90839f556 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -8,7 +8,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateListReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper; @@ -84,15 +84,17 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic for (int i = 0; i < createListReqVO.getList().size(); i++) { AiKnowledgeDocumentCreateListReqVO.Document documentVO = createListReqVO.getList().get(i); String content = contents.get(i); - documentDOs.add(BeanUtils.toBean(documentVO, AiKnowledgeDocumentDO.class).setKnowledgeId(createListReqVO.getKnowledgeId()) - .setContent(content).setContentLength(content.length()).setTokens(tokenCountEstimator.estimate(content)) + documentDOs.add(BeanUtils.toBean(documentVO, AiKnowledgeDocumentDO.class) + .setKnowledgeId(createListReqVO.getKnowledgeId()) + .setContent(content).setContentLength(content.length()) + .setTokens(tokenCountEstimator.estimate(content)) .setStatus(CommonStatusEnum.ENABLE.getStatus())); } knowledgeDocumentMapper.insertBatch(documentDOs); // 4. 批量创建文档切片(异步) - documentDOs.forEach(documentDO -> - knowledgeSegmentService.createKnowledgeSegmentBySplitContentAsync(documentDO.getId(), documentDO.getContent())); + documentDOs.forEach(documentDO -> knowledgeSegmentService + .createKnowledgeSegmentBySplitContentAsync(documentDO.getId(), documentDO.getContent())); return convertList(documentDOs, AiKnowledgeDocumentDO::getId); } @@ -101,6 +103,11 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic return knowledgeDocumentMapper.selectPage(pageReqVO); } + @Override + public AiKnowledgeDocumentDO getKnowledgeDocument(Long id) { + return knowledgeDocumentMapper.selectById(id); + } + @Override public void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO) { // 1. 校验文档是否存在 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java index 5c454383f9..d1a7f7d1c4 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java @@ -27,6 +27,14 @@ public interface AiKnowledgeService { */ void updateKnowledge(AiKnowledgeSaveReqVO updateReqVO); + /** + * 获得知识库 + * + * @param id 编号 + * @return 知识库 + */ + AiKnowledgeDO getKnowledge(Long id); + /** * 校验知识库是否存在 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java index 583eabaeaa..900cfb4a17 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.ai.service.knowledge; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO; @@ -8,11 +7,9 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnow import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.vectorstore.VectorStore; import org.springframework.stereotype.Service; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -40,7 +37,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { // 2. 插入知识库 AiKnowledgeDO knowledge = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class) - .setEmbeddingModel(model.getModel()).setStatus(CommonStatusEnum.ENABLE.getStatus()); + .setEmbeddingModel(model.getModel()); knowledgeMapper.insert(knowledge); return knowledge.getId(); } @@ -56,6 +53,13 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { AiKnowledgeDO updateObj = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class) .setEmbeddingModel(model.getModel()); knowledgeMapper.updateById(updateObj); + + // TODO @芋艿:如果模型变化,需要 reindex 所有的文档 + } + + @Override + public AiKnowledgeDO getKnowledge(Long id) { + return knowledgeMapper.selectById(id); } @Override From d1e207899a1b7e4497104cd3321f4d2fcf5c3a4c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 1 Mar 2025 07:48:16 +0800 Subject: [PATCH 20/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E7=9F=A5=E8=AF=86=E5=BA=93?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=88=87=E7=89=87=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AiKnowledgeSegmentController.http | 5 +++ .../AiKnowledgeSegmentController.java | 23 +++++++++++-- .../vo/segment/AiKnowledgeSegmentRespVO.java | 12 +++++-- .../knowledge/AiKnowledgeDocumentService.java | 8 +++++ .../AiKnowledgeDocumentServiceImpl.java | 5 +-- .../knowledge/AiKnowledgeSegmentService.java | 13 ++++++-- .../AiKnowledgeSegmentServiceImpl.java | 32 ++++++++++++++++--- 7 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http new file mode 100644 index 0000000000..25d622d4c1 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http @@ -0,0 +1,5 @@ +### 切片内容 +GET {{baseUrl}}/ai/knowledge/segment/split?url=https://static.iocoder.cn/README_yudao.md&segmentMaxTokens=800 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java index 267f0021d2..6177f5411a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java @@ -10,12 +10,16 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowle import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.List; + import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; // TODO @芋艿:增加权限标识 @@ -30,7 +34,8 @@ public class AiKnowledgeSegmentController { @GetMapping("/page") @Operation(summary = "获取段落分页") - public CommonResult> getKnowledgeSegmentPage(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) { + public CommonResult> getKnowledgeSegmentPage( + @Valid AiKnowledgeSegmentPageReqVO pageReqVO) { PageResult pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO); return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class)); } @@ -44,9 +49,23 @@ public class AiKnowledgeSegmentController { @PutMapping("/update-status") @Operation(summary = "启禁用段落内容") - public CommonResult updateKnowledgeSegmentStatus(@Valid @RequestBody AiKnowledgeSegmentUpdateStatusReqVO reqVO) { + public CommonResult updateKnowledgeSegmentStatus( + @Valid @RequestBody AiKnowledgeSegmentUpdateStatusReqVO reqVO) { segmentService.updateKnowledgeSegmentStatus(reqVO); return success(true); } + @GetMapping("/split") + @Operation(summary = "切片内容") + @Parameters({ + @Parameter(name = "url", description = "文档 URL", required = true), + @Parameter(name = "segmentMaxTokens", description = "分段的最大 Token 数", required = true) + }) + public CommonResult> splitContent( + @RequestParam("url") String url, + @RequestParam(value = "segmentMaxTokens") Integer segmentMaxTokens) { + List segments = segmentService.splitContent(url, segmentMaxTokens); + return success(BeanUtils.toBean(segments, AiKnowledgeSegmentRespVO.class)); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java index 5e3f2d8cbb..24c452621a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentRespVO.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -@Schema(description = "管理后台 - AI 知识库-文档 Response VO") +@Schema(description = "管理后台 - AI 知识库文档分片 Response VO") @Data public class AiKnowledgeSegmentRespVO { @@ -22,13 +22,19 @@ public class AiKnowledgeSegmentRespVO { @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") private String content; + @Schema(description = "切片内容长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer contentLength; + @Schema(description = "token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Integer tokens; - @Schema(description = "字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1008") - private Integer wordCount; + @Schema(description = "召回次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer retrievalCount; @Schema(description = "文档状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Long createTime; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java index 5d5c811bb2..8bbee98179 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -63,4 +63,12 @@ public interface AiKnowledgeDocumentService { */ AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id); + /** + * 读取 URL 内容 + * + * @param url URL + * @return 内容 + */ + String readUrl(String url); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index c90839f556..6ed7fdd57d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -127,9 +127,10 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic return knowledgeDocument; } - private static String readUrl(String url) { + @Override + public String readUrl(String url) { // 下载文件 - ByteArrayResource resource = null; + ByteArrayResource resource; try { byte[] bytes = HttpUtil.downloadBytes(url); if (bytes.length == 0) { diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java index 064d373b73..b105dcb8eb 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java @@ -29,7 +29,7 @@ public interface AiKnowledgeSegmentService { * 基于 content 内容,切片创建多个段落 * * @param documentId 知识库文档编号 - * @param content 文档内容 + * @param content 文档内容 */ void createKnowledgeSegmentBySplitContent(Long documentId, String content); @@ -37,7 +37,7 @@ public interface AiKnowledgeSegmentService { * 【异步】基于 content 内容,切片创建多个段落 * * @param documentId 知识库文档编号 - * @param content 文档内容 + * @param content 文档内容 */ @Async default void createKnowledgeSegmentBySplitContentAsync(Long documentId, String content) { @@ -66,4 +66,13 @@ public interface AiKnowledgeSegmentService { */ List similaritySearch(AiKnowledgeSegmentSearchReqVO reqVO); + /** + * 根据 URL 内容,切片创建多个段落 + * + * @param url URL 地址 + * @param segmentMaxTokens 段落最大 Token 数 + * @return 切片后的段落列表 + */ + List splitContent(String url, Integer segmentMaxTokens); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index 615299b2db..37599b01fc 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -75,9 +75,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService VectorStore vectorStore = getVectorStoreById(knowledgeDO); // 2. 文档切片 - Document document = new Document(content); - TextSplitter textSplitter = buildTokenTextSplitter(documentDO.getSegmentMaxTokens()); - List documentSegments = textSplitter.apply(Collections.singletonList(document)); + List documentSegments = splitContentByToken(content, documentDO.getSegmentMaxTokens()); // 3.1 存储切片 List segmentDOs = convertList(documentSegments, segment -> { @@ -86,7 +84,8 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService } return new AiKnowledgeSegmentDO().setKnowledgeId(documentDO.getKnowledgeId()).setDocumentId(documentId) .setContent(segment.getText()).setContentLength(segment.getText().length()) - .setVectorId(AiKnowledgeSegmentDO.VECTOR_ID_EMPTY).setTokens(tokenCountEstimator.estimate(segment.getText())) + .setVectorId(AiKnowledgeSegmentDO.VECTOR_ID_EMPTY) + .setTokens(tokenCountEstimator.estimate(segment.getText())) .setStatus(CommonStatusEnum.ENABLE.getStatus()); }); segmentMapper.insertBatch(segmentDOs); @@ -180,6 +179,26 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService return segmentMapper.selectListByVectorIds(convertList(documents, Document::getId)); } + @Override + public List splitContent(String url, Integer segmentMaxTokens) { + // 1. 读取 URL 内容 + String content = knowledgeDocumentService.readUrl(url); + + // 2. 文档切片 + List documentSegments = splitContentByToken(content, segmentMaxTokens); + + // 3. 转换为段落对象 + return convertList(documentSegments, segment -> { + if (StrUtil.isEmpty(segment.getText())) { + return null; + } + return new AiKnowledgeSegmentDO() + .setContent(segment.getText()) + .setContentLength(segment.getText().length()) + .setTokens(tokenCountEstimator.estimate(segment.getText())); + }); + } + /** * 校验段落是否存在 * @@ -202,6 +221,11 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService return getVectorStoreById(knowledgeService.validateKnowledgeExists(knowledgeId)); } + private static List splitContentByToken(String content, Integer segmentMaxTokens) { + TextSplitter textSplitter = buildTokenTextSplitter(segmentMaxTokens); + return textSplitter.apply(Collections.singletonList(new Document(content))); + } + private static TextSplitter buildTokenTextSplitter(Integer segmentMaxTokens) { return TokenTextSplitter.builder() .withChunkSize(segmentMaxTokens) From a998168b3b62713821c15b5798115bc589eb98f4 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 1 Mar 2025 11:42:11 +0800 Subject: [PATCH 21/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=96=B0=E5=A2=9E=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E5=BA=93=E6=96=87=E6=A1=A3=E7=9A=84=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E4=B8=8E=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AiKnowledgeDocumentController.java | 13 +++++-- .../AiKnowledgeSegmentController.java | 3 +- .../AiKnowledgeDocumentUpdateReqVO.java | 8 ++--- .../knowledge/AiKnowledgeSegmentMapper.java | 6 ++++ .../knowledge/AiKnowledgeDocumentService.java | 14 ++++++-- .../AiKnowledgeDocumentServiceImpl.java | 34 +++++++++++++++++-- .../knowledge/AiKnowledgeSegmentService.java | 7 ++++ .../AiKnowledgeSegmentServiceImpl.java | 16 +++++++++ 8 files changed, 87 insertions(+), 14 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java index 12e8ad1fcd..e828615ea1 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowl import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentRespVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; @@ -47,14 +48,14 @@ public class AiKnowledgeDocumentController { } @PostMapping("/create") - @Operation(summary = "新建文档") + @Operation(summary = "新建文档(单个)") public CommonResult createKnowledgeDocument(@RequestBody @Valid AiKnowledgeDocumentCreateReqVO reqVO) { Long id = documentService.createKnowledgeDocument(reqVO); return success(id); } @PostMapping("/create-list") - @Operation(summary = "批量新建文档") + @Operation(summary = "新建文档(多个)") public CommonResult> createKnowledgeDocumentList( @RequestBody @Valid AiKnowledgeDocumentCreateListReqVO reqVO) { List ids = documentService.createKnowledgeDocumentList(reqVO); @@ -68,4 +69,12 @@ public class AiKnowledgeDocumentController { return success(true); } + @PutMapping("/update-status") + @Operation(summary = "更新文档状态") + public CommonResult updateKnowledgeDocumentStatus( + @Valid @RequestBody AiKnowledgeDocumentUpdateStatusReqVO reqVO) { + documentService.updateKnowledgeDocumentStatus(reqVO); + return success(true); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java index 6177f5411a..2b21e0d1e4 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java @@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; +import org.hibernate.validator.constraints.URL; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -62,7 +63,7 @@ public class AiKnowledgeSegmentController { @Parameter(name = "segmentMaxTokens", description = "分段的最大 Token 数", required = true) }) public CommonResult> splitContent( - @RequestParam("url") String url, + @RequestParam("url") @URL String url, @RequestParam(value = "segmentMaxTokens") Integer segmentMaxTokens) { List segments = segmentService.splitContent(url, segmentMaxTokens); return success(BeanUtils.toBean(segments, AiKnowledgeSegmentRespVO.class)); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java index 1097f40606..e6dbe5cbd7 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateReqVO.java @@ -1,12 +1,9 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; -// TODO @芋艿:稍后优化 @Schema(description = "管理后台 - AI 知识库文档更新 Request VO") @Data public class AiKnowledgeDocumentUpdateReqVO { @@ -18,8 +15,7 @@ public class AiKnowledgeDocumentUpdateReqVO { @Schema(description = "名称", example = "Java 开发手册") private String name; - @Schema(description = "是否启用", example = "1") - @InEnum(CommonStatusEnum.class) - private Integer status; + @Schema(description = "分片最大 Token 数", example = "1000") + private Integer segmentMaxTokens; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java index 30b38f6c8c..8381c84300 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java @@ -31,4 +31,10 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX selectListByDocumentId(Long documentId) { + return selectList(new LambdaQueryWrapperX() + .eq(AiKnowledgeSegmentDO::getDocumentId, documentId) + .orderByDesc(AiKnowledgeSegmentDO::getId)); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java index 8bbee98179..4a45422d82 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; @@ -10,14 +11,14 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO import java.util.List; /** - * AI 知识库-文档 Service 接口 + * AI 知识库文档 Service 接口 * * @author xiaoxin */ public interface AiKnowledgeDocumentService { /** - * 创建文档 + * 创建文档(单个) * * @param createReqVO 文档创建 Request VO * @return 文档编号 @@ -25,7 +26,7 @@ public interface AiKnowledgeDocumentService { Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO); /** - * 批量创建文档 + * 创建文档(多个) * * @param createListReqVO 批量创建 Request VO * @return 文档编号列表 @@ -55,6 +56,13 @@ public interface AiKnowledgeDocumentService { */ void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO); + /** + * 更新文档状态 + * + * @param reqVO 更新状态信息 + */ + void updateKnowledgeDocumentStatus(AiKnowledgeDocumentUpdateStatusReqVO reqVO); + /** * 校验文档是否存在 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 6ed7fdd57d..4cff8324c6 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; @@ -8,6 +9,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; @@ -88,6 +90,7 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic .setKnowledgeId(createListReqVO.getKnowledgeId()) .setContent(content).setContentLength(content.length()) .setTokens(tokenCountEstimator.estimate(content)) + .setSegmentMaxTokens(createListReqVO.getSegmentMaxTokens()) .setStatus(CommonStatusEnum.ENABLE.getStatus())); } knowledgeDocumentMapper.insertBatch(documentDOs); @@ -111,11 +114,38 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic @Override public void updateKnowledgeDocument(AiKnowledgeDocumentUpdateReqVO reqVO) { // 1. 校验文档是否存在 - validateKnowledgeDocumentExists(reqVO.getId()); + AiKnowledgeDocumentDO oldDocument = validateKnowledgeDocumentExists(reqVO.getId()); + // 2. 更新文档 AiKnowledgeDocumentDO document = BeanUtils.toBean(reqVO, AiKnowledgeDocumentDO.class); knowledgeDocumentMapper.updateById(document); - // TODO @芋艿:这里要处理状态的变更 + + // 3. 如果处于开启状态,并且最大 tokens 发生变化,则 segment 需要重新索引 + if (CommonStatusEnum.isEnable(oldDocument.getStatus()) + && reqVO.getSegmentMaxTokens() != null + && ObjUtil.notEqual(reqVO.getSegmentMaxTokens(), oldDocument.getSegmentMaxTokens())) { + // 删除旧的文档切片 + knowledgeSegmentService.deleteKnowledgeSegmentByDocumentId(reqVO.getId()); + // 重新创建文档切片 + knowledgeSegmentService.createKnowledgeSegmentBySplitContentAsync(reqVO.getId(), oldDocument.getContent()); + } + } + + @Override + public void updateKnowledgeDocumentStatus(AiKnowledgeDocumentUpdateStatusReqVO reqVO) { + // 1. 校验存在 + AiKnowledgeDocumentDO document = validateKnowledgeDocumentExists(reqVO.getId()); + + // 2. 更新状态 + knowledgeDocumentMapper.updateById(new AiKnowledgeDocumentDO() + .setId(reqVO.getId()).setStatus(reqVO.getStatus())); + + // 3. 处理文档切片 + if (CommonStatusEnum.isEnable(reqVO.getStatus())) { + knowledgeSegmentService.createKnowledgeSegmentBySplitContentAsync(reqVO.getId(), document.getContent()); + } else { + knowledgeSegmentService.deleteKnowledgeSegmentByDocumentId(reqVO.getId()); + } } @Override diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java index b105dcb8eb..137887af77 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java @@ -58,6 +58,13 @@ public interface AiKnowledgeSegmentService { */ void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO); + /** + * 根据文档编号删除段落 + * + * @param documentId 文档编号 + */ + void deleteKnowledgeSegmentByDocumentId(Long documentId); + /** * 召回段落 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index 37599b01fc..a20a18e8c3 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -115,6 +115,22 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService } } + @Override + public void deleteKnowledgeSegmentByDocumentId(Long documentId) { + // 1. 查询需要删除的段落 + List segments = segmentMapper.selectListByDocumentId(documentId); + if (CollUtil.isEmpty(segments)) { + return; + } + + // 2. 批量删除段落记录 + segmentMapper.deleteByIds(convertList(segments, AiKnowledgeSegmentDO::getId)); + + // 3. 删除向量存储中的段落 + VectorStore vectorStore = getVectorStoreById(segments.get(0).getKnowledgeId()); + vectorStore.delete(convertList(segments, AiKnowledgeSegmentDO::getVectorId)); + } + @Override public void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO) { // 1. 校验 From ebd93514b3cbb722dbb0a4f5b7c7490ffce0b417 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 1 Mar 2025 13:35:00 +0800 Subject: [PATCH 22/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=96=B0=E5=A2=9E=20document=20?= =?UTF-8?q?=E5=90=91=E9=87=8F=E7=9A=84=E8=BF=9B=E5=BA=A6=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AiKnowledgeSegmentController.http | 9 ++++++++- .../AiKnowledgeSegmentController.java | 10 ++++++++++ .../AiKnowledgeSegmentProcessRespVO.java | 19 +++++++++++++++++++ .../AiKnowledgeSegmentUpdateReqVO.java | 2 +- .../knowledge/AiKnowledgeSegmentMapper.java | 15 +++++++++++++++ .../AiKnowledgeDocumentServiceImpl.java | 4 ++-- .../knowledge/AiKnowledgeSegmentService.java | 9 +++++++++ .../AiKnowledgeSegmentServiceImpl.java | 13 +++++++++---- 8 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentProcessRespVO.java diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http index 25d622d4c1..eaeee5704f 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http @@ -2,4 +2,11 @@ GET {{baseUrl}}/ai/knowledge/segment/split?url=https://static.iocoder.cn/README_yudao.md&segmentMaxTokens=800 Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenantId}} \ No newline at end of file +tenant-id: {{adminTenantId}} + +### 获取文档处理列表 +GET {{baseUrl}}/ai/knowledge/segment/get-process-list?documentIds=1,2,3 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java index 2b21e0d1e4..d683c32d6a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowle import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentRespVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; import io.swagger.v3.oas.annotations.Operation; @@ -69,4 +70,13 @@ public class AiKnowledgeSegmentController { return success(BeanUtils.toBean(segments, AiKnowledgeSegmentRespVO.class)); } + @GetMapping("/get-process-list") + @Operation(summary = "获取文档处理列表") + @Parameter(name = "documentIds", description = "文档编号列表", required = true, example = "1,2,3") + public CommonResult> getKnowledgeSegmentProcessList( + @RequestParam("documentIds") List documentIds) { + List list = segmentService.getKnowledgeSegmentProcessList(documentIds); + return success(list); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentProcessRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentProcessRespVO.java new file mode 100644 index 0000000000..a6b95265b7 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentProcessRespVO.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - AI 知识库段落向量进度 Response VO") +@Data +public class AiKnowledgeSegmentProcessRespVO { + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long documentId; + + @Schema(description = "总段落数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long count; + + @Schema(description = "已向量化段落数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + private Long embeddingCount; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java index 23b1461e2d..2fc7a3141f 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; -@Schema(description = "管理后台 - AI 更新 知识库-段落 request VO") +@Schema(description = "管理后台 - AI 更新 知识库段落 request VO") @Data public class AiKnowledgeSegmentUpdateReqVO { diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java index 8381c84300..6f731811fe 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java @@ -3,11 +3,15 @@ package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; +import com.github.yulichang.wrapper.MPJLambdaWrapper; import org.apache.ibatis.annotations.Mapper; import java.util.List; +import java.util.Collection; /** * AI 知识库分片 Mapper @@ -37,4 +41,15 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX selectProcessList(Collection documentIds) { + MPJLambdaWrapper wrapper = new MPJLambdaWrapperX() + .selectAs(AiKnowledgeSegmentDO::getDocumentId, AiKnowledgeSegmentProcessRespVO::getDocumentId) + .selectCount(AiKnowledgeSegmentDO::getId, "count") + .select("COUNT(CASE WHEN vector_id > '" + AiKnowledgeSegmentDO.VECTOR_ID_EMPTY + + "' THEN 1 ELSE NULL END) AS embeddingCount") + .in(AiKnowledgeSegmentDO::getDocumentId, documentIds) + .groupBy(AiKnowledgeSegmentDO::getDocumentId); + return selectJoinList(AiKnowledgeSegmentProcessRespVO.class, wrapper); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 4cff8324c6..466b440cb7 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -67,8 +67,8 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic .setStatus(CommonStatusEnum.ENABLE.getStatus()); knowledgeDocumentMapper.insert(documentDO); - // 4. 文档切片入库(同步) - knowledgeSegmentService.createKnowledgeSegmentBySplitContent(documentDO.getId(), content); + // 4. 文档切片入库(异步) + knowledgeSegmentService.createKnowledgeSegmentBySplitContentAsync(documentDO.getId(), content); return documentDO.getId(); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java index 137887af77..a1a0b2d455 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowle import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import org.springframework.scheduling.annotation.Async; @@ -82,4 +83,12 @@ public interface AiKnowledgeSegmentService { */ List splitContent(String url, Integer segmentMaxTokens); + /** + * 获取文档处理进度(多个) + * + * @param documentIds 文档编号列表 + * @return 文档处理列表 + */ + List getKnowledgeSegmentProcessList(List documentIds); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index a20a18e8c3..0dce49f2be 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -6,10 +6,7 @@ import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.*; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; @@ -252,4 +249,12 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService .build(); } + @Override + public List getKnowledgeSegmentProcessList(List documentIds) { + if (CollUtil.isEmpty(documentIds)) { + return Collections.emptyList(); + } + return segmentMapper.selectProcessList(documentIds); + } + } From 5f5e77a3923bc39b3ef160195e00df5f3baee1aa Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 2 Mar 2025 20:54:02 +0800 Subject: [PATCH 23/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=96=B0=E5=A2=9E=20document=20?= =?UTF-8?q?=E5=90=91=E9=87=8F=E7=9A=84=E8=BF=9B=E5=BA=A6=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../knowledge/AiKnowledgeController.http | 6 +- .../AiKnowledgeDocumentController.http | 2 +- .../AiKnowledgeSegmentController.http | 9 +- .../AiKnowledgeSegmentController.java | 37 +++++- .../AiKnowledgeDocumentUpdateStatusReqVO.java | 22 ++++ .../AiKnowledgeSegmentSearchReqVO.java | 16 ++- .../AiKnowledgeSegmentSearchRespVO.java | 16 +++ .../dataobject/chat/AiChatConversationDO.java | 1 + .../knowledge/AiKnowledgeDocumentMapper.java | 9 ++ .../knowledge/AiKnowledgeSegmentMapper.java | 9 +- .../chat/AiChatMessageServiceImpl.java | 5 +- .../knowledge/AiKnowledgeDocumentService.java | 31 ++++- .../AiKnowledgeDocumentServiceImpl.java | 19 ++- .../knowledge/AiKnowledgeSegmentService.java | 13 +- .../AiKnowledgeSegmentServiceImpl.java | 43 +++++-- .../bo/AiKnowledgeSegmentSearchReqBO.java | 39 ++++++ .../bo/AiKnowledgeSegmentSearchRespBO.java | 45 +++++++ .../ai/service/model/AiApiKeyServiceImpl.java | 3 +- .../ai/core/factory/AiModelFactory.java | 3 +- .../ai/core/factory/AiModelFactoryImpl.java | 118 ++++++++++++++---- 20 files changed, 385 insertions(+), 61 deletions(-) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateStatusReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchRespVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchReqBO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchRespBO.java diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http index 235e880343..a0f127865a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.http @@ -9,7 +9,8 @@ tenant-id: {{adminTenantId}} "description": "测试描述", "embeddingModelId": 30, "topK": 3, - "similarityThreshold": 0.5 + "similarityThreshold": 0.5, + "status": 0 } ### 更新知识库 @@ -24,7 +25,8 @@ tenant-id: {{adminTenantId}} "description": "测试描述", "embeddingModelId": 30, "topK": 5, - "similarityThreshold": 0.6 + "similarityThreshold": 0.6, + "status": 0 } ### 获取知识库分页 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http index 22c1d91115..1c858ed3eb 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.http @@ -5,7 +5,7 @@ Authorization: Bearer {{token}} tenant-id: {{adminTenantId}} { - "knowledgeId": 1, + "knowledgeId": 2, "name": "测试文档", "url": "https://static.iocoder.cn/README.md", "segmentMaxTokens": 800 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http index eaeee5704f..09018da3dc 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.http @@ -4,9 +4,14 @@ Content-Type: application/json Authorization: Bearer {{token}} tenant-id: {{adminTenantId}} +### 搜索段落内容 +GET {{baseUrl}}/ai/knowledge/segment/search?knowledgeId=2&content=如何使用这个产品&topK=5&similarityThreshold=0.1 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenantId}} + ### 获取文档处理列表 GET {{baseUrl}}/ai/knowledge/segment/get-process-list?documentIds=1,2,3 Content-Type: application/json Authorization: Bearer {{token}} -tenant-id: {{adminTenantId}} - +tenant-id: {{adminTenantId}} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java index d683c32d6a..06b61708f0 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java @@ -1,15 +1,17 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentRespVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.*; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; @@ -20,9 +22,12 @@ import org.hibernate.validator.constraints.URL; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.Collections; import java.util.List; +import java.util.Map; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; // TODO @芋艿:增加权限标识 @Tag(name = "管理后台 - AI 知识库段落") @@ -34,6 +39,9 @@ public class AiKnowledgeSegmentController { @Resource private AiKnowledgeSegmentService segmentService; + @Resource + private AiKnowledgeDocumentService documentService; + @GetMapping("/page") @Operation(summary = "获取段落分页") public CommonResult> getKnowledgeSegmentPage( @@ -79,4 +87,23 @@ public class AiKnowledgeSegmentController { return success(list); } + @GetMapping("/search") + @Operation(summary = "搜索段落内容") + public CommonResult> searchKnowledgeSegment( + @Valid AiKnowledgeSegmentSearchReqVO reqVO) { + // 1. 搜索段落 + List segments = segmentService + .searchKnowledgeSegment(BeanUtils.toBean(reqVO, AiKnowledgeSegmentSearchReqBO.class)); + if (CollUtil.isEmpty(segments)) { + return success(Collections.emptyList()); + } + + // 2. 拼接 VO + Map documentMap = documentService.getKnowledgeDocumentMap(convertSet( + segments, AiKnowledgeSegmentSearchRespBO::getDocumentId)); + return success(BeanUtils.toBean(segments, AiKnowledgeSegmentSearchRespVO.class, + segment -> MapUtils.findAndThen(documentMap, segment.getDocumentId(), + document -> segment.setDocumentName(document.getName())))); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateStatusReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateStatusReqVO.java new file mode 100644 index 0000000000..93d393ab4a --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/document/AiKnowledgeDocumentUpdateStatusReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - AI 知识库文档更新状态 Request VO") +@Data +public class AiKnowledgeDocumentUpdateStatusReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchReqVO.java index 75349df628..3b3cd984b9 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchReqVO.java @@ -3,15 +3,25 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; -@Schema(description = "管理后台 - AI 知识库段落召回 Request VO") +@Schema(description = "管理后台 - AI 知识库段落搜索 Request VO") @Data public class AiKnowledgeSegmentSearchReqVO { - @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") + @Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "知识库编号不能为空") private Long knowledgeId; - @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 学习路线") + @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "如何使用这个产品") + @NotEmpty(message = "内容不能为空") private String content; + @Schema(description = "最大返回数量", example = "5") + private Integer topK; + + @Schema(description = "相似度阈值", example = "0.7") + private Double similarityThreshold; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchRespVO.java new file mode 100644 index 0000000000..50bbc5c867 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSearchRespVO.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - AI 知识库段落搜索 Response VO") +@Data +public class AiKnowledgeSegmentSearchRespVO extends AiKnowledgeSegmentRespVO { + + @Schema(description = "文档名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品使用手册") + private String documentName; + + @Schema(description = "相似度分数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.95") + private Double score; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java index cc8ae7d42a..b5e9d06541 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java @@ -65,6 +65,7 @@ public class AiChatConversationDO extends BaseDO { */ private Long roleId; + // TODO @芋艿:可优化,绑定多个知识库。前提,spring ai 支持 RerankModel 的封装 /** * 知识库编号 *

diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java index 8e6bb4d2b7..eeacca6345 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java @@ -5,8 +5,11 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import org.apache.ibatis.annotations.Mapper; +import java.util.Collection; + /** * AI 知识库文档 Mapper * @@ -22,4 +25,10 @@ public interface AiKnowledgeDocumentMapper extends BaseMapperX ids) { + update( new LambdaUpdateWrapper() + .setSql(" retrieval_count = retrieval_count + 1") + .in(AiKnowledgeDocumentDO::getId, ids)); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java index 6f731811fe..fab5572ceb 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java @@ -7,11 +7,12 @@ import cn.iocoder.yudao.framework.mybatis.core.query.MPJLambdaWrapperX; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.github.yulichang.wrapper.MPJLambdaWrapper; import org.apache.ibatis.annotations.Mapper; -import java.util.List; import java.util.Collection; +import java.util.List; /** * AI 知识库分片 Mapper @@ -52,4 +53,10 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX ids) { + update( new LambdaUpdateWrapper() + .setSql(" retrieval_count = retrieval_count + 1") + .in(AiKnowledgeSegmentDO::getId, ids)); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java index 8c4a08e1d0..1c8927e07a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -12,7 +12,6 @@ import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; @@ -133,7 +132,6 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { Flux streamResponse = chatModel.stream(prompt); // 3.4 流式返回 - // TODO 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题 StringBuffer contentBuffer = new StringBuffer(); return streamResponse.map(chunk -> { String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; @@ -159,7 +157,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { if (Objects.isNull(knowledgeId)) { return Collections.emptyList(); } - return knowledgeSegmentService.similaritySearch(new AiKnowledgeSegmentSearchReqVO().setKnowledgeId(knowledgeId).setContent(content)); +// return knowledgeSegmentService.similaritySearch(new AiKnowledgeSegmentSearchReqVO().setKnowledgeId(knowledgeId).setContent(content)); + return null; } private Prompt buildPrompt(AiChatConversationDO conversation, List messages,List segmentList, diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java index 4a45422d82..32720ed6a5 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -1,14 +1,18 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; +import java.util.Collection; import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; /** * AI 知识库文档 Service 接口 @@ -63,6 +67,13 @@ public interface AiKnowledgeDocumentService { */ void updateKnowledgeDocumentStatus(AiKnowledgeDocumentUpdateStatusReqVO reqVO); + /** + * 更新文档检索次数(增加 +1) + * + * @param ids 文档编号列表 + */ + void updateKnowledgeDocumentRetrievalCountIncr(Collection ids); + /** * 校验文档是否存在 * @@ -79,4 +90,22 @@ public interface AiKnowledgeDocumentService { */ String readUrl(String url); + /** + * 获取文档列表 + * + * @param ids 文档编号列表 + * @return 文档列表 + */ + List getKnowledgeDocumentList(Collection ids); + + /** + * 获取文档 Map + * + * @param ids 文档编号列表 + * @return 文档 Map + */ + default Map getKnowledgeDocumentMap(Collection ids) { + return convertMap(getKnowledgeDocumentList(ids), AiKnowledgeDocumentDO::getId); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 466b440cb7..a04bf80221 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -7,10 +7,10 @@ import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateStatusReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper; @@ -25,6 +25,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -148,6 +149,14 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic } } + @Override + public void updateKnowledgeDocumentRetrievalCountIncr(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + knowledgeDocumentMapper.updateRetrievalCountIncr(ids); + } + @Override public AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id) { AiKnowledgeDocumentDO knowledgeDocument = knowledgeDocumentMapper.selectById(id); @@ -182,4 +191,12 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic return document.getText(); } + @Override + public List getKnowledgeDocumentList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return new ArrayList<>(); + } + return knowledgeDocumentMapper.selectByIds(ids); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java index a1a0b2d455..ce79c8dd00 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java @@ -2,11 +2,12 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; import org.springframework.scheduling.annotation.Async; import java.util.List; @@ -67,12 +68,12 @@ public interface AiKnowledgeSegmentService { void deleteKnowledgeSegmentByDocumentId(Long documentId); /** - * 召回段落 + * 搜索知识库段落,并返回结果 * - * @param reqVO 召回请求信息 - * @return 召回的段落 + * @param reqBO 搜索请求信息 + * @return 搜索结果段落列表 */ - List similaritySearch(AiKnowledgeSegmentSearchReqVO reqVO); + List searchKnowledgeSegment(AiKnowledgeSegmentSearchReqBO reqBO); /** * 根据 URL 内容,切片创建多个段落 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index 0dce49f2be..eefad0bc8b 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -12,6 +13,8 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper; import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.document.Document; @@ -171,25 +174,45 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService } @Override - public List similaritySearch(AiKnowledgeSegmentSearchReqVO reqVO) { + public List searchKnowledgeSegment(AiKnowledgeSegmentSearchReqBO reqBO) { // 1. 校验 - AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqVO.getKnowledgeId()); + AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqBO.getKnowledgeId()); - // 2. 获取向量存储实例 + // 2.1 向量检索 VectorStore vectorStore = apiKeyService.getOrCreateVectorStoreByModelId(knowledge.getEmbeddingModelId()); - - // 3.1 向量检索 List documents = vectorStore.similaritySearch(SearchRequest.builder() - .query(reqVO.getContent()) - .topK(knowledge.getTopK()).similarityThreshold(knowledge.getSimilarityThreshold()) + .query(reqBO.getContent()) + .topK(ObjUtil.defaultIfNull(reqBO.getTopK(), knowledge.getTopK())) + .similarityThreshold(ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold())) .filterExpression(new FilterExpressionBuilder() - .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqVO.getKnowledgeId()).build()) + .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId()).build()) .build()); if (CollUtil.isEmpty(documents)) { return ListUtil.empty(); } - // 3.2 段落召回 - return segmentMapper.selectListByVectorIds(convertList(documents, Document::getId)); + // 2.2 段落召回 + List segments = segmentMapper + .selectListByVectorIds(convertList(documents, Document::getId)); + if (CollUtil.isEmpty(segments)) { + return ListUtil.empty(); + } + + // 3. 增加召回次数 + segmentMapper.updateRetrievalCountIncrByIds(convertList(segments, AiKnowledgeSegmentDO::getId)); + + // 4. 构建结果 + List result = convertList(segments, segment -> { + Document document = CollUtil.findOne(documents, // 找到对应的文档 + doc -> Objects.equals(doc.getId(), segment.getVectorId())); + if (document == null) { + return null; + } + return BeanUtils.toBean(segment, AiKnowledgeSegmentSearchRespBO.class) + .setScore(document.getScore()); + }); + result.sort((o1, o2) + -> Double.compare(o2.getScore(), o1.getScore())); // 按照分数降序排序 + return result; } @Override diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchReqBO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchReqBO.java new file mode 100644 index 0000000000..9ff63b6460 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchReqBO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.ai.service.knowledge.bo; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +import jakarta.validation.constraints.NotEmpty; + +/** + * AI 知识库段落搜索 Request BO + * + * @author 芋道源码 + */ +@Data +public class AiKnowledgeSegmentSearchReqBO { + + /** + * 知识库编号 + */ + @NotNull(message = "知识库编号不能为空") + private Long knowledgeId; + + /** + * 内容 + */ + @NotEmpty(message = "内容不能为空") + private String content; + + /** + * 最大返回数量 + */ + private Integer topK; + + /** + * 相似度阈值 + */ + private Double similarityThreshold; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchRespBO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchRespBO.java new file mode 100644 index 0000000000..72eb84624a --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/bo/AiKnowledgeSegmentSearchRespBO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.ai.service.knowledge.bo; + +import lombok.Data; + +/** + * AI 知识库段落搜索 Response BO + * + * @author 芋道源码 + */ +@Data +public class AiKnowledgeSegmentSearchRespBO { + + /** + * 段落编号 + */ + private Long id; + /** + * 文档编号 + */ + private Long documentId; + /** + * 知识库编号 + */ + private Long knowledgeId; + + /** + * 内容 + */ + private String content; + /** + * 内容长度 + */ + private Integer contentLength; + + /** + * Token 数量 + */ + private Integer tokens; + + /** + * 相似度分数 + */ + private Double score; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java index 1ef7640f29..38d6c95495 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java @@ -16,6 +16,7 @@ import jakarta.annotation.Resource; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; +import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @@ -154,7 +155,7 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { apiKey.getUrl(), chatModel.getModel()); // 创建或获取 VectorStore 对象 - return modelFactory.getOrCreateVectorStore(embeddingModel); + return modelFactory.getOrCreateVectorStore(SimpleVectorStore.class, embeddingModel); } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java index d4275d5e5d..3a8bf5e04d 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java @@ -99,9 +99,10 @@ public interface AiModelFactory { *

* 如果不存在,则进行创建 * + * @param type 向量存储类型 * @param embeddingModel 向量模型 * @return VectorStore 对象 */ - VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel); + VectorStore getOrCreateVectorStore(Class type, EmbeddingModel embeddingModel); } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index 779bd8e2fb..1f9b1f21a2 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -1,9 +1,11 @@ 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; @@ -24,6 +26,7 @@ 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 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; @@ -60,7 +63,11 @@ import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; import org.springframework.web.client.RestClient; +import java.io.File; +import java.time.Duration; import java.util.List; +import java.util.Timer; +import java.util.TimerTask; /** * AI Model 模型工厂的实现类 @@ -73,7 +80,7 @@ public class AiModelFactoryImpl implements AiModelFactory { public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url) { String cacheKey = buildClientCacheKey(ChatModel.class, platform, apiKey, url); return Singleton.get(cacheKey, (Func0) () -> { - //noinspection EnhancedSwitchMigration + // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: return buildTongYiChatModel(apiKey); @@ -105,7 +112,7 @@ public class AiModelFactoryImpl implements AiModelFactory { @Override public ChatModel getDefaultChatModel(AiPlatformEnum platform) { - //noinspection EnhancedSwitchMigration + // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: return SpringUtil.getBean(DashScopeChatModel.class); @@ -136,7 +143,7 @@ public class AiModelFactoryImpl implements AiModelFactory { @Override public ImageModel getDefaultImageModel(AiPlatformEnum platform) { - //noinspection EnhancedSwitchMigration + // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: return SpringUtil.getBean(DashScopeImageModel.class); @@ -155,7 +162,7 @@ public class AiModelFactoryImpl implements AiModelFactory { @Override public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url) { - //noinspection EnhancedSwitchMigration + // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: return buildTongYiImagesModel(apiKey); @@ -174,9 +181,11 @@ public class AiModelFactoryImpl implements AiModelFactory { @Override public MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url) { - String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, url); + String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, + url); return Singleton.get(cacheKey, (Func0) () -> { - YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class).getMidjourney(); + YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class) + .getMidjourney(); return new MidjourneyApi(url, apiKey, properties.getNotifyUrl()); }); } @@ -204,25 +213,31 @@ public class AiModelFactoryImpl implements AiModelFactory { } @Override - public VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel) { -// String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url); - String cacheKey = buildClientCacheKey(VectorStore.class, embeddingModel); + public VectorStore getOrCreateVectorStore(Class type, EmbeddingModel embeddingModel) { + // String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, + // url); + String cacheKey = buildClientCacheKey(VectorStore.class, embeddingModel, type); return Singleton.get(cacheKey, (Func0) () -> { + if (type == SimpleVectorStore.class) { + return buildSimpleVectorStore(embeddingModel); + } + throw new IllegalArgumentException(StrUtil.format("未知类型({})", type)); // TODO @芋艿:先临时使用 store - return SimpleVectorStore.builder(embeddingModel).build(); // TODO @芋艿:@xin:后续看看,是不是切到阿里云之类的 -// String prefix = StrUtil.format("{}#{}:", platform.getPlatform(), apiKey); -// var config = RedisVectorStore.RedisVectorStoreConfig.builder() -// .withIndexName(cacheKey) -// .withPrefix(prefix) -// .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC)) -// .build(); -// RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); -// RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel, -// new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), -// true); -// redisVectorStore.afterPropertiesSet(); -// return redisVectorStore; + // String prefix = StrUtil.format("{}#{}:", platform.getPlatform(), apiKey); + // var config = RedisVectorStore.RedisVectorStoreConfig.builder() + // .withIndexName(cacheKey) + // .withPrefix(prefix) + // .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", + // Schema.FieldType.NUMERIC)) + // .build(); + // RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); + // RedisVectorStore redisVectorStore = new RedisVectorStore(config, + // embeddingModel, + // new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), + // true); + // redisVectorStore.afterPropertiesSet(); + // return redisVectorStore; }); } @@ -307,7 +322,7 @@ public class AiModelFactoryImpl implements AiModelFactory { */ private ChatModel buildSiliconFlowChatModel(String apiKey) { YudaoAiProperties.SiliconFlowProperties properties = new YudaoAiProperties.SiliconFlowProperties() - .setApiKey(apiKey); + .setApiKey(apiKey); return new YudaoAiAutoConfiguration().buildSiliconFlowChatClient(properties); } @@ -397,7 +412,8 @@ public class AiModelFactoryImpl implements AiModelFactory { */ private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) { DashScopeApi dashScopeApi = new DashScopeApi(apiKey); - DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model).build(); + DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model) + .build(); return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions); } @@ -407,4 +423,58 @@ public class AiModelFactoryImpl implements AiModelFactory { return OllamaEmbeddingModel.builder().ollamaApi(ollamaApi).defaultOptions(ollamaOptions).build(); } + // ========== 各种创建 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; + } + + /** + * 创建向量存储文件 + * + * @param embeddingModel 嵌入模型 + * @return 向量存储文件 + */ + private File createVectorStoreFile(EmbeddingModel embeddingModel) { + // 获取简单类名 + String simpleClassName = embeddingModel.getClass().getSimpleName(); + // 获取用户主目录 + String userHome = FileUtil.getUserHomePath(); + // 创建vector_store目录 + File vectorStoreDir = new File(userHome, "vector_store"); + if (!vectorStoreDir.exists()) { + vectorStoreDir.mkdirs(); + } + + // 创建文件 + return new File(vectorStoreDir, "simple_" + simpleClassName + ".json"); + } + } From 3f460dc620b306f995768354cdc6be227072b947 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 2 Mar 2025 21:52:31 +0800 Subject: [PATCH 24/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=96=B0=E5=A2=9E=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E5=BA=93=E5=88=86=E6=AE=B5=E7=9A=84=E6=96=B0=E5=A2=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/enums/ErrorCodeConstants.java | 1 + .../knowledge/AiKnowledgeController.java | 5 ++ .../AiKnowledgeDocumentController.java | 16 ++++- .../AiKnowledgeSegmentController.java | 27 ++++++++- .../segment/AiKnowledgeSegmentPageReqVO.java | 11 ++-- .../AiKnowledgeSegmentUpdateReqVO.java | 17 ------ .../knowledge/AiKnowledgeSegmentMapper.java | 2 +- .../knowledge/AiKnowledgeDocumentService.java | 7 +++ .../AiKnowledgeDocumentServiceImpl.java | 13 +++++ .../knowledge/AiKnowledgeSegmentService.java | 20 ++++++- .../AiKnowledgeSegmentServiceImpl.java | 58 ++++++++++++++----- 11 files changed, 136 insertions(+), 41 deletions(-) delete mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index 5ac6d2ae9e..2b236e6f6a 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -60,5 +60,6 @@ public interface ErrorCodeConstants { ErrorCode KNOWLEDGE_DOCUMENT_FILE_READ_FAIL = new ErrorCode(1_022_008_102, "文档加载失败!"); ErrorCode KNOWLEDGE_SEGMENT_NOT_EXISTS = new ErrorCode(1_022_008_202, "段落不存在!"); + ErrorCode KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG = new ErrorCode(1_022_008_203, "内容 Token 数为 {},超过最大限制 {}"); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java index df49395750..f3dc6f022e 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java @@ -13,6 +13,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -30,6 +31,7 @@ public class AiKnowledgeController { @GetMapping("/page") @Operation(summary = "获取知识库分页") + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") public CommonResult> getKnowledgePage(@Valid AiKnowledgePageReqVO pageReqVO) { PageResult pageResult = knowledgeService.getKnowledgePage(pageReqVO); @@ -39,6 +41,7 @@ public class AiKnowledgeController { @GetMapping("/get") @Operation(summary = "获得知识库") @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") public CommonResult getKnowledge(@RequestParam("id") Long id) { AiKnowledgeDO knowledge = knowledgeService.getKnowledge(id); return success(BeanUtils.toBean(knowledge, AiKnowledgeRespVO.class)); @@ -46,12 +49,14 @@ public class AiKnowledgeController { @PostMapping("/create") @Operation(summary = "创建知识库") + @PreAuthorize("@ss.hasPermission('ai:knowledge:create')") public CommonResult createKnowledge(@RequestBody @Valid AiKnowledgeSaveReqVO createReqVO) { return success(knowledgeService.createKnowledge(createReqVO)); } @PutMapping("/update") @Operation(summary = "更新知识库") + @PreAuthorize("@ss.hasPermission('ai:knowledge:update')") public CommonResult updateKnowledge(@RequestBody @Valid AiKnowledgeSaveReqVO updateReqVO) { knowledgeService.updateKnowledge(updateReqVO); return success(true); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java index e828615ea1..cca856fe17 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java @@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -22,7 +23,6 @@ import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -// TODO @芋艿:增加权限标识 @Tag(name = "管理后台 - AI 知识库文档") @RestController @RequestMapping("/ai/knowledge/document") @@ -34,6 +34,7 @@ public class AiKnowledgeDocumentController { @GetMapping("/page") @Operation(summary = "获取文档分页") + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") public CommonResult> getKnowledgeDocumentPage( @Valid AiKnowledgeDocumentPageReqVO pageReqVO) { PageResult pageResult = documentService.getKnowledgeDocumentPage(pageReqVO); @@ -42,6 +43,7 @@ public class AiKnowledgeDocumentController { @GetMapping("/get") @Operation(summary = "获取文档详情") + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") public CommonResult getKnowledgeDocument(@RequestParam("id") Long id) { AiKnowledgeDocumentDO document = documentService.getKnowledgeDocument(id); return success(BeanUtils.toBean(document, AiKnowledgeDocumentRespVO.class)); @@ -49,6 +51,7 @@ public class AiKnowledgeDocumentController { @PostMapping("/create") @Operation(summary = "新建文档(单个)") + @PreAuthorize("@ss.hasPermission('ai:knowledge:create')") public CommonResult createKnowledgeDocument(@RequestBody @Valid AiKnowledgeDocumentCreateReqVO reqVO) { Long id = documentService.createKnowledgeDocument(reqVO); return success(id); @@ -56,6 +59,7 @@ public class AiKnowledgeDocumentController { @PostMapping("/create-list") @Operation(summary = "新建文档(多个)") + @PreAuthorize("@ss.hasPermission('ai:knowledge:create')") public CommonResult> createKnowledgeDocumentList( @RequestBody @Valid AiKnowledgeDocumentCreateListReqVO reqVO) { List ids = documentService.createKnowledgeDocumentList(reqVO); @@ -64,6 +68,7 @@ public class AiKnowledgeDocumentController { @PutMapping("/update") @Operation(summary = "更新文档") + @PreAuthorize("@ss.hasPermission('ai:knowledge:update')") public CommonResult updateKnowledgeDocument(@Valid @RequestBody AiKnowledgeDocumentUpdateReqVO reqVO) { documentService.updateKnowledgeDocument(reqVO); return success(true); @@ -71,10 +76,19 @@ public class AiKnowledgeDocumentController { @PutMapping("/update-status") @Operation(summary = "更新文档状态") + @PreAuthorize("@ss.hasPermission('ai:knowledge:update')") public CommonResult updateKnowledgeDocumentStatus( @Valid @RequestBody AiKnowledgeDocumentUpdateStatusReqVO reqVO) { documentService.updateKnowledgeDocumentStatus(reqVO); return success(true); } + @DeleteMapping("/delete") + @Operation(summary = "删除文档") + @PreAuthorize("@ss.hasPermission('ai:knowledge:delete')") + public CommonResult deleteKnowledgeDocument(@RequestParam("id") Long id) { + documentService.deleteKnowledgeDocument(id); + return success(true); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java index 06b61708f0..34f324491b 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeSegmentController.java @@ -19,6 +19,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.hibernate.validator.constraints.URL; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -29,7 +30,6 @@ import java.util.Map; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; -// TODO @芋艿:增加权限标识 @Tag(name = "管理后台 - AI 知识库段落") @RestController @RequestMapping("/ai/knowledge/segment") @@ -38,27 +38,45 @@ public class AiKnowledgeSegmentController { @Resource private AiKnowledgeSegmentService segmentService; - @Resource private AiKnowledgeDocumentService documentService; + @GetMapping("/get") + @Operation(summary = "获取段落详情") + @Parameter(name = "id", description = "段落编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") + public CommonResult getKnowledgeSegment(@RequestParam("id") Long id) { + AiKnowledgeSegmentDO segment = segmentService.getKnowledgeSegment(id); + return success(BeanUtils.toBean(segment, AiKnowledgeSegmentRespVO.class)); + } + @GetMapping("/page") @Operation(summary = "获取段落分页") + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") public CommonResult> getKnowledgeSegmentPage( @Valid AiKnowledgeSegmentPageReqVO pageReqVO) { PageResult pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO); return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class)); } + @PostMapping("/create") + @Operation(summary = "创建段落") + @PreAuthorize("@ss.hasPermission('ai:knowledge:create')") + public CommonResult createKnowledgeSegment(@Valid @RequestBody AiKnowledgeSegmentSaveReqVO createReqVO) { + return success(segmentService.createKnowledgeSegment(createReqVO)); + } + @PutMapping("/update") @Operation(summary = "更新段落内容") - public CommonResult updateKnowledgeSegment(@Valid @RequestBody AiKnowledgeSegmentUpdateReqVO reqVO) { + @PreAuthorize("@ss.hasPermission('ai:knowledge:update')") + public CommonResult updateKnowledgeSegment(@Valid @RequestBody AiKnowledgeSegmentSaveReqVO reqVO) { segmentService.updateKnowledgeSegment(reqVO); return success(true); } @PutMapping("/update-status") @Operation(summary = "启禁用段落内容") + @PreAuthorize("@ss.hasPermission('ai:knowledge:update')") public CommonResult updateKnowledgeSegmentStatus( @Valid @RequestBody AiKnowledgeSegmentUpdateStatusReqVO reqVO) { segmentService.updateKnowledgeSegmentStatus(reqVO); @@ -71,6 +89,7 @@ public class AiKnowledgeSegmentController { @Parameter(name = "url", description = "文档 URL", required = true), @Parameter(name = "segmentMaxTokens", description = "分段的最大 Token 数", required = true) }) + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") public CommonResult> splitContent( @RequestParam("url") @URL String url, @RequestParam(value = "segmentMaxTokens") Integer segmentMaxTokens) { @@ -81,6 +100,7 @@ public class AiKnowledgeSegmentController { @GetMapping("/get-process-list") @Operation(summary = "获取文档处理列表") @Parameter(name = "documentIds", description = "文档编号列表", required = true, example = "1,2,3") + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") public CommonResult> getKnowledgeSegmentProcessList( @RequestParam("documentIds") List documentIds) { List list = segmentService.getKnowledgeSegmentProcessList(documentIds); @@ -89,6 +109,7 @@ public class AiKnowledgeSegmentController { @GetMapping("/search") @Operation(summary = "搜索段落内容") + @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") public CommonResult> searchKnowledgeSegment( @Valid AiKnowledgeSegmentSearchReqVO reqVO) { // 1. 搜索段落 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java index 8be3db501b..f53d5be076 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentPageReqVO.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -8,13 +10,14 @@ import lombok.Data; @Data public class AiKnowledgeSegmentPageReqVO extends PageParam { - @Schema(description = "分段状态", example = "1") - private Integer status; - @Schema(description = "文档编号", example = "1") private Integer documentId; @Schema(description = "分段内容关键字", example = "Java 开发") - private String keyword; + private String content; + + @Schema(description = "分段状态", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java deleted file mode 100644 index 2fc7a3141f..0000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentUpdateReqVO.java +++ /dev/null @@ -1,17 +0,0 @@ -package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - - -@Schema(description = "管理后台 - AI 更新 知识库段落 request VO") -@Data -public class AiKnowledgeSegmentUpdateReqVO { - - @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") - private Long id; - - @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") - private String content; - -} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java index fab5572ceb..a2a4e15b6d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java @@ -25,8 +25,8 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX selectPage(AiKnowledgeSegmentPageReqVO reqVO) { return selectPage(reqVO, new LambdaQueryWrapperX() .eq(AiKnowledgeSegmentDO::getDocumentId, reqVO.getDocumentId()) + .likeIfPresent(AiKnowledgeSegmentDO::getContent, reqVO.getContent()) .eqIfPresent(AiKnowledgeSegmentDO::getStatus, reqVO.getStatus()) - .likeIfPresent(AiKnowledgeSegmentDO::getContent, reqVO.getKeyword()) .orderByDesc(AiKnowledgeSegmentDO::getId)); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java index 32720ed6a5..8ff137b331 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -74,6 +74,13 @@ public interface AiKnowledgeDocumentService { */ void updateKnowledgeDocumentRetrievalCountIncr(Collection ids); + /** + * 删除文档 + * + * @param id 文档编号 + */ + void deleteKnowledgeDocument(Long id); + /** * 校验文档是否存在 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index a04bf80221..b1513a9bd4 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -149,6 +149,19 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic } } + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteKnowledgeDocument(Long id) { + // 1. 校验存在 + validateKnowledgeDocumentExists(id); + + // 2. 删除 + knowledgeDocumentMapper.deleteById(id); + + // 3. 删除对应的段落 + knowledgeSegmentService.deleteKnowledgeSegmentByDocumentId(id); + } + @Override public void updateKnowledgeDocumentRetrievalCountIncr(Collection ids) { if (CollUtil.isEmpty(ids)) { diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java index ce79c8dd00..15ab941fe8 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSaveReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; @@ -19,6 +19,14 @@ import java.util.List; */ public interface AiKnowledgeSegmentService { + /** + * 获取知识库段落详情 + * + * @param id 段落编号 + * @return 段落详情 + */ + AiKnowledgeSegmentDO getKnowledgeSegment(Long id); + /** * 获取段落分页 * @@ -46,12 +54,20 @@ public interface AiKnowledgeSegmentService { createKnowledgeSegmentBySplitContent(documentId, content); } + /** + * 创建知识库段落 + * + * @param createReqVO 创建信息 + * @return 段落编号 + */ + Long createKnowledgeSegment(AiKnowledgeSegmentSaveReqVO createReqVO); + /** * 更新段落的内容 * * @param reqVO 更新内容 */ - void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO); + void updateKnowledgeSegment(AiKnowledgeSegmentSaveReqVO reqVO); /** * 更新段落的状态 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index eefad0bc8b..adf1616cb0 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -34,6 +34,7 @@ import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG; /** * AI 知识库分片 Service 实现类 @@ -98,20 +99,20 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService } @Override - public void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO) { + public void updateKnowledgeSegment(AiKnowledgeSegmentSaveReqVO reqVO) { // 1. 校验 - AiKnowledgeSegmentDO segment = validateKnowledgeSegmentExists(reqVO.getId()); + AiKnowledgeSegmentDO oldSegment = validateKnowledgeSegmentExists(reqVO.getId()); // 2. 删除向量 - VectorStore vectorStore = getVectorStoreById(segment.getKnowledgeId()); - deleteVectorStore(vectorStore, segment); + VectorStore vectorStore = getVectorStoreById(oldSegment.getKnowledgeId()); + deleteVectorStore(vectorStore, oldSegment); // 3.1 更新切片 - AiKnowledgeSegmentDO segmentDO = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class); - segmentMapper.updateById(segmentDO); + AiKnowledgeSegmentDO newSegment = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class); + segmentMapper.updateById(newSegment); // 3.2 重新向量化,必须开启状态 - if (CommonStatusEnum.isEnable(segmentDO.getStatus())) { - writeVectorStore(vectorStore, segmentDO, new Document(segmentDO.getContent())); + if (CommonStatusEnum.isEnable(oldSegment.getStatus())) { + writeVectorStore(vectorStore, newSegment, new Document(newSegment.getContent())); } } @@ -143,7 +144,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService segmentMapper.updateById(new AiKnowledgeSegmentDO().setId(reqVO.getId()).setStatus(reqVO.getStatus())); // 4. 更新向量 - if (Objects.equals(reqVO.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + if (CommonStatusEnum.isEnable(reqVO.getStatus())) { writeVectorStore(vectorStore, segment, new Document(segment.getContent())); } else { deleteVectorStore(vectorStore, segment); @@ -183,7 +184,8 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService List documents = vectorStore.similaritySearch(SearchRequest.builder() .query(reqBO.getContent()) .topK(ObjUtil.defaultIfNull(reqBO.getTopK(), knowledge.getTopK())) - .similarityThreshold(ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold())) + .similarityThreshold( + ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold())) .filterExpression(new FilterExpressionBuilder() .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId()).build()) .build()); @@ -202,7 +204,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService // 4. 构建结果 List result = convertList(segments, segment -> { - Document document = CollUtil.findOne(documents, // 找到对应的文档 + Document document = CollUtil.findOne(documents, // 找到对应的文档 doc -> Objects.equals(doc.getId(), segment.getVectorId())); if (document == null) { return null; @@ -210,8 +212,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService return BeanUtils.toBean(segment, AiKnowledgeSegmentSearchRespBO.class) .setScore(document.getScore()); }); - result.sort((o1, o2) - -> Double.compare(o2.getScore(), o1.getScore())); // 按照分数降序排序 + result.sort((o1, o2) -> Double.compare(o2.getScore(), o1.getScore())); // 按照分数降序排序 return result; } @@ -280,4 +281,35 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService return segmentMapper.selectProcessList(documentIds); } + @Override + public Long createKnowledgeSegment(AiKnowledgeSegmentSaveReqVO createReqVO) { + // 1.1 校验文档是否存在 + AiKnowledgeDocumentDO document = knowledgeDocumentService + .validateKnowledgeDocumentExists(createReqVO.getDocumentId()); + // 1.2 获取知识库信息 + AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(document.getKnowledgeId()); + // 1.3 校验 token 熟练 + Integer tokens = tokenCountEstimator.estimate(createReqVO.getContent()); + if (tokens > document.getSegmentMaxTokens()) { + throw exception(KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG, tokens, document.getSegmentMaxTokens()); + } + + // 2. 保存段落 + AiKnowledgeSegmentDO segment = BeanUtils.toBean(createReqVO, AiKnowledgeSegmentDO.class) + .setKnowledgeId(knowledge.getId()).setDocumentId(document.getId()) + .setContentLength(createReqVO.getContent().length()).setTokens(tokens) + .setVectorId(AiKnowledgeSegmentDO.VECTOR_ID_EMPTY) + .setRetrievalCount(0).setStatus(CommonStatusEnum.ENABLE.getStatus()); + segmentMapper.insert(segment); + + // 3. 向量化 + writeVectorStore(getVectorStoreById(knowledge), segment, new Document(segment.getContent())); + return segment.getId(); + } + + @Override + public AiKnowledgeSegmentDO getKnowledgeSegment(Long id) { + return validateKnowledgeSegmentExists(id); + } + } From 89d079349c987eaa1065883433cd0e2ab6fc5bfe Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 3 Mar 2025 21:26:31 +0800 Subject: [PATCH 25/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E3=80=91AI=EF=BC=9A=E2=80=9C=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E2=80=9D=E9=87=8D=E6=9E=84=E4=B8=BA=E2=80=9C?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E2=80=9D=EF=BC=8C=E6=94=AF=E6=8C=81=20type?= =?UTF-8?q?=20=E6=A8=A1=E5=9E=8B=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/enums/ErrorCodeConstants.java | 14 +- .../AiChatConversationRespVO.java | 4 +- .../admin/image/vo/AiImageDrawReqVO.java | 9 +- .../admin/model/AiApiKeyController.java | 7 +- .../admin/model/AiChatModelController.java | 84 ----------- .../admin/model/AiModelController.java | 89 ++++++++++++ .../model/vo/chatRole/AiChatRoleRespVO.java | 4 +- .../AiModelPageReqVO.java} | 6 +- .../AiModelRespVO.java} | 9 +- .../AiModelSaveReqVO.java} | 19 ++- .../dataobject/chat/AiChatConversationDO.java | 6 +- .../dal/dataobject/chat/AiChatMessageDO.java | 6 +- .../ai/dal/dataobject/image/AiImageDO.java | 13 +- .../dataobject/knowledge/AiKnowledgeDO.java | 6 +- .../dal/dataobject/mindmap/AiMindMapDO.java | 8 +- .../ai/dal/dataobject/model/AiChatRoleDO.java | 2 +- .../{AiChatModelDO.java => AiModelDO.java} | 18 ++- .../ai/dal/dataobject/write/AiWriteDO.java | 17 ++- .../ai/dal/mysql/model/AiChatMapper.java | 47 +++++++ .../ai/dal/mysql/model/AiChatModelMapper.java | 43 ------ .../chat/AiChatConversationServiceImpl.java | 19 +-- .../chat/AiChatMessageServiceImpl.java | 23 ++- .../ai/service/image/AiImageServiceImpl.java | 72 ++++++---- .../AiKnowledgeSegmentServiceImpl.java | 18 ++- .../knowledge/AiKnowledgeServiceImpl.java | 10 +- .../service/mindmap/AiMindMapServiceImpl.java | 28 ++-- .../ai/service/model/AiApiKeyService.java | 51 +------ .../ai/service/model/AiApiKeyServiceImpl.java | 74 +--------- .../ai/service/model/AiChatModelService.java | 92 ------------ .../service/model/AiChatModelServiceImpl.java | 128 ++++++++++++----- .../ai/service/model/AiModelService.java | 131 ++++++++++++++++++ .../ai/service/music/AiMusicServiceImpl.java | 8 +- .../ai/service/write/AiWriteServiceImpl.java | 32 ++--- .../ai/core/enums/AiModelTypeEnum.java | 41 ++++++ .../ai/core/enums/AiPlatformEnum.java | 12 +- .../ai/core/factory/AiModelFactoryImpl.java | 21 --- 36 files changed, 617 insertions(+), 554 deletions(-) delete mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatModelController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiModelController.java rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/{chatModel/AiChatModelPageReqVO.java => model/AiModelPageReqVO.java} (67%) rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/{chatModel/AiChatModelRespVO.java => model/AiModelRespVO.java} (83%) rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/{chatModel/AiChatModelSaveReqVO.java => model/AiModelSaveReqVO.java} (71%) rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/{AiChatModelDO.java => AiModelDO.java} (77%) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatMapper.java delete mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatModelMapper.java delete mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiModelTypeEnum.java diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index 2b236e6f6a..205fc80b10 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -12,31 +12,25 @@ public interface ErrorCodeConstants { // ========== API 密钥 1-040-000-000 ========== ErrorCode API_KEY_NOT_EXISTS = new ErrorCode(1_040_000_000, "API 密钥不存在"); ErrorCode API_KEY_DISABLE = new ErrorCode(1_040_000_001, "API 密钥已禁用!"); - ErrorCode API_KEY_MIDJOURNEY_NOT_FOUND = new ErrorCode(1_040_000_900, "Midjourney 模型不存在"); - ErrorCode API_KEY_SUNO_NOT_FOUND = new ErrorCode(1_040_000_901, "Suno 模型不存在"); - ErrorCode API_KEY_IMAGE_NODE_FOUND = new ErrorCode(1_040_000_902, "平台({}) 图片模型未配置"); - // ========== API 聊天模型 1-040-001-000 ========== - ErrorCode CHAT_MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "模型不存在!"); - ErrorCode CHAT_MODEL_DISABLE = new ErrorCode(1_040_001_001, "模型({})已禁用!"); - ErrorCode CHAT_MODEL_DEFAULT_NOT_EXISTS = new ErrorCode(1_040_001_002, "操作失败,找不到默认聊天模型"); + // ========== API 模型 1-040-001-000 ========== + ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "模型不存在!"); + ErrorCode MODEL_DISABLE = new ErrorCode(1_040_001_001, "模型({})已禁用!"); + ErrorCode MODEL_DEFAULT_NOT_EXISTS = new ErrorCode(1_040_001_002, "操作失败,找不到默认模型"); // ========== API 聊天角色 1-040-002-000 ========== ErrorCode CHAT_ROLE_NOT_EXISTS = new ErrorCode(1_040_002_000, "聊天角色不存在"); ErrorCode CHAT_ROLE_DISABLE = new ErrorCode(1_040_001_001, "聊天角色({})已禁用!"); // ========== API 聊天会话 1-040-003-000 ========== - ErrorCode CHAT_CONVERSATION_NOT_EXISTS = new ErrorCode(1_040_003_000, "对话不存在!"); ErrorCode CHAT_CONVERSATION_MODEL_ERROR = new ErrorCode(1_040_003_001, "操作失败,该聊天模型的配置不完整"); // ========== API 聊天消息 1-040-004-000 ========== - ErrorCode CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_040_004_000, "消息不存在!"); ErrorCode CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "对话生成异常!"); // ========== API 绘画 1-040-005-000 ========== - ErrorCode IMAGE_NOT_EXISTS = new ErrorCode(1_022_005_000, "图片不存在!"); ErrorCode IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_022_005_001, "Midjourney 提交失败!原因:{}"); ErrorCode IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_022_005_002, "Midjourney 按钮 customId 不存在! {}"); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java index 66eb24db5f..7da37ebc9b 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import com.fhs.core.trans.anno.Trans; import com.fhs.core.trans.constant.TransType; @@ -31,7 +31,7 @@ public class AiChatConversationRespVO implements VO { private Long roleId; @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @Trans(type = TransType.SIMPLE, target = AiChatModelDO.class, fields = "name", ref = "modelName") + @Trans(type = TransType.SIMPLE, target = AiModelDO.class, fields = "name", ref = "modelName") private Long modelId; @Schema(description = "模型标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "ERNIE-Bot-turbo-0922") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java index a38935ef71..02225ea496 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java @@ -14,18 +14,15 @@ import java.util.Map; @Data public class AiImageDrawReqVO { - @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI") - private String platform; // 参见 AiPlatformEnum 枚举 + @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "模型编号不能为空") + private Long modelId; @Schema(description = "提示词", requiredMode = Schema.RequiredMode.REQUIRED, example = "画一个长城") @NotEmpty(message = "提示词不能为空") @Size(max = 1200, message = "提示词最大 1200") private String prompt; - @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "stable-diffusion-v1-6") - @NotEmpty(message = "模型不能为空") - private String model; - /** * 1. dall-e-2 模型:256x256、512x512、1024x1024 * 2. dall-e-3 模型:1024x1024, 1792x1024, 或 1024x1792 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java index 2bc190051d..c109b033ca 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java @@ -6,9 +6,8 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyRespVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelRespVO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -76,9 +75,9 @@ public class AiApiKeyController { @GetMapping("/simple-list") @Operation(summary = "获得 API 密钥分页列表") - public CommonResult> getApiKeySimpleList() { + public CommonResult> getApiKeySimpleList() { List list = apiKeyService.getApiKeyList(); - return success(convertList(list, key -> new AiChatModelRespVO().setId(key.getId()).setName(key.getName()))); + return success(convertList(list, key -> new AiModelRespVO().setId(key.getId()).setName(key.getName()))); } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatModelController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatModelController.java deleted file mode 100644 index 08a53b286b..0000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatModelController.java +++ /dev/null @@ -1,84 +0,0 @@ -package cn.iocoder.yudao.module.ai.controller.admin.model; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelRespVO; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelSaveReqVO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; -import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import jakarta.validation.Valid; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; - -@Tag(name = "管理后台 - AI 聊天模型") -@RestController -@RequestMapping("/ai/chat-model") -@Validated -public class AiChatModelController { - - @Resource - private AiChatModelService chatModelService; - - @PostMapping("/create") - @Operation(summary = "创建聊天模型") - @PreAuthorize("@ss.hasPermission('ai:chat-model:create')") - public CommonResult createChatModel(@Valid @RequestBody AiChatModelSaveReqVO createReqVO) { - return success(chatModelService.createChatModel(createReqVO)); - } - - @PutMapping("/update") - @Operation(summary = "更新聊天模型") - @PreAuthorize("@ss.hasPermission('ai:chat-model:update')") - public CommonResult updateChatModel(@Valid @RequestBody AiChatModelSaveReqVO updateReqVO) { - chatModelService.updateChatModel(updateReqVO); - return success(true); - } - - @DeleteMapping("/delete") - @Operation(summary = "删除聊天模型") - @Parameter(name = "id", description = "编号", required = true) - @PreAuthorize("@ss.hasPermission('ai:chat-model:delete')") - public CommonResult deleteChatModel(@RequestParam("id") Long id) { - chatModelService.deleteChatModel(id); - return success(true); - } - - @GetMapping("/get") - @Operation(summary = "获得聊天模型") - @Parameter(name = "id", description = "编号", required = true, example = "1024") - @PreAuthorize("@ss.hasPermission('ai:chat-model:query')") - public CommonResult getChatModel(@RequestParam("id") Long id) { - AiChatModelDO chatModel = chatModelService.getChatModel(id); - return success(BeanUtils.toBean(chatModel, AiChatModelRespVO.class)); - } - - @GetMapping("/page") - @Operation(summary = "获得聊天模型分页") - @PreAuthorize("@ss.hasPermission('ai:chat-model:query')") - public CommonResult> getChatModelPage(@Valid AiChatModelPageReqVO pageReqVO) { - PageResult pageResult = chatModelService.getChatModelPage(pageReqVO); - return success(BeanUtils.toBean(pageResult, AiChatModelRespVO.class)); - } - - @GetMapping("/simple-list") - @Operation(summary = "获得聊天模型列表") - @Parameter(name = "status", description = "状态", required = true, example = "1") - public CommonResult> getChatModelSimpleList(@RequestParam("status") Integer status) { - List list = chatModelService.getChatModelListByStatus(status); - return success(convertList(list, model -> new AiChatModelRespVO().setId(model.getId()) - .setName(model.getName()).setModel(model.getModel()))); - } - -} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiModelController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiModelController.java new file mode 100644 index 0000000000..86dd4d0a61 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiModelController.java @@ -0,0 +1,89 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - AI 模型") +@RestController +@RequestMapping("/ai/model") +@Validated +public class AiModelController { + + @Resource + private AiModelService modelService; + + @PostMapping("/create") + @Operation(summary = "创建模型") + @PreAuthorize("@ss.hasPermission('ai:model:create')") + public CommonResult createModel(@Valid @RequestBody AiModelSaveReqVO createReqVO) { + return success(modelService.createModel(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新模型") + @PreAuthorize("@ss.hasPermission('ai:model:update')") + public CommonResult updateModel(@Valid @RequestBody AiModelSaveReqVO updateReqVO) { + modelService.updateModel(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除模型") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('ai:model:delete')") + public CommonResult deleteModel(@RequestParam("id") Long id) { + modelService.deleteModel(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得模型") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:model:query')") + public CommonResult getModel(@RequestParam("id") Long id) { + AiModelDO model = modelService.getModel(id); + return success(BeanUtils.toBean(model, AiModelRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得模型分页") + @PreAuthorize("@ss.hasPermission('ai:model:query')") + public CommonResult> getModelPage(@Valid AiModelPageReqVO pageReqVO) { + PageResult pageResult = modelService.getModelPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiModelRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得模型列表") + @Parameter(name = "type", description = "类型", required = true, example = "1") + @Parameter(name = "platform", description = "平台", example = "midjourney") + public CommonResult> getModelSimpleList( + @RequestParam("type") Integer type, + @RequestParam(value = "platform", required = false) String platform) { + List list = modelService.getModelListByStatusAndType( + CommonStatusEnum.ENABLE.getStatus(), type, platform); + return success(convertList(list, model -> new AiModelRespVO().setId(model.getId()) + .setName(model.getName()).setModel(model.getModel()).setPlatform(model.getPlatform()))); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java index eb34da2748..d47c66d2bd 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import com.fhs.core.trans.anno.Trans; import com.fhs.core.trans.constant.TransType; import com.fhs.core.trans.vo.VO; @@ -20,7 +20,7 @@ public class AiChatRoleRespVO implements VO { private Long userId; @Schema(description = "模型编号", example = "17640") - @Trans(type = TransType.SIMPLE, target = AiChatModelDO.class, fields = {"name", "model"}, refs = {"modelName", "model"}) + @Trans(type = TransType.SIMPLE, target = AiModelDO.class, fields = {"name", "model"}, refs = {"modelName", "model"}) private Long modelId; @Schema(description = "模型名字", example = "张三") private String modelName; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelPageReqVO.java similarity index 67% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelPageReqVO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelPageReqVO.java index ce2f83b4b1..af8d1121a4 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelPageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelPageReqVO.java @@ -1,12 +1,12 @@ -package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel; +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.model; import lombok.*; import io.swagger.v3.oas.annotations.media.Schema; import cn.iocoder.yudao.framework.common.pojo.PageParam; -@Schema(description = "管理后台 - API 聊天模型分页 Request VO") +@Schema(description = "管理后台 - API 模型分页 Request VO") @Data -public class AiChatModelPageReqVO extends PageParam { +public class AiModelPageReqVO extends PageParam { @Schema(description = "模型名字", example = "张三") private String name; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelRespVO.java similarity index 83% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelRespVO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelRespVO.java index 681dabe687..b50b70a087 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelRespVO.java @@ -1,13 +1,13 @@ -package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel; +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.model; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; -@Schema(description = "管理后台 - AI 聊天模型 Response VO") +@Schema(description = "管理后台 - AI 模型 Response VO") @Data -public class AiChatModelRespVO { +public class AiModelRespVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2630") private Long id; @@ -24,6 +24,9 @@ public class AiChatModelRespVO { @Schema(description = "模型平台", example = "OpenAI") private String platform; + @Schema(description = "模型类型", example = "1") + private Integer type; + @Schema(description = "排序", example = "1") private Integer sort; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelSaveReqVO.java similarity index 71% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelSaveReqVO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelSaveReqVO.java index 4fad5a1fc1..281bcd6732 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelSaveReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/model/AiModelSaveReqVO.java @@ -1,14 +1,17 @@ -package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel; +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.model; +import cn.iocoder.yudao.framework.ai.core.enums.AiModelTypeEnum; +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.validation.InEnum; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.*; -import jakarta.validation.constraints.*; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; -@Schema(description = "管理后台 - API 聊天模型新增/修改 Request VO") +@Schema(description = "管理后台 - API 模型新增/修改 Request VO") @Data -public class AiChatModelSaveReqVO { +public class AiModelSaveReqVO { @Schema(description = "编号", example = "2630") private Long id; @@ -27,8 +30,14 @@ public class AiChatModelSaveReqVO { @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI") @NotEmpty(message = "模型平台不能为空") + @InEnum(AiPlatformEnum.class) private String platform; + @Schema(description = "模型类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "模型类型不能为空") + @InEnum(AiModelTypeEnum.class) + private Integer type; + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "排序不能为空") private Integer sort; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java index b5e9d06541..a9c956deae 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; @@ -76,13 +76,13 @@ public class AiChatConversationDO extends BaseDO { /** * 模型编号 * - * 关联 {@link AiChatModelDO#getId()} 字段 + * 关联 {@link AiModelDO#getId()} 字段 */ private Long modelId; /** * 模型标志 * - * 冗余 {@link AiChatModelDO#getModel()} 字段 + * 冗余 {@link AiModelDO#getModel()} 字段 */ private String model; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java index c65360ccbb..a026ec8193 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; @@ -83,13 +83,13 @@ public class AiChatMessageDO extends BaseDO { /** * 模型标志 * - * 冗余 {@link AiChatModelDO#getModel()} + * 冗余 {@link AiModelDO#getModel()} */ private String model; /** * 模型编号 * - * 关联 {@link AiChatModelDO#getId()} 字段 + * 关联 {@link AiModelDO#getId()} 字段 */ private Long modelId; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java index 5b43bc91e9..a18904c022 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.image; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import com.baomidou.mybatisplus.annotation.KeySequence; @@ -52,11 +52,16 @@ public class AiImageDO extends BaseDO { * 枚举 {@link cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum} */ private String platform; - // TODO @芋艿:modelId? /** - * 模型 + * 模型编号 * - * 冗余 {@link AiChatModelDO#getModel()} + * 关联 {@link AiModelDO#getId()} + */ + private Long modelId; + /** + * 模型标识 + * + * 冗余 {@link AiModelDO#getModel()} */ private String model; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java index 732d54e24a..e1327a50ef 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/knowledge/AiKnowledgeDO.java @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -35,13 +35,13 @@ public class AiKnowledgeDO extends BaseDO { /** * 向量模型编号 * - * 关联 {@link AiChatModelDO#getId()} + * 关联 {@link AiModelDO#getId()} */ private Long embeddingModelId; /** * 模型标识 * - * 冗余 {@link AiChatModelDO#getModel()} + * 冗余 {@link AiModelDO#getModel()} */ private String embeddingModel; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java index 47aa0ce4a5..6dd5d44302 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.mindmap; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -36,7 +37,12 @@ public class AiMindMapDO extends BaseDO { * 枚举 {@link AiPlatformEnum} */ private String platform; - // TODO @芋艿:modelId? + /** + * 模型编号 + * + * 关联 {@link AiModelDO#getId()} + */ + private Long modelId; /** * 模型 */ diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java index f5ed533a92..fd35b85795 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java @@ -58,7 +58,7 @@ public class AiChatRoleDO extends BaseDO { /** * 模型编号 * - * 关联 {@link AiChatModelDO#getId()} 字段 + * 关联 {@link AiModelDO#getId()} 字段 */ private Long modelId; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatModelDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java similarity index 77% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatModelDO.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java index c3800bd4ca..f4ff09212a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatModelDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.model; +import cn.iocoder.yudao.framework.ai.core.enums.AiModelTypeEnum; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; @@ -8,23 +9,22 @@ import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; -// TODO @芋艿,需要改造,增加 type /** - * AI 聊天模型 DO + * AI 模型 DO * - * 默认聊天模型:{@link #status} 为开启,并且 {@link #sort} 排序第一 + * 默认模型:{@link #status} 为开启,并且 {@link #sort} 排序第一 * * @author fansili * @since 2024/4/24 19:39 */ -@TableName("ai_chat_model") -@KeySequence("ai_chat_model_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@TableName("ai_model") +@KeySequence("ai_model_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor -public class AiChatModelDO extends BaseDO { +public class AiModelDO extends BaseDO { /** * 编号 @@ -51,6 +51,12 @@ public class AiChatModelDO extends BaseDO { * 枚举 {@link AiPlatformEnum} */ private String platform; + /** + * 类型 + * + * 枚举 {@link AiModelTypeEnum} + */ + private Integer type; /** * 排序值 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java index a99f94e82d..e07f994aad 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java @@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.write; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; +import cn.iocoder.yudao.module.ai.enums.DictTypeConstants; import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; @@ -44,7 +46,12 @@ public class AiWriteDO extends BaseDO { * 枚举 {@link AiPlatformEnum} */ private String platform; - // TODO @芋艿:modelId? + /** + * 模型编号 + * + * 关联 {@link AiModelDO#getId()} + */ + private Long modelId; /** * 模型 */ @@ -67,25 +74,25 @@ public class AiWriteDO extends BaseDO { /** * 长度提示词 * - * 字典:{@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_LENGTH} + * 字典:{@link DictTypeConstants#AI_WRITE_LENGTH} */ private Integer length; /** * 格式提示词 * - * 字典:{@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_FORMAT} + * 字典:{@link DictTypeConstants#AI_WRITE_FORMAT} */ private Integer format; /** * 语气提示词 * - * 字典:{@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_TONE} + * 字典:{@link DictTypeConstants#AI_WRITE_TONE} */ private Integer tone; /** * 语言提示词 * - * 字典:{@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_LANGUAGE} + * 字典:{@link DictTypeConstants#AI_WRITE_LANGUAGE} */ private Integer language; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatMapper.java new file mode 100644 index 0000000000..bfe2caf52a --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatMapper.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.model; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; +import org.apache.ibatis.annotations.Mapper; + +import javax.annotation.Nullable; +import java.util.List; + +/** + * API 模型 Mapper + * + * @author fansili + */ +@Mapper +public interface AiChatMapper extends BaseMapperX { + + default AiModelDO selectFirstByStatus(Integer type, Integer status) { + return selectOne(new QueryWrapperX() + .eq("type", type) + .eq("status", status) + .limitN(1) + .orderByAsc("sort")); + } + + default PageResult selectPage(AiModelPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(AiModelDO::getName, reqVO.getName()) + .eqIfPresent(AiModelDO::getModel, reqVO.getModel()) + .eqIfPresent(AiModelDO::getPlatform, reqVO.getPlatform()) + .orderByAsc(AiModelDO::getSort)); + } + + default List selectListByStatusAndType(Integer status, Integer type, + @Nullable String platform) { + return selectList(new LambdaQueryWrapperX() + .eq(AiModelDO::getStatus, status) + .eq(AiModelDO::getType, type) + .eqIfPresent(AiModelDO::getPlatform, platform) + .orderByAsc(AiModelDO::getSort)); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatModelMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatModelMapper.java deleted file mode 100644 index a3136fa9f6..0000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatModelMapper.java +++ /dev/null @@ -1,43 +0,0 @@ -package cn.iocoder.yudao.module.ai.dal.mysql.model; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; -import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; -import org.apache.ibatis.annotations.Mapper; - -import java.util.Collection; -import java.util.List; - -/** - * API 聊天模型 Mapper - * - * @author fansili - */ -@Mapper -public interface AiChatModelMapper extends BaseMapperX { - - default AiChatModelDO selectFirstByStatus(Integer status) { - return selectOne(new QueryWrapperX() - .eq("status", status) - .limitN(1) - .orderByAsc("sort")); - } - - default PageResult selectPage(AiChatModelPageReqVO reqVO) { - return selectPage(reqVO, new LambdaQueryWrapperX() - .likeIfPresent(AiChatModelDO::getName, reqVO.getName()) - .eqIfPresent(AiChatModelDO::getModel, reqVO.getModel()) - .eqIfPresent(AiChatModelDO::getPlatform, reqVO.getPlatform()) - .orderByAsc(AiChatModelDO::getSort)); - } - - default List selectList(Integer status) { - return selectList(new LambdaQueryWrapperX() - .eq(AiChatModelDO::getStatus, status) - .orderByAsc(AiChatModelDO::getSort)); - } - -} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java index 8f094087f1..792a8ae150 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java @@ -4,17 +4,18 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.ai.core.enums.AiModelTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationCreateMyReqVO; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationUpdateMyReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatConversationMapper; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService; -import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -44,7 +45,7 @@ public class AiChatConversationServiceImpl implements AiChatConversationService private AiChatConversationMapper chatConversationMapper; @Resource - private AiChatModelService chatModalService; + private AiModelService chatModalService; @Resource private AiChatRoleService chatRoleService; @Resource @@ -54,9 +55,9 @@ public class AiChatConversationServiceImpl implements AiChatConversationService public Long createChatConversationMy(AiChatConversationCreateMyReqVO createReqVO, Long userId) { // 1.1 获得 AiChatRoleDO 聊天角色 AiChatRoleDO role = createReqVO.getRoleId() != null ? chatRoleService.validateChatRole(createReqVO.getRoleId()) : null; - // 1.2 获得 AiChatModelDO 聊天模型 - AiChatModelDO model = role != null && role.getModelId() != null ? chatModalService.validateChatModel(role.getModelId()) - : chatModalService.getRequiredDefaultChatModel(); + // 1.2 获得 AiModelDO 聊天模型 + AiModelDO model = role != null && role.getModelId() != null ? chatModalService.validateModel(role.getModelId()) + : chatModalService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType()); Assert.notNull(model, "必须找到默认模型"); validateChatModel(model); @@ -86,9 +87,9 @@ public class AiChatConversationServiceImpl implements AiChatConversationService throw exception(CHAT_CONVERSATION_NOT_EXISTS); } // 1.2 校验模型是否存在(修改模型的情况) - AiChatModelDO model = null; + AiModelDO model = null; if (updateReqVO.getModelId() != null) { - model = chatModalService.validateChatModel(updateReqVO.getModelId()); + model = chatModalService.validateModel(updateReqVO.getModelId()); } // 1.3 校验知识库是否存在 @@ -139,7 +140,7 @@ public class AiChatConversationServiceImpl implements AiChatConversationService chatConversationMapper.deleteById(id); } - private void validateChatModel(AiChatModelDO model) { + private void validateChatModel(AiModelDO model) { if (ObjectUtil.isAllNotEmpty(model.getTemperature(), model.getMaxTokens(), model.getMaxContexts())) { return; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java index 1c8927e07a..2298d9b1d9 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -15,13 +15,12 @@ import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessage import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper; import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum; import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; -import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.messages.Message; @@ -63,9 +62,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { @Resource private AiChatConversationService chatConversationService; @Resource - private AiChatModelService chatModalService; - @Resource - private AiApiKeyService apiKeyService; + private AiModelService modalService; @Resource private AiKnowledgeSegmentService knowledgeSegmentService; @@ -78,8 +75,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } List historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId()); // 1.2 校验模型 - AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId()); - ChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); + AiModelDO model = modalService.validateModel(conversation.getModelId()); + ChatModel chatModel = modalService.getChatModel(model.getKeyId()); // 2. 插入 user 发送消息 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, @@ -112,8 +109,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } List historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId()); // 1.2 校验模型 - AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId()); - StreamingChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); + AiModelDO model = modalService.validateModel(conversation.getModelId()); + StreamingChatModel chatModel = modalService.getChatModel(model.getKeyId()); // 2. 插入 user 发送消息 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, @@ -161,8 +158,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { return null; } - private Prompt buildPrompt(AiChatConversationDO conversation, List messages,List segmentList, - AiChatModelDO model, AiChatMessageSendReqVO sendReqVO) { + private Prompt buildPrompt(AiChatConversationDO conversation, List messages, List segmentList, + AiModelDO model, AiChatMessageSendReqVO sendReqVO) { // 1. 构建 Prompt Message 列表 List chatMessages = new ArrayList<>(); @@ -232,7 +229,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } private AiChatMessageDO createChatMessage(Long conversationId, Long replyId, - AiChatModelDO model, Long userId, Long roleId, + AiModelDO model, Long userId, Long roleId, MessageType messageType, String content, Boolean useContext) { AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId) .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index 523014d530..7204f4d5b2 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -12,13 +12,17 @@ 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.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePublicPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO; import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper; import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.infra.api.file.FileApi; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import jakarta.annotation.Resource; @@ -54,15 +58,15 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; @Slf4j public class AiImageServiceImpl implements AiImageService { + @Resource + private AiModelService modelService; + @Resource private AiImageMapper imageMapper; @Resource private FileApi fileApi; - @Resource - private AiApiKeyService apiKeyService; - @Override public PageResult getImagePageMy(Long userId, AiImagePageReqVO pageReqVO) { return imageMapper.selectPageMy(userId, pageReqVO); @@ -88,23 +92,31 @@ public class AiImageServiceImpl implements AiImageService { @Override public Long drawImage(Long userId, AiImageDrawReqVO drawReqVO) { - // 1. 保存数据库 - AiImageDO image = BeanUtils.toBean(drawReqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false) - .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus()); + // 1. 校验模型 + AiModelDO model = modelService.validateModel(drawReqVO.getModelId()); + + // 2. 保存数据库 + AiImageDO image = BeanUtils.toBean(drawReqVO, AiImageDO.class).setUserId(userId) + .setPlatform(model.getPlatform()).setModelId(model.getId()).setModel(model.getModel()) + .setPublicStatus(false).setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus()); imageMapper.insert(image); - // 2. 异步绘制,后续前端通过返回的 id 进行轮询结果 - getSelf().executeDrawImage(image, drawReqVO); + + // 3. 异步绘制,后续前端通过返回的 id 进行轮询结果 + getSelf().executeDrawImage(image, drawReqVO, model); return image.getId(); } @Async - public void executeDrawImage(AiImageDO image, AiImageDrawReqVO req) { + public void executeDrawImage(AiImageDO image, AiImageDrawReqVO reqVO, AiModelDO model) { try { // 1.1 构建请求 - ImageOptions request = buildImageOptions(req); + ImageOptions request = buildImageOptions(reqVO, model); // 1.2 执行请求 - ImageModel imageModel = apiKeyService.getImageModel(AiPlatformEnum.validatePlatform(req.getPlatform())); - ImageResponse response = imageModel.call(new ImagePrompt(req.getPrompt(), request)); + ImageModel imageModel = modelService.getImageModel(model.getId()); + ImageResponse response = imageModel.call(new ImagePrompt(reqVO.getPrompt(), request)); + if (response.getResult() == null) { + throw new IllegalArgumentException("生成结果为空"); + } // 2. 上传到文件服务 String b64Json = response.getResult().getOutput().getB64Json(); @@ -116,25 +128,25 @@ public class AiImageServiceImpl implements AiImageService { imageMapper.updateById(new AiImageDO().setId(image.getId()).setStatus(AiImageStatusEnum.SUCCESS.getStatus()) .setPicUrl(filePath).setFinishTime(LocalDateTime.now())); } catch (Exception ex) { - log.error("[doDall][image({}) 生成异常]", image, ex); + log.error("[executeDrawImage][image({}) 生成异常]", image, ex); imageMapper.updateById(new AiImageDO().setId(image.getId()) .setStatus(AiImageStatusEnum.FAIL.getStatus()) .setErrorMessage(ex.getMessage()).setFinishTime(LocalDateTime.now())); } } - private static ImageOptions buildImageOptions(AiImageDrawReqVO draw) { - if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.OPENAI.getPlatform())) { + private static ImageOptions buildImageOptions(AiImageDrawReqVO draw, AiModelDO model) { + if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.OPENAI.getPlatform())) { // https://platform.openai.com/docs/api-reference/images/create - return OpenAiImageOptions.builder().withModel(draw.getModel()) + return OpenAiImageOptions.builder().withModel(model.getModel()) .withHeight(draw.getHeight()).withWidth(draw.getWidth()) .withStyle(MapUtil.getStr(draw.getOptions(), "style")) // 风格 .withResponseFormat("b64_json") .build(); - } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.STABLE_DIFFUSION.getPlatform())) { + } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.STABLE_DIFFUSION.getPlatform())) { // https://platform.stability.ai/docs/api-reference#tag/SDXL-and-SD1.6/operation/textToImage // https://platform.stability.ai/docs/api-reference#tag/Text-to-Image/operation/textToImage - return StabilityAiImageOptions.builder().model(draw.getModel()) + return StabilityAiImageOptions.builder().model(model.getModel()) .height(draw.getHeight()).width(draw.getWidth()) .seed(Long.valueOf(draw.getOptions().get("seed"))) .cfgScale(Float.valueOf(draw.getOptions().get("scale"))) @@ -143,22 +155,22 @@ public class AiImageServiceImpl implements AiImageService { .stylePreset(String.valueOf(draw.getOptions().get("stylePreset"))) .clipGuidancePreset(String.valueOf(draw.getOptions().get("clipGuidancePreset"))) .build(); - } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) { + } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) { return DashScopeImageOptions.builder() - .withModel(draw.getModel()).withN(1) + .withModel(model.getModel()).withN(1) .withHeight(draw.getHeight()).withWidth(draw.getWidth()) .build(); - } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) { + } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) { return QianFanImageOptions.builder() - .model(draw.getModel()).N(1) + .model(model.getModel()).N(1) .height(draw.getHeight()).width(draw.getWidth()) .build(); - } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.ZHI_PU.getPlatform())) { + } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.ZHI_PU.getPlatform())) { return ZhiPuAiImageOptions.builder() - .model(draw.getModel()) + .model(model.getModel()) .build(); } - throw new IllegalArgumentException("不支持的 AI 平台:" + draw.getPlatform()); + throw new IllegalArgumentException("不支持的 AI 平台:" + model.getPlatform()); } @Override @@ -206,7 +218,7 @@ public class AiImageServiceImpl implements AiImageService { @Override @Transactional(rollbackFor = Exception.class) public Long midjourneyImagine(Long userId, AiMidjourneyImagineReqVO reqVO) { - MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi(); + MidjourneyApi midjourneyApi = modelService.getMidjourneyApi(); // 1. 保存数据库 AiImageDO image = BeanUtils.toBean(reqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false) .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus()) @@ -237,7 +249,7 @@ public class AiImageServiceImpl implements AiImageService { @Override public Integer midjourneySync() { - MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi(); + MidjourneyApi midjourneyApi = modelService.getMidjourneyApi(); // 1.1 获取 Midjourney 平台,状态在 “进行中” 的 image List imageList = imageMapper.selectListByStatusAndPlatform( AiImageStatusEnum.IN_PROGRESS.getStatus(), AiPlatformEnum.MIDJOURNEY.getPlatform()); @@ -308,7 +320,7 @@ public class AiImageServiceImpl implements AiImageService { @Override public Long midjourneyAction(Long userId, AiMidjourneyActionReqVO reqVO) { - MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi(); + MidjourneyApi midjourneyApi = modelService.getMidjourneyApi(); // 1.1 检查 image AiImageDO image = validateImageExists(reqVO.getId()); if (ObjUtil.notEqual(userId, image.getUserId())) { diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index adf1616cb0..910ff1249a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -7,14 +7,17 @@ import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.*; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentProcessRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSaveReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.document.Document; @@ -33,8 +36,8 @@ import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; -import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS; /** * AI 知识库分片 Service 实现类 @@ -58,7 +61,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService @Lazy // 延迟加载,避免循环依赖 private AiKnowledgeDocumentService knowledgeDocumentService; @Resource - private AiApiKeyService apiKeyService; + private AiModelService modelService; @Resource private TokenCountEstimator tokenCountEstimator; @@ -180,7 +183,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqBO.getKnowledgeId()); // 2.1 向量检索 - VectorStore vectorStore = apiKeyService.getOrCreateVectorStoreByModelId(knowledge.getEmbeddingModelId()); + VectorStore vectorStore = getVectorStoreById(knowledge); List documents = vectorStore.similaritySearch(SearchRequest.builder() .query(reqBO.getContent()) .topK(ObjUtil.defaultIfNull(reqBO.getTopK(), knowledge.getTopK())) @@ -251,11 +254,12 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService } private VectorStore getVectorStoreById(AiKnowledgeDO knowledge) { - return apiKeyService.getOrCreateVectorStoreByModelId(knowledge.getEmbeddingModelId()); + return modelService.getOrCreateVectorStore(knowledge.getEmbeddingModelId()); } private VectorStore getVectorStoreById(Long knowledgeId) { - return getVectorStoreById(knowledgeService.validateKnowledgeExists(knowledgeId)); + AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(knowledgeId); + return getVectorStoreById(knowledge); } private static List splitContentByToken(String content, Integer segmentMaxTokens) { diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java index 900cfb4a17..55822796ce 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java @@ -5,9 +5,9 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper; -import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -28,12 +28,12 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { private AiKnowledgeMapper knowledgeMapper; @Resource - private AiChatModelService chatModelService; + private AiModelService chatModelService; @Override public Long createKnowledge(AiKnowledgeSaveReqVO createReqVO) { // 1. 校验模型配置 - AiChatModelDO model = chatModelService.validateChatModel(createReqVO.getEmbeddingModelId()); + AiModelDO model = chatModelService.validateModel(createReqVO.getEmbeddingModelId()); // 2. 插入知识库 AiKnowledgeDO knowledge = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class) @@ -47,7 +47,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { // 1.1 校验知识库存在 validateKnowledgeExists(updateReqVO.getId()); // 1.2 校验模型配置 - AiChatModelDO model = chatModelService.validateChatModel(updateReqVO.getEmbeddingModelId()); + AiModelDO model = chatModelService.validateModel(updateReqVO.getEmbeddingModelId()); // 2. 更新知识库 AiKnowledgeDO updateObj = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java index 5536831469..3eb49441b5 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.service.mindmap; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.ai.core.enums.AiModelTypeEnum; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.ai.core.util.AiUtils; import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -12,14 +13,13 @@ import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.mindmap.AiMindMapMapper; import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum; import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; -import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.messages.Message; @@ -50,9 +50,7 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MIND_MAP_NOT_E public class AiMindMapServiceImpl implements AiMindMapService { @Resource - private AiApiKeyService apiKeyService; - @Resource - private AiChatModelService chatModalService; + private AiModelService modalService; @Resource private AiChatRoleService chatRoleService; @@ -65,17 +63,17 @@ public class AiMindMapServiceImpl implements AiMindMapService { AiChatRoleDO role = CollUtil.getFirst( chatRoleService.getChatRoleListByName(AiChatRoleEnum.AI_MIND_MAP_ROLE.getName())); // 1.1 获取导图执行模型 - AiChatModelDO model = getModel(role); + AiModelDO model = getModel(role); // 1.2 获取角色设定消息 String systemMessage = role != null && StrUtil.isNotBlank(role.getSystemMessage()) ? role.getSystemMessage() : AiChatRoleEnum.AI_MIND_MAP_ROLE.getSystemMessage(); // 1.3 校验平台 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); - ChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); + ChatModel chatModel = modalService.getChatModel(model.getId()); // 2. 插入思维导图信息 - AiMindMapDO mindMapDO = BeanUtils.toBean(generateReqVO, AiMindMapDO.class, - mindMap -> mindMap.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform())); + AiMindMapDO mindMapDO = BeanUtils.toBean(generateReqVO, AiMindMapDO.class, mindMap -> mindMap.setUserId(userId) + .setPlatform(platform.getPlatform()).setModelId(model.getId()).setModel(model.getModel())); mindMapMapper.insert(mindMapDO); // 3.1 构建 Prompt,并进行调用 @@ -103,7 +101,7 @@ public class AiMindMapServiceImpl implements AiMindMapService { } - private Prompt buildPrompt(AiMindMapGenerateReqVO generateReqVO, AiChatModelDO model, String systemMessage) { + private Prompt buildPrompt(AiMindMapGenerateReqVO generateReqVO, AiModelDO model, String systemMessage) { // 1. 构建 message 列表 List chatMessages = buildMessages(generateReqVO, systemMessage); // 2. 构建 options 对象 @@ -123,13 +121,13 @@ public class AiMindMapServiceImpl implements AiMindMapService { return chatMessages; } - private AiChatModelDO getModel(AiChatRoleDO role) { - AiChatModelDO model = null; + private AiModelDO getModel(AiChatRoleDO role) { + AiModelDO model = null; if (role != null && role.getModelId() != null) { - model = chatModalService.getChatModel(role.getModelId()); + model = modalService.getModel(role.getModelId()); } if (model == null) { - model = chatModalService.getRequiredDefaultChatModel(); + model = modalService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType()); } Assert.notNull(model, "[AI] 获取不到模型"); return model; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java index 29d9e3e07a..44da80041c 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java @@ -1,16 +1,10 @@ package cn.iocoder.yudao.module.ai.service.model; -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 cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; import jakarta.validation.Valid; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.image.ImageModel; -import org.springframework.ai.vectorstore.VectorStore; import java.util.List; @@ -74,50 +68,13 @@ public interface AiApiKeyService { */ List getApiKeyList(); - // ========== 与 spring-ai 集成 ========== - /** - * 获得 ChatModel 对象 - * - * @param id 编号 - * @return ChatModel 对象 - */ - ChatModel getChatModel(Long id); - - /** - * 获得 ImageModel 对象 - * - * TODO 可优化点:目前默认获取 platform 对应的第一个开启的配置用于绘画;后续可以支持配置选择 + * 获得默认的 API 密钥 * * @param platform 平台 - * @return ImageModel 对象 + * @param status 状态 + * @return API 密钥 */ - ImageModel getImageModel(AiPlatformEnum platform); - - /** - * 获得 MidjourneyApi 对象 - * - * TODO 可优化点:目前默认获取 Midjourney 对应的第一个开启的配置用于绘画;后续可以支持配置选择 - * - * @return MidjourneyApi 对象 - */ - MidjourneyApi getMidjourneyApi(); - - /** - * 获得 SunoApi 对象 - * - * TODO 可优化点:目前默认获取 Suno 对应的第一个开启的配置用于音乐;后续可以支持配置选择 - * - * @return SunoApi 对象 - */ - SunoApi getSunoApi(); - - /** - * 获得 VectorStore 对象 - * - * @param modelId 编号 - * @return VectorStore 对象 - */ - VectorStore getOrCreateVectorStoreByModelId(Long modelId); + AiApiKeyDO getRequiredDefaultApiKey(String platform, Integer status); } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java index 38d6c95495..f0bac8a6d9 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java @@ -1,31 +1,21 @@ package cn.iocoder.yudao.module.ai.service.model; -import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; -import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; -import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; -import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper; import jakarta.annotation.Resource; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.embedding.EmbeddingModel; -import org.springframework.ai.image.ImageModel; -import org.springframework.ai.vectorstore.SimpleVectorStore; -import org.springframework.ai.vectorstore.VectorStore; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.API_KEY_DISABLE; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.API_KEY_NOT_EXISTS; /** * AI API 密钥 Service 实现类 @@ -39,14 +29,6 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { @Resource private AiApiKeyMapper apiKeyMapper; - // TODO @芋艿:后续要不要改? - @Resource - @Lazy // 延迟加载,解决渲染依赖 - private AiChatModelService chatModelService; - - @Resource - private AiModelFactory modelFactory; - @Override public Long createApiKey(AiApiKeySaveReqVO createReqVO) { // 插入 @@ -105,57 +87,13 @@ public class AiApiKeyServiceImpl implements AiApiKeyService { return apiKeyMapper.selectList(); } - // ========== 与 spring-ai 集成 ========== - @Override - public ChatModel getChatModel(Long id) { - AiApiKeyDO apiKey = validateApiKey(id); - AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); - return modelFactory.getOrCreateChatModel(platform, apiKey.getApiKey(), apiKey.getUrl()); - } - - @Override - public ImageModel getImageModel(AiPlatformEnum platform) { - AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); + public AiApiKeyDO getRequiredDefaultApiKey(String platform, Integer status) { + AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform, status); if (apiKey == null) { - throw exception(API_KEY_IMAGE_NODE_FOUND, platform.getName()); + throw exception(API_KEY_NOT_EXISTS); } - return modelFactory.getOrCreateImageModel(platform, apiKey.getApiKey(), apiKey.getUrl()); - } - - @Override - public MidjourneyApi getMidjourneyApi() { - AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus( - AiPlatformEnum.MIDJOURNEY.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); - if (apiKey == null) { - throw exception(API_KEY_MIDJOURNEY_NOT_FOUND); - } - return modelFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl()); - } - - @Override - public SunoApi getSunoApi() { - AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus( - AiPlatformEnum.SUNO.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); - if (apiKey == null) { - throw exception(API_KEY_SUNO_NOT_FOUND); - } - return modelFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl()); - } - - @Override - public VectorStore getOrCreateVectorStoreByModelId(Long modelId) { - // 获取模型 + 密钥 - AiChatModelDO chatModel = chatModelService.validateChatModel(modelId); - AiApiKeyDO apiKey = validateApiKey(chatModel.getKeyId()); - AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); - - // 创建或获取 EmbeddingModel 对象 - EmbeddingModel embeddingModel = modelFactory.getOrCreateEmbeddingModel(platform, apiKey.getApiKey(), - apiKey.getUrl(), chatModel.getModel()); - - // 创建或获取 VectorStore 对象 - return modelFactory.getOrCreateVectorStore(SimpleVectorStore.class, embeddingModel); + return apiKey; } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelService.java deleted file mode 100644 index f83ac73c91..0000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelService.java +++ /dev/null @@ -1,92 +0,0 @@ -package cn.iocoder.yudao.module.ai.service.model; - -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelSaveReqVO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; -import jakarta.validation.Valid; - -import java.util.Collection; -import java.util.List; - -import java.util.Set; - -/** - * AI 聊天模型 Service 接口 - * - * @author fansili - * @since 2024/4/24 19:42 - */ -public interface AiChatModelService { - - /** - * 创建聊天模型 - * - * @param createReqVO 创建信息 - * @return 编号 - */ - Long createChatModel(@Valid AiChatModelSaveReqVO createReqVO); - - /** - * 更新聊天模型 - * - * @param updateReqVO 更新信息 - */ - void updateChatModel(@Valid AiChatModelSaveReqVO updateReqVO); - - /** - * 删除聊天模型 - * - * @param id 编号 - */ - void deleteChatModel(Long id); - - /** - * 获得聊天模型 - * - * @param id 编号 - * @return 聊天模型 - */ - AiChatModelDO getChatModel(Long id); - - /** - * 获得默认的聊天模型 - * - * 如果获取不到,则抛出 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} 业务异常 - * - * @return 聊天模型 - */ - AiChatModelDO getRequiredDefaultChatModel(); - - /** - * 获得聊天模型分页 - * - * @param pageReqVO 分页查询 - * @return 聊天模型分页 - */ - PageResult getChatModelPage(AiChatModelPageReqVO pageReqVO); - - /** - * 校验聊天模型 - * - * @param id 编号 - * @return 聊天模型 - */ - AiChatModelDO validateChatModel(Long id); - - /** - * 获得聊天模型列表 - * - * @param status 状态 - * @return 聊天模型列表 - */ - List getChatModelListByStatus(Integer status); - - /** - * 获得聊天模型列表 - * - * @param ids 编号数组 - * @return 模型列表 - */ - List getChatModelList(Collection ids); -} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java index 4b11602f54..9140882a1f 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java @@ -1,114 +1,168 @@ package cn.iocoder.yudao.module.ai.service.model; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelSaveReqVO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; -import cn.iocoder.yudao.module.ai.dal.mysql.model.AiChatModelMapper; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; +import cn.iocoder.yudao.module.ai.dal.mysql.model.AiChatMapper; import jakarta.annotation.Resource; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.image.ImageModel; +import org.springframework.ai.vectorstore.SimpleVectorStore; +import org.springframework.ai.vectorstore.VectorStore; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; -import java.util.Collection; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; /** - * AI 聊天模型 Service 实现类 + * AI 模型 Service 实现类 * * @author fansili */ @Service @Validated -public class AiChatModelServiceImpl implements AiChatModelService { +public class AiChatModelServiceImpl implements AiModelService { @Resource private AiApiKeyService apiKeyService; @Resource - private AiChatModelMapper chatModelMapper; + private AiChatMapper modelMapper; + + @Resource + private AiModelFactory modelFactory; @Override - public Long createChatModel(AiChatModelSaveReqVO createReqVO) { + public Long createModel(AiModelSaveReqVO createReqVO) { // 1. 校验 AiPlatformEnum.validatePlatform(createReqVO.getPlatform()); apiKeyService.validateApiKey(createReqVO.getKeyId()); // 2. 插入 - AiChatModelDO chatModel = BeanUtils.toBean(createReqVO, AiChatModelDO.class); - chatModelMapper.insert(chatModel); - return chatModel.getId(); + AiModelDO model = BeanUtils.toBean(createReqVO, AiModelDO.class); + modelMapper.insert(model); + return model.getId(); } @Override - public void updateChatModel(AiChatModelSaveReqVO updateReqVO) { + public void updateModel(AiModelSaveReqVO updateReqVO) { // 1. 校验 - validateChatModelExists(updateReqVO.getId()); + validateModelExists(updateReqVO.getId()); AiPlatformEnum.validatePlatform(updateReqVO.getPlatform()); apiKeyService.validateApiKey(updateReqVO.getKeyId()); // 2. 更新 - AiChatModelDO updateObj = BeanUtils.toBean(updateReqVO, AiChatModelDO.class); - chatModelMapper.updateById(updateObj); + AiModelDO updateObj = BeanUtils.toBean(updateReqVO, AiModelDO.class); + modelMapper.updateById(updateObj); } @Override - public void deleteChatModel(Long id) { + public void deleteModel(Long id) { // 校验存在 - validateChatModelExists(id); + validateModelExists(id); // 删除 - chatModelMapper.deleteById(id); + modelMapper.deleteById(id); } - private AiChatModelDO validateChatModelExists(Long id) { - AiChatModelDO model = chatModelMapper.selectById(id); - if (chatModelMapper.selectById(id) == null) { - throw exception(CHAT_MODEL_NOT_EXISTS); + private AiModelDO validateModelExists(Long id) { + AiModelDO model = modelMapper.selectById(id); + if (modelMapper.selectById(id) == null) { + throw exception(MODEL_NOT_EXISTS); } return model; } @Override - public AiChatModelDO getChatModel(Long id) { - return chatModelMapper.selectById(id); + public AiModelDO getModel(Long id) { + return modelMapper.selectById(id); } @Override - public AiChatModelDO getRequiredDefaultChatModel() { - AiChatModelDO model = chatModelMapper.selectFirstByStatus(CommonStatusEnum.ENABLE.getStatus()); + public AiModelDO getRequiredDefaultModel(Integer type) { + AiModelDO model = modelMapper.selectFirstByStatus(type, CommonStatusEnum.ENABLE.getStatus()); if (model == null) { - throw exception(CHAT_MODEL_DEFAULT_NOT_EXISTS); + throw exception(MODEL_DEFAULT_NOT_EXISTS); } return model; } @Override - public PageResult getChatModelPage(AiChatModelPageReqVO pageReqVO) { - return chatModelMapper.selectPage(pageReqVO); + public PageResult getModelPage(AiModelPageReqVO pageReqVO) { + return modelMapper.selectPage(pageReqVO); } @Override - public AiChatModelDO validateChatModel(Long id) { - AiChatModelDO model = validateChatModelExists(id); + public AiModelDO validateModel(Long id) { + AiModelDO model = validateModelExists(id); if (CommonStatusEnum.isDisable(model.getStatus())) { - throw exception(CHAT_MODEL_DISABLE); + throw exception(MODEL_DISABLE); } return model; } @Override - public List getChatModelListByStatus(Integer status) { - return chatModelMapper.selectList(status); + public List getModelListByStatusAndType(Integer status, Integer type, + String platform) { + return modelMapper.selectListByStatusAndType(status, type, platform); + } + + // ========== 与 Spring AI 集成 ========== + + @Override + public ChatModel getChatModel(Long id) { + AiModelDO model = validateModel(id); + AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId()); + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); + return modelFactory.getOrCreateChatModel(platform, apiKey.getApiKey(), apiKey.getUrl()); } @Override - public List getChatModelList(Collection ids) { - return chatModelMapper.selectBatchIds(ids); + public ImageModel getImageModel(Long id) { + AiModelDO model = validateModel(id); + AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId()); + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); + return modelFactory.getOrCreateImageModel(platform, apiKey.getApiKey(), apiKey.getUrl()); + } + + @Override + public MidjourneyApi getMidjourneyApi() { + AiApiKeyDO apiKey = apiKeyService.getRequiredDefaultApiKey( + AiPlatformEnum.MIDJOURNEY.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); + return modelFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl()); + } + + @Override + public SunoApi getSunoApi() { + AiApiKeyDO apiKey = apiKeyService.getRequiredDefaultApiKey( + AiPlatformEnum.SUNO.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); + return modelFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl()); + } + + @Override + public VectorStore getOrCreateVectorStore(Long id) { + // 获取模型 + 密钥 + AiModelDO model = validateModel(id); + AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId()); + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); + + // 创建或获取 EmbeddingModel 对象 + EmbeddingModel embeddingModel = modelFactory.getOrCreateEmbeddingModel( + platform, apiKey.getApiKey(), apiKey.getUrl(), model.getModel()); + + // 创建或获取 VectorStore 对象 + return modelFactory.getOrCreateVectorStore(SimpleVectorStore.class, embeddingModel); } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java new file mode 100644 index 0000000000..e3746b14d2 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java @@ -0,0 +1,131 @@ +package cn.iocoder.yudao.module.ai.service.model; + +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; +import jakarta.validation.Valid; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.image.ImageModel; +import org.springframework.ai.vectorstore.VectorStore; + +import javax.annotation.Nullable; +import java.util.List; + +/** + * AI 模型 Service 接口 + * + * @author fansili + * @since 2024/4/24 19:42 + */ +public interface AiModelService { + + /** + * 创建模型 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createModel(@Valid AiModelSaveReqVO createReqVO); + + /** + * 更新模型 + * + * @param updateReqVO 更新信息 + */ + void updateModel(@Valid AiModelSaveReqVO updateReqVO); + + /** + * 删除模型 + * + * @param id 编号 + */ + void deleteModel(Long id); + + /** + * 获得模型 + * + * @param id 编号 + * @return 模型 + */ + AiModelDO getModel(Long id); + + /** + * 获得默认的模型 + * + * 如果获取不到,则抛出 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} 业务异常 + * + * @return 模型 + */ + AiModelDO getRequiredDefaultModel(Integer type); + + /** + * 获得模型分页 + * + * @param pageReqVO 分页查询 + * @return 模型分页 + */ + PageResult getModelPage(AiModelPageReqVO pageReqVO); + + /** + * 校验模型是否可使用 + * + * @param id 编号 + * @return 模型 + */ + AiModelDO validateModel(Long id); + + /** + * 获得模型列表 + * + * @param status 状态 + * @param type 类型 + * @param platform 平台,允许空 + * @return 模型列表 + */ + List getModelListByStatusAndType(Integer status, Integer type, + @Nullable String platform); + + // ========== 与 Spring AI 集成 ========== + + /** + * 获得 ChatModel 对象 + * + * @param id 编号 + * @return ChatModel 对象 + */ + ChatModel getChatModel(Long id); + + /** + * 获得 ImageModel 对象 + * + * @param id 编号 + * @return ImageModel 对象 + */ + ImageModel getImageModel(Long id); + + /** + * 获得 MidjourneyApi 对象 + * + * @return MidjourneyApi 对象 + */ + MidjourneyApi getMidjourneyApi(); + + /** + * 获得 SunoApi 对象 + * + * @return SunoApi 对象 + */ + SunoApi getSunoApi(); + + /** + * 获得 VectorStore 对象 + * + * @param id 编号 + * @return VectorStore 对象 + */ + VectorStore getOrCreateVectorStore(Long id); + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java index 3f10ec8402..e4ff81a477 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java @@ -16,7 +16,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper; import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum; import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.infra.api.file.FileApi; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -41,7 +41,7 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MUSIC_NOT_EXIS public class AiMusicServiceImpl implements AiMusicService { @Resource - private AiApiKeyService apiKeyService; + private AiModelService modelService; @Resource private AiMusicMapper musicMapper; @@ -53,7 +53,7 @@ public class AiMusicServiceImpl implements AiMusicService { @Transactional(rollbackFor = Exception.class) public List generateMusic(Long userId, AiSunoGenerateReqVO reqVO) { // 1. 调用 Suno 生成音乐 - SunoApi sunoApi = apiKeyService.getSunoApi(); + SunoApi sunoApi = modelService.getSunoApi(); List musicDataList; if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) { // 1.1 描述模式 @@ -88,7 +88,7 @@ public class AiMusicServiceImpl implements AiMusicService { log.info("[syncMusic][Suno 开始同步, 共 ({}) 个任务]", streamingTask.size()); // GET 请求,为避免参数过长,分批次处理 - SunoApi sunoApi = apiKeyService.getSunoApi(); + SunoApi sunoApi = modelService.getSunoApi(); CollUtil.split(streamingTask, 36).forEach(chunkList -> { Map taskIdMap = convertMap(chunkList, AiMusicDO::getTaskId, AiMusicDO::getId); List musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet())); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java index f238bd3e30..ceae2fc9c9 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.service.write; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.ai.core.enums.AiModelTypeEnum; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.ai.core.util.AiUtils; import cn.iocoder.yudao.framework.common.pojo.CommonResult; @@ -11,17 +12,16 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWritePageReqVO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.write.AiWriteDO; import cn.iocoder.yudao.module.ai.dal.mysql.write.AiWriteMapper; import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum; import cn.iocoder.yudao.module.ai.enums.DictTypeConstants; import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; -import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.system.api.dict.DictDataApi; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -54,17 +54,15 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.WRITE_NOT_EXIS public class AiWriteServiceImpl implements AiWriteService { @Resource - private AiApiKeyService apiKeyService; - @Resource - private AiChatModelService chatModalService; + private AiModelService chatModalService; @Resource private AiChatRoleService chatRoleService; @Resource - private DictDataApi dictDataApi; + private AiWriteMapper writeMapper; @Resource - private AiWriteMapper writeMapper; + private DictDataApi dictDataApi; @Override public Flux> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId) { @@ -72,17 +70,17 @@ public class AiWriteServiceImpl implements AiWriteService { AiChatRoleDO writeRole = CollUtil.getFirst( chatRoleService.getChatRoleListByName(AiChatRoleEnum.AI_WRITE_ROLE.getName())); // 1.1 获取写作执行模型 - AiChatModelDO model = getModel(writeRole); + AiModelDO model = getModel(writeRole); // 1.2 获取角色设定消息 String systemMessage = Objects.nonNull(writeRole) && StrUtil.isNotBlank(writeRole.getSystemMessage()) ? writeRole.getSystemMessage() : AiChatRoleEnum.AI_WRITE_ROLE.getSystemMessage(); // 1.3 校验平台 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); - StreamingChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); + StreamingChatModel chatModel = chatModalService.getChatModel(model.getKeyId()); // 2. 插入写作信息 - AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class, - write -> write.setUserId(userId).setPlatform(platform.getPlatform()).setModel(model.getModel())); + AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class, write -> write.setUserId(userId) + .setPlatform(platform.getPlatform()).setModelId(model.getId()).setModel(model.getModel())); writeMapper.insert(writeDO); // 3.1 构建 Prompt,并进行调用 @@ -109,19 +107,19 @@ public class AiWriteServiceImpl implements AiWriteService { }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.WRITE_STREAM_ERROR))); } - private AiChatModelDO getModel(AiChatRoleDO writeRole) { - AiChatModelDO model = null; + private AiModelDO getModel(AiChatRoleDO writeRole) { + AiModelDO model = null; if (Objects.nonNull(writeRole) && Objects.nonNull(writeRole.getModelId())) { - model = chatModalService.getChatModel(writeRole.getModelId()); + model = chatModalService.getModel(writeRole.getModelId()); } if (model == null) { - model = chatModalService.getRequiredDefaultChatModel(); + model = chatModalService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType()); } Assert.notNull(model, "[AI] 获取不到模型"); return model; } - private Prompt buildPrompt(AiWriteGenerateReqVO generateReqVO, AiChatModelDO model, String systemMessage) { + private Prompt buildPrompt(AiWriteGenerateReqVO generateReqVO, AiModelDO model, String systemMessage) { // 1. 构建 message 列表 List chatMessages = buildMessages(generateReqVO, systemMessage); // 2. 构建 options 对象 diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiModelTypeEnum.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiModelTypeEnum.java new file mode 100644 index 0000000000..4f7a4e462d --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiModelTypeEnum.java @@ -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 { + + 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; + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java index 7b479d8f1a..a154db8c88 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java @@ -1,8 +1,11 @@ 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 模型平台 * @@ -10,7 +13,7 @@ import lombok.Getter; */ @Getter @AllArgsConstructor -public enum AiPlatformEnum { +public enum AiPlatformEnum implements ArrayValuable { // ========== 国内平台 ========== @@ -44,6 +47,8 @@ public enum AiPlatformEnum { */ 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)) { @@ -53,4 +58,9 @@ public enum AiPlatformEnum { throw new IllegalArgumentException("非法平台: " + platform); } + @Override + public String[] array() { + return ARRAYS; + } + } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index 1f9b1f21a2..ffc052c5d3 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -456,25 +456,4 @@ public class AiModelFactoryImpl implements AiModelFactory { return vectorStore; } - /** - * 创建向量存储文件 - * - * @param embeddingModel 嵌入模型 - * @return 向量存储文件 - */ - private File createVectorStoreFile(EmbeddingModel embeddingModel) { - // 获取简单类名 - String simpleClassName = embeddingModel.getClass().getSimpleName(); - // 获取用户主目录 - String userHome = FileUtil.getUserHomePath(); - // 创建vector_store目录 - File vectorStoreDir = new File(userHome, "vector_store"); - if (!vectorStoreDir.exists()) { - vectorStoreDir.mkdirs(); - } - - // 创建文件 - return new File(vectorStoreDir, "simple_" + simpleClassName + ".json"); - } - } From 1c9c9790cd0feec6154637c20fa1bcb20d692dfb Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 3 Mar 2025 21:48:22 +0800 Subject: [PATCH 26/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E3=80=91AI=EF=BC=9A=E2=80=9C=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E2=80=9D=E9=87=8D=E6=9E=84=E4=B8=BA=E2=80=9C?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E2=80=9D=EF=BC=8C=E6=94=AF=E6=8C=81=20type?= =?UTF-8?q?=20=E6=A8=A1=E5=9E=8B=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../midjourney/AiMidjourneyImagineReqVO.java | 6 +-- .../ai/service/image/AiImageServiceImpl.java | 45 ++++++++++--------- .../service/model/AiChatModelServiceImpl.java | 6 +-- .../ai/service/model/AiModelService.java | 3 +- 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java index b90882639d..efb5906157 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java @@ -13,9 +13,9 @@ public class AiMidjourneyImagineReqVO { @NotEmpty(message = "提示词不能为空!") private String prompt; - @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "midjourney") - @NotEmpty(message = "模型不能为空") - private String model; // 参考 MidjourneyApi.ModelEnum + @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "模型编号不能为空") + private Long modelId; @Schema(description = "图片宽度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "图片宽度不能为空") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index 7204f4d5b2..60ca9ac996 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.service.image; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.codec.Base64; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; @@ -217,52 +218,56 @@ public class AiImageServiceImpl implements AiImageService { @Override @Transactional(rollbackFor = Exception.class) - public Long midjourneyImagine(Long userId, AiMidjourneyImagineReqVO reqVO) { - MidjourneyApi midjourneyApi = modelService.getMidjourneyApi(); - // 1. 保存数据库 - AiImageDO image = BeanUtils.toBean(reqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false) + public Long midjourneyImagine(Long userId, AiMidjourneyImagineReqVO drawReqVO) { + // 1. 校验模型 + AiModelDO model = modelService.validateModel(drawReqVO.getModelId()); + Assert.equals(model.getPlatform(), AiPlatformEnum.MIDJOURNEY.getPlatform(), "平台不匹配"); + MidjourneyApi midjourneyApi = modelService.getMidjourneyApi(model.getId()); + + // 2. 保存数据库 + AiImageDO image = BeanUtils.toBean(drawReqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false) .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus()) - .setPlatform(AiPlatformEnum.MIDJOURNEY.getPlatform()); + .setPlatform(AiPlatformEnum.MIDJOURNEY.getPlatform()).setModelId(model.getId()).setModel(model.getName()); imageMapper.insert(image); - // 2. 调用 Midjourney Proxy 提交任务 - List base64Array = StrUtil.isBlank(reqVO.getReferImageUrl()) ? null : - Collections.singletonList("data:image/jpeg;base64,".concat(Base64.encode(HttpUtil.downloadBytes(reqVO.getReferImageUrl())))); + // 3. 调用 Midjourney Proxy 提交任务 + List base64Array = StrUtil.isBlank(drawReqVO.getReferImageUrl()) ? null : + Collections.singletonList("data:image/jpeg;base64,".concat(Base64.encode(HttpUtil.downloadBytes(drawReqVO.getReferImageUrl())))); MidjourneyApi.ImagineRequest imagineRequest = new MidjourneyApi.ImagineRequest( - base64Array, reqVO.getPrompt(),null, - MidjourneyApi.ImagineRequest.buildState(reqVO.getWidth(), - reqVO.getHeight(), reqVO.getVersion(), reqVO.getModel())); + base64Array, drawReqVO.getPrompt(),null, + MidjourneyApi.ImagineRequest.buildState(drawReqVO.getWidth(), + drawReqVO.getHeight(), drawReqVO.getVersion(), model.getModel())); MidjourneyApi.SubmitResponse imagineResponse = midjourneyApi.imagine(imagineRequest); - // 3. 情况一【失败】:抛出业务异常 + // 4.1 情况一【失败】:抛出业务异常 if (!MidjourneyApi.SubmitCodeEnum.SUCCESS_CODES.contains(imagineResponse.code())) { String description = imagineResponse.description().contains("quota_not_enough") ? "账户余额不足" : imagineResponse.description(); throw exception(IMAGE_MIDJOURNEY_SUBMIT_FAIL, description); } - // 4. 情况二【成功】:更新 taskId 和参数 + // 4.2 情况二【成功】:更新 taskId 和参数 imageMapper.updateById(new AiImageDO().setId(image.getId()) - .setTaskId(imagineResponse.result()).setOptions(BeanUtil.beanToMap(reqVO))); + .setTaskId(imagineResponse.result()).setOptions(BeanUtil.beanToMap(drawReqVO))); return image.getId(); } @Override public Integer midjourneySync() { - MidjourneyApi midjourneyApi = modelService.getMidjourneyApi(); // 1.1 获取 Midjourney 平台,状态在 “进行中” 的 image - List imageList = imageMapper.selectListByStatusAndPlatform( + List images = imageMapper.selectListByStatusAndPlatform( AiImageStatusEnum.IN_PROGRESS.getStatus(), AiPlatformEnum.MIDJOURNEY.getPlatform()); - if (CollUtil.isEmpty(imageList)) { + if (CollUtil.isEmpty(images)) { return 0; } // 1.2 调用 Midjourney Proxy 获取任务进展 - List taskList = midjourneyApi.getTaskList(convertSet(imageList, AiImageDO::getTaskId)); + MidjourneyApi midjourneyApi = modelService.getMidjourneyApi(images.get(0).getModelId()); + List taskList = midjourneyApi.getTaskList(convertSet(images, AiImageDO::getTaskId)); Map taskMap = convertMap(taskList, MidjourneyApi.Notify::id); // 2. 逐个处理,更新进展 int count = 0; - for (AiImageDO image : imageList) { + for (AiImageDO image : images) { MidjourneyApi.Notify notify = taskMap.get(image.getTaskId()); if (notify == null) { log.error("[midjourneySync][image({}) 查询不到进展]", image); @@ -320,12 +325,12 @@ public class AiImageServiceImpl implements AiImageService { @Override public Long midjourneyAction(Long userId, AiMidjourneyActionReqVO reqVO) { - MidjourneyApi midjourneyApi = modelService.getMidjourneyApi(); // 1.1 检查 image AiImageDO image = validateImageExists(reqVO.getId()); if (ObjUtil.notEqual(userId, image.getUserId())) { throw exception(IMAGE_NOT_EXISTS); } + MidjourneyApi midjourneyApi = modelService.getMidjourneyApi(image.getModelId()); // 1.2 检查 customId MidjourneyApi.Button button = CollUtil.findOne(image.getButtons(), buttonX -> buttonX.customId().equals(reqVO.getCustomId())); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java index 9140882a1f..2b38a68cf7 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java @@ -137,9 +137,9 @@ public class AiChatModelServiceImpl implements AiModelService { } @Override - public MidjourneyApi getMidjourneyApi() { - AiApiKeyDO apiKey = apiKeyService.getRequiredDefaultApiKey( - AiPlatformEnum.MIDJOURNEY.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); + public MidjourneyApi getMidjourneyApi(Long id) { + AiModelDO model = validateModel(id); + AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId()); return modelFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl()); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java index e3746b14d2..9e6185d450 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java @@ -109,9 +109,10 @@ public interface AiModelService { /** * 获得 MidjourneyApi 对象 * + * @param id 编号 * @return MidjourneyApi 对象 */ - MidjourneyApi getMidjourneyApi(); + MidjourneyApi getMidjourneyApi(Long id); /** * 获得 SunoApi 对象 From 433e91da8e0d9b683e7375a2668e8c4b01b5472f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 3 Mar 2025 21:55:49 +0800 Subject: [PATCH 27/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E3=80=91AI=EF=BC=9A=E2=80=9C=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E2=80=9D=E9=87=8D=E6=9E=84=E4=B8=BA=E2=80=9C?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E2=80=9D=EF=BC=8C=E6=94=AF=E6=8C=81=20type?= =?UTF-8?q?=20=E6=A8=A1=E5=9E=8B=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/service/chat/AiChatConversationServiceImpl.java | 9 +++++---- .../ai/service/knowledge/AiKnowledgeServiceImpl.java | 6 +++--- ...ChatModelServiceImpl.java => AiModelServiceImpl.java} | 2 +- .../module/ai/service/write/AiWriteServiceImpl.java | 8 ++++---- .../yudao/framework/ai/image/OpenAiImageModelTests.java | 4 ---- 5 files changed, 13 insertions(+), 16 deletions(-) rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/{AiChatModelServiceImpl.java => AiModelServiceImpl.java} (98%) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java index 792a8ae150..6483166a7f 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java @@ -45,7 +45,7 @@ public class AiChatConversationServiceImpl implements AiChatConversationService private AiChatConversationMapper chatConversationMapper; @Resource - private AiModelService chatModalService; + private AiModelService modalService; @Resource private AiChatRoleService chatRoleService; @Resource @@ -56,8 +56,8 @@ public class AiChatConversationServiceImpl implements AiChatConversationService // 1.1 获得 AiChatRoleDO 聊天角色 AiChatRoleDO role = createReqVO.getRoleId() != null ? chatRoleService.validateChatRole(createReqVO.getRoleId()) : null; // 1.2 获得 AiModelDO 聊天模型 - AiModelDO model = role != null && role.getModelId() != null ? chatModalService.validateModel(role.getModelId()) - : chatModalService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType()); + AiModelDO model = role != null && role.getModelId() != null ? modalService.validateModel(role.getModelId()) + : modalService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType()); Assert.notNull(model, "必须找到默认模型"); validateChatModel(model); @@ -89,7 +89,7 @@ public class AiChatConversationServiceImpl implements AiChatConversationService // 1.2 校验模型是否存在(修改模型的情况) AiModelDO model = null; if (updateReqVO.getModelId() != null) { - model = chatModalService.validateModel(updateReqVO.getModelId()); + model = modalService.validateModel(updateReqVO.getModelId()); } // 1.3 校验知识库是否存在 @@ -144,6 +144,7 @@ public class AiChatConversationServiceImpl implements AiChatConversationService if (ObjectUtil.isAllNotEmpty(model.getTemperature(), model.getMaxTokens(), model.getMaxContexts())) { return; } + Assert.equals(model.getType(), AiModelTypeEnum.CHAT.getType(), "模型类型不正确:" + model); throw exception(CHAT_CONVERSATION_MODEL_ERROR); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java index 55822796ce..0bd4029850 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java @@ -28,12 +28,12 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { private AiKnowledgeMapper knowledgeMapper; @Resource - private AiModelService chatModelService; + private AiModelService modelService; @Override public Long createKnowledge(AiKnowledgeSaveReqVO createReqVO) { // 1. 校验模型配置 - AiModelDO model = chatModelService.validateModel(createReqVO.getEmbeddingModelId()); + AiModelDO model = modelService.validateModel(createReqVO.getEmbeddingModelId()); // 2. 插入知识库 AiKnowledgeDO knowledge = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class) @@ -47,7 +47,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { // 1.1 校验知识库存在 validateKnowledgeExists(updateReqVO.getId()); // 1.2 校验模型配置 - AiModelDO model = chatModelService.validateModel(updateReqVO.getEmbeddingModelId()); + AiModelDO model = modelService.validateModel(updateReqVO.getEmbeddingModelId()); // 2. 更新知识库 AiKnowledgeDO updateObj = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java similarity index 98% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java index 2b38a68cf7..8583984c94 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java @@ -33,7 +33,7 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; */ @Service @Validated -public class AiChatModelServiceImpl implements AiModelService { +public class AiModelServiceImpl implements AiModelService { @Resource private AiApiKeyService apiKeyService; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java index ceae2fc9c9..4c38cd94d2 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java @@ -54,7 +54,7 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.WRITE_NOT_EXIS public class AiWriteServiceImpl implements AiWriteService { @Resource - private AiModelService chatModalService; + private AiModelService modalService; @Resource private AiChatRoleService chatRoleService; @@ -76,7 +76,7 @@ public class AiWriteServiceImpl implements AiWriteService { ? writeRole.getSystemMessage() : AiChatRoleEnum.AI_WRITE_ROLE.getSystemMessage(); // 1.3 校验平台 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); - StreamingChatModel chatModel = chatModalService.getChatModel(model.getKeyId()); + StreamingChatModel chatModel = modalService.getChatModel(model.getKeyId()); // 2. 插入写作信息 AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class, write -> write.setUserId(userId) @@ -110,10 +110,10 @@ public class AiWriteServiceImpl implements AiWriteService { private AiModelDO getModel(AiChatRoleDO writeRole) { AiModelDO model = null; if (Objects.nonNull(writeRole) && Objects.nonNull(writeRole.getModelId())) { - model = chatModalService.getModel(writeRole.getModelId()); + model = modalService.getModel(writeRole.getModelId()); } if (model == null) { - model = chatModalService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType()); + model = modalService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType()); } Assert.notNull(model, "[AI] 获取不到模型"); return model; diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageModelTests.java index f7a64e6a6a..64e921a5ac 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageModelTests.java @@ -5,13 +5,9 @@ import org.junit.jupiter.api.Test; import org.springframework.ai.image.ImageOptions; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.OpenAiImageModel; import org.springframework.ai.openai.OpenAiImageOptions; -import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiImageApi; -import org.springframework.web.client.RestClient; /** * {@link OpenAiImageModel} 集成测试类 From cc24eca470cea786c065b327ef2e4ea1dbe06e40 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 4 Mar 2025 20:19:30 +0800 Subject: [PATCH 28/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E3=80=91AI=EF=BC=9A=E2=80=9C=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E2=80=9D=E9=87=8D=E6=9E=84=E4=B8=BA=E2=80=9C?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E2=80=9D=EF=BC=8C=E6=94=AF=E6=8C=81=20type?= =?UTF-8?q?=20=E6=A8=A1=E5=9E=8B=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/enums/ErrorCodeConstants.java | 1 + .../segment/AiKnowledgeSegmentSaveReqVO.java | 21 +++++++++++++++++++ .../service/mindmap/AiMindMapServiceImpl.java | 12 ++++++++--- .../ai/service/write/AiWriteServiceImpl.java | 12 ++++++++--- 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSaveReqVO.java diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index 205fc80b10..9530d5bf57 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -17,6 +17,7 @@ public interface ErrorCodeConstants { ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "模型不存在!"); ErrorCode MODEL_DISABLE = new ErrorCode(1_040_001_001, "模型({})已禁用!"); ErrorCode MODEL_DEFAULT_NOT_EXISTS = new ErrorCode(1_040_001_002, "操作失败,找不到默认模型"); + ErrorCode MODEL_USE_TYPE_ERROR = new ErrorCode(1_040_001_003, "操作失败,该模型的模型类型不正确"); // ========== API 聊天角色 1-040-002-000 ========== ErrorCode CHAT_ROLE_NOT_EXISTS = new ErrorCode(1_040_002_000, "聊天角色不存在"); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSaveReqVO.java new file mode 100644 index 0000000000..0c5dad11db --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/vo/segment/AiKnowledgeSegmentSaveReqVO.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +@Schema(description = "管理后台 - AI 新增/修改知识库段落 request VO") +@Data +public class AiKnowledgeSegmentSaveReqVO { + + @Schema(description = "编号", example = "24790") + private Long id; + + @Schema(description = "知识库文档编号", example = "1024") + private Long documentId; + + @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") + @NotEmpty(message = "切片内容不能为空") + private String content; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java index 3eb49441b5..0dc851c216 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.ai.service.mindmap; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.ai.core.enums.AiModelTypeEnum; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; @@ -38,7 +38,7 @@ import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MIND_MAP_NOT_EXISTS; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; /** * AI 思维导图 Service 实现类 @@ -129,7 +129,13 @@ public class AiMindMapServiceImpl implements AiMindMapService { if (model == null) { model = modalService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType()); } - Assert.notNull(model, "[AI] 获取不到模型"); + // 校验模型存在、且合法 + if (model == null) { + throw exception(MODEL_NOT_EXISTS); + } + if (ObjUtil.notEqual(model.getType(), AiModelTypeEnum.CHAT.getType())) { + throw exception(MODEL_USE_TYPE_ERROR); + } return model; } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java index 4c38cd94d2..787f8d2046 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.ai.service.write; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.ai.core.enums.AiModelTypeEnum; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; @@ -42,7 +42,7 @@ import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.WRITE_NOT_EXISTS; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; /** * AI 写作 Service 实现类 @@ -115,7 +115,13 @@ public class AiWriteServiceImpl implements AiWriteService { if (model == null) { model = modalService.getRequiredDefaultModel(AiModelTypeEnum.CHAT.getType()); } - Assert.notNull(model, "[AI] 获取不到模型"); + // 校验模型存在、且合法 + if (model == null) { + throw exception(MODEL_NOT_EXISTS); + } + if (ObjUtil.notEqual(model.getType(), AiModelTypeEnum.CHAT.getType())) { + throw exception(MODEL_USE_TYPE_ERROR); + } return model; } From 6ccd0ca61e6a3540c01dd61d7f7dede71e849332 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 4 Mar 2025 23:12:24 +0800 Subject: [PATCH 29/51] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E3=80=91AI=EF=BC=9A=E4=BC=98=E5=8C=96=20pom=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E7=9A=84=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/AiChatMessageServiceImpl.java | 1 - .../yudao-spring-boot-starter-ai/pom.xml | 102 +++++++++--------- .../src/main/resources/application.yaml | 6 -- 3 files changed, 50 insertions(+), 59 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java index 2298d9b1d9..51bcc45d44 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -120,7 +120,6 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext()); - // 3.2 召回段落 List segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId()); diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index 156d3f0769..2d1a0af3aa 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -20,10 +20,11 @@ - ${spring-ai.groupId} - spring-ai-zhipuai-spring-boot-starter - ${spring-ai.version} + cn.iocoder.boot + yudao-common + + ${spring-ai.groupId} spring-ai-openai-spring-boot-starter @@ -44,16 +45,53 @@ spring-ai-stability-ai-spring-boot-starter ${spring-ai.version} - - - - - - - - - + + com.alibaba.cloud.ai + spring-ai-alibaba-starter + 1.0.0-M5.1 + + + + ${spring-ai.groupId} + spring-ai-qianfan-spring-boot-starter + ${spring-ai.version} + + + + ${spring-ai.groupId} + spring-ai-zhipuai-spring-boot-starter + ${spring-ai.version} + + + + + + ${spring-ai.groupId} + spring-ai-qdrant-store + ${spring-ai.version} + + + + + ${spring-ai.groupId} + spring-ai-redis-store + ${spring-ai.version} + + + cn.iocoder.boot + yudao-spring-boot-starter-redis + + + + + org.springframework.ai + spring-ai-milvus-store + ${spring-ai.version} + + + + ${spring-ai.groupId} spring-ai-tika-document-reader ${spring-ai.version} @@ -69,46 +107,6 @@ - - ${spring-ai.groupId} - spring-ai-redis-store - ${spring-ai.version} - - - - cn.iocoder.boot - yudao-spring-boot-starter-redis - - - - cn.iocoder.boot - yudao-common - - - - ${spring-ai.groupId} - spring-ai-qianfan-spring-boot-starter - ${spring-ai.version} - - - com.alibaba.cloud.ai - spring-ai-alibaba-starter - 1.0.0-M5.1 - - - - - - com.alibaba - dashscope-sdk-java - 2.14.0 - - - org.slf4j - slf4j-simple - - - diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index b3792c4c16..850b40906c 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -151,12 +151,6 @@ spring: redis: index: default-index prefix: "default:" - embedding: - transformer: - onnx: - model-uri: https://raw.gitcode.com/yudaocode/yudao-demo/raw/master/yudao-static/ai/model.onnx - tokenizer: - uri: https://raw.gitcode.com/yudaocode/yudao-demo/raw/master/yudao-static/ai/tokenizer.json qianfan: # 文心一言 api-key: x0cuLZ7XsaTCU08vuJWO87Lg secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK From 44bcc9476d7a3a1cdbebfab206640b2dac470c48 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 6 Mar 2025 22:22:22 +0800 Subject: [PATCH 30/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E5=A2=9E=E5=8A=A0=20QdrantVectorS?= =?UTF-8?q?tore=20=E5=90=91=E9=87=8F=E5=BA=93=E7=9A=84=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AiKnowledgeDocumentServiceImpl.java | 2 - .../AiKnowledgeSegmentServiceImpl.java | 11 ++-- .../ai/service/model/AiModelServiceImpl.java | 5 +- .../yudao-spring-boot-starter-ai/pom.xml | 1 + .../ai/config/YudaoAiAutoConfiguration.java | 5 +- .../ai/core/factory/AiModelFactoryImpl.java | 51 ++++++++++++++++++- .../src/main/resources/application-dev.yaml | 5 ++ .../src/main/resources/application-local.yaml | 4 +- .../src/main/resources/application.yaml | 6 +++ 9 files changed, 78 insertions(+), 12 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index b1513a9bd4..48dd78dbb9 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -54,7 +54,6 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic private AiKnowledgeService knowledgeService; @Override - @Transactional(rollbackFor = Exception.class) public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) { // 1. 校验参数 knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId()); @@ -74,7 +73,6 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic } @Override - @Transactional(rollbackFor = Exception.class) public List createKnowledgeDocumentList(AiKnowledgeDocumentCreateListReqVO createListReqVO) { // 1. 校验参数 knowledgeService.validateKnowledgeExists(createListReqVO.getKnowledgeId()); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index 910ff1249a..3b5ed91d55 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -115,6 +115,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService segmentMapper.updateById(newSegment); // 3.2 重新向量化,必须开启状态 if (CommonStatusEnum.isEnable(oldSegment.getStatus())) { + newSegment.setKnowledgeId(oldSegment.getKnowledgeId()).setDocumentId(oldSegment.getDocumentId()); writeVectorStore(vectorStore, newSegment, new Document(newSegment.getContent())); } } @@ -156,9 +157,10 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService private void writeVectorStore(VectorStore vectorStore, AiKnowledgeSegmentDO segmentDO, Document segment) { // 1. 向量存储 - segment.getMetadata().put(VECTOR_STORE_METADATA_KNOWLEDGE_ID, segmentDO.getKnowledgeId()); - segment.getMetadata().put(VECTOR_STORE_METADATA_DOCUMENT_ID, segmentDO.getDocumentId()); - segment.getMetadata().put(VECTOR_STORE_METADATA_SEGMENT_ID, segmentDO.getId()); + // 为什么要 toString 呢?因为部分 VectorStore 实现,不支持 Long 类型,例如说 QdrantVectorStore + segment.getMetadata().put(VECTOR_STORE_METADATA_KNOWLEDGE_ID, segmentDO.getKnowledgeId().toString()); + segment.getMetadata().put(VECTOR_STORE_METADATA_DOCUMENT_ID, segmentDO.getDocumentId().toString()); + segment.getMetadata().put(VECTOR_STORE_METADATA_SEGMENT_ID, segmentDO.getId().toString()); vectorStore.add(List.of(segment)); // 2. 更新向量 ID @@ -190,7 +192,8 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService .similarityThreshold( ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold())) .filterExpression(new FilterExpressionBuilder() - .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId()).build()) + .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId().toString()) + .build()) .build()); if (CollUtil.isEmpty(documents)) { return ListUtil.empty(); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java index 8583984c94..16338ca3dc 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java @@ -16,8 +16,8 @@ import jakarta.annotation.Resource; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; -import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -162,7 +162,8 @@ public class AiModelServiceImpl implements AiModelService { platform, apiKey.getApiKey(), apiKey.getUrl(), model.getModel()); // 创建或获取 VectorStore 对象 - return modelFactory.getOrCreateVectorStore(SimpleVectorStore.class, embeddingModel); +// return modelFactory.getOrCreateVectorStore(SimpleVectorStore.class, embeddingModel); + return modelFactory.getOrCreateVectorStore(QdrantVectorStore.class, embeddingModel); } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index 2d1a0af3aa..ffcf3941e9 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -70,6 +70,7 @@ ${spring-ai.groupId} spring-ai-qdrant-store ${spring-ai.version} + diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 73c2b8db9b..ccad732e53 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -11,6 +11,7 @@ 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.qdrant.QdrantVectorStoreProperties; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi; @@ -29,7 +30,9 @@ import org.springframework.context.annotation.Lazy; * @author fansili */ @AutoConfiguration -@EnableConfigurationProperties(YudaoAiProperties.class) +@EnableConfigurationProperties({YudaoAiProperties.class, + QdrantVectorStoreProperties.class // 解析 Qdrant 配置 +}) @Slf4j public class YudaoAiAutoConfiguration { diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index ffc052c5d3..6b0a1c3187 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -5,6 +5,7 @@ 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.ReflectUtil; import cn.hutool.core.util.RuntimeUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; @@ -26,6 +27,9 @@ 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.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; @@ -33,11 +37,14 @@ import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionPr 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.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration; +import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties; import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration; import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiConnectionProperties; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; 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.ollama.OllamaChatModel; @@ -57,10 +64,15 @@ 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.observation.DefaultVectorStoreObservationConvention; +import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention; +import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiImageModel; 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.web.client.RestClient; import java.io.File; @@ -214,13 +226,14 @@ public class AiModelFactoryImpl implements AiModelFactory { @Override public VectorStore getOrCreateVectorStore(Class type, EmbeddingModel embeddingModel) { - // String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, - // url); String cacheKey = buildClientCacheKey(VectorStore.class, embeddingModel, type); return Singleton.get(cacheKey, (Func0) () -> { if (type == SimpleVectorStore.class) { return buildSimpleVectorStore(embeddingModel); } + if (type == QdrantVectorStore.class) { + return buildQdrantVectorStore(embeddingModel); + } throw new IllegalArgumentException(StrUtil.format("未知类型({})", type)); // TODO @芋艿:先临时使用 store // TODO @芋艿:@xin:后续看看,是不是切到阿里云之类的 @@ -456,4 +469,38 @@ public class AiModelFactoryImpl implements AiModelFactory { return vectorStore; } + private QdrantVectorStore buildQdrantVectorStore(EmbeddingModel embeddingModel) { + QdrantVectorStoreAutoConfiguration configuration = new QdrantVectorStoreAutoConfiguration(); + QdrantVectorStoreProperties vectorStoreProperties = SpringUtil.getBean(QdrantVectorStoreProperties.class); + // 参考 QdrantVectorStoreAutoConfiguration 实现,创建 QdrantClient 对象 + QdrantGrpcClient.Builder grpcClientBuilder = QdrantGrpcClient.newBuilder( + vectorStoreProperties.getHost(), vectorStoreProperties.getPort(), vectorStoreProperties.isUseTls()); + if (StrUtil.isNotEmpty(vectorStoreProperties.getApiKey())) { + grpcClientBuilder.withApiKey(vectorStoreProperties.getApiKey()); + } + QdrantClient qdrantClient = new QdrantClient(grpcClientBuilder.build()); + // 参考 QdrantVectorStoreAutoConfiguration 实现,实现 batchingStrategy + BatchingStrategy batchingStrategy = ReflectUtil.invoke(configuration, "batchingStrategy"); + + // 创建 QdrantVectorStore 对象 + ObjectProvider observationRegistry = new ObjectProvider<>() { + + @Override + public ObservationRegistry getObject() throws BeansException { + return SpringUtil.getBean(ObservationRegistry.class); + } + + }; + ObjectProvider customObservationConvention = new ObjectProvider<>() { + + @Override + public VectorStoreObservationConvention getObject() throws BeansException { + return new DefaultVectorStoreObservationConvention(); + } + + }; + return configuration.vectorStore(embeddingModel, vectorStoreProperties, qdrantClient, + observationRegistry, customObservationConvention, batchingStrategy); + } + } diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index cd8652c79e..9377af95cd 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -4,6 +4,11 @@ server: --- #################### 数据库相关配置 #################### spring: + spring: + autoconfigure: + exclude: + - org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 + - org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 # 数据源配置项 datasource: druid: # Druid 【监控】相关的全局配置 diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 885f73db59..60564f094b 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -3,13 +3,15 @@ server: --- #################### 数据库相关配置 #################### spring: - # 数据源配置项 autoconfigure: exclude: - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 默认 local 环境,不开启 Quartz 的自动配置 - de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration # 禁用 Spring Boot Admin 的 Server 的自动配置 - de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration # 禁用 Spring Boot Admin 的 Server UI 的自动配置 - de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置 + - org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 + - org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 + # 数据源配置项 datasource: druid: # Druid 【监控】相关的全局配置 web-stat-filter: diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 850b40906c..08888ffb9d 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -151,6 +151,12 @@ spring: redis: index: default-index prefix: "default:" + qdrant: + collection-name: knowledge_segment + host: 127.0.0.1 + port: 6334 + use-tls: false + api-key: qianfan: # 文心一言 api-key: x0cuLZ7XsaTCU08vuJWO87Lg secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK From 588c9fe323e2f48f1c3a6fa7159755279049cc09 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 8 Mar 2025 22:09:16 +0800 Subject: [PATCH 31/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E5=A2=9E=E5=8A=A0=20RedisVectorSt?= =?UTF-8?q?ore=20=E5=90=91=E9=87=8F=E5=BA=93=E7=9A=84=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AiKnowledgeSegmentServiceImpl.java | 14 ++- .../ai/service/model/AiModelService.java | 4 +- .../ai/service/model/AiModelServiceImpl.java | 8 +- .../ai/config/YudaoAiAutoConfiguration.java | 31 +----- .../ai/core/factory/AiModelFactory.java | 9 +- .../ai/core/factory/AiModelFactoryImpl.java | 100 ++++++++++++------ .../src/main/resources/application.yaml | 8 +- 7 files changed, 103 insertions(+), 71 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index 3b5ed91d55..efdd86203f 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -32,6 +32,7 @@ import org.springframework.stereotype.Service; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -48,9 +49,14 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGM @Slf4j public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService { - public static final String VECTOR_STORE_METADATA_KNOWLEDGE_ID = "knowledgeId"; - public static final String VECTOR_STORE_METADATA_DOCUMENT_ID = "documentId"; - public static final String VECTOR_STORE_METADATA_SEGMENT_ID = "segmentId"; + private static final String VECTOR_STORE_METADATA_KNOWLEDGE_ID = "knowledgeId"; + private static final String VECTOR_STORE_METADATA_DOCUMENT_ID = "documentId"; + private static final String VECTOR_STORE_METADATA_SEGMENT_ID = "segmentId"; + + private static final Map> VECTOR_STORE_METADATA_TYPES = Map.of( + VECTOR_STORE_METADATA_KNOWLEDGE_ID, String.class, + VECTOR_STORE_METADATA_DOCUMENT_ID, String.class, + VECTOR_STORE_METADATA_SEGMENT_ID, String.class); @Resource private AiKnowledgeSegmentMapper segmentMapper; @@ -257,7 +263,7 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService } private VectorStore getVectorStoreById(AiKnowledgeDO knowledge) { - return modelService.getOrCreateVectorStore(knowledge.getEmbeddingModelId()); + return modelService.getOrCreateVectorStore(knowledge.getEmbeddingModelId(), VECTOR_STORE_METADATA_TYPES); } private VectorStore getVectorStoreById(Long knowledgeId) { diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java index 9e6185d450..127f72cc46 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java @@ -13,6 +13,7 @@ import org.springframework.ai.vectorstore.VectorStore; import javax.annotation.Nullable; import java.util.List; +import java.util.Map; /** * AI 模型 Service 接口 @@ -125,8 +126,9 @@ public interface AiModelService { * 获得 VectorStore 对象 * * @param id 编号 + * @param metadataFields 元数据的定义 * @return VectorStore 对象 */ - VectorStore getOrCreateVectorStore(Long id); + VectorStore getOrCreateVectorStore(Long id, Map> metadataFields); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java index 16338ca3dc..d6649a4b36 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java @@ -22,6 +22,7 @@ import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.util.List; +import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; @@ -151,7 +152,7 @@ public class AiModelServiceImpl implements AiModelService { } @Override - public VectorStore getOrCreateVectorStore(Long id) { + public VectorStore getOrCreateVectorStore(Long id, Map> metadataFields) { // 获取模型 + 密钥 AiModelDO model = validateModel(id); AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId()); @@ -162,8 +163,9 @@ public class AiModelServiceImpl implements AiModelService { platform, apiKey.getApiKey(), apiKey.getUrl(), model.getModel()); // 创建或获取 VectorStore 对象 -// return modelFactory.getOrCreateVectorStore(SimpleVectorStore.class, embeddingModel); - return modelFactory.getOrCreateVectorStore(QdrantVectorStore.class, embeddingModel); +// return modelFactory.getOrCreateVectorStore(SimpleVectorStore.class, embeddingModel, metadataFields); + return modelFactory.getOrCreateVectorStore(QdrantVectorStore.class, embeddingModel, metadataFields); +// return modelFactory.getOrCreateVectorStore(RedisVectorStore.class, embeddingModel, metadataFields); } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index ccad732e53..38e6eb2e33 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -12,12 +12,12 @@ 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.qdrant.QdrantVectorStoreProperties; +import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties; 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.ai.transformer.splitter.TokenTextSplitter; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -31,7 +31,8 @@ import org.springframework.context.annotation.Lazy; */ @AutoConfiguration @EnableConfigurationProperties({YudaoAiProperties.class, - QdrantVectorStoreProperties.class // 解析 Qdrant 配置 + QdrantVectorStoreProperties.class, // 解析 Qdrant 配置 + RedisVectorStoreProperties.class, // 解析 Redis 配置 }) @Slf4j public class YudaoAiAutoConfiguration { @@ -200,32 +201,6 @@ public class YudaoAiAutoConfiguration { // return new TransformersEmbeddingModel(MetadataMode.EMBED); // } - /** - * TODO @xin 默认版本先不弄,目前都先取对应的 EmbeddingModel - */ -// @Bean -// @Lazy // TODO 芋艿:临时注释,避免无法启动 -// public RedisVectorStore vectorStore(TransformersEmbeddingModel embeddingModel, RedisVectorStoreProperties properties, -// RedisProperties redisProperties) { -// var config = RedisVectorStore.RedisVectorStoreConfig.builder() -// .withIndexName(properties.getIndex()) -// .withPrefix(properties.getPrefix()) -// .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC)) -// .build(); -// -// RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel, -// new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), -// properties.isInitializeSchema()); -// redisVectorStore.afterPropertiesSet(); -// return redisVectorStore; -// } - @Bean - @Lazy // TODO 芋艿:临时注释,避免无法启动 - public TokenTextSplitter tokenTextSplitter() { - //TODO @xin 配置提取 - return new TokenTextSplitter(500, 100, 5, 10000, true); - } - @Bean @Lazy // TODO 芋艿:临时注释,避免无法启动 public TokenCountEstimator tokenCountEstimator() { diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java index 3a8bf5e04d..66ab41def7 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java @@ -8,6 +8,8 @@ import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; import org.springframework.ai.vectorstore.VectorStore; +import java.util.Map; + /** * AI Model 模型工厂的接口类 * @@ -96,13 +98,16 @@ public interface AiModelFactory { /** * 基于指定配置,获得 VectorStore 对象 - *

+ * * 如果不存在,则进行创建 * * @param type 向量存储类型 * @param embeddingModel 向量模型 + * @param metadataFields 元数据字段 * @return VectorStore 对象 */ - VectorStore getOrCreateVectorStore(Class type, EmbeddingModel embeddingModel); + VectorStore getOrCreateVectorStore(Class type, + EmbeddingModel embeddingModel, + Map> metadataFields); } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index 6b0a1c3187..f7c77c45b8 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -19,6 +19,7 @@ import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; 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 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; @@ -39,12 +40,13 @@ import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration; 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.autoconfigure.zhipuai.ZhiPuAiConnectionProperties; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; 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.ollama.OllamaChatModel; @@ -67,20 +69,26 @@ import org.springframework.ai.vectorstore.VectorStore; 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.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiImageModel; 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; + /** * AI Model 模型工厂的实现类 * @@ -225,7 +233,9 @@ public class AiModelFactoryImpl implements AiModelFactory { } @Override - public VectorStore getOrCreateVectorStore(Class type, EmbeddingModel embeddingModel) { + public VectorStore getOrCreateVectorStore(Class type, + EmbeddingModel embeddingModel, + Map> metadataFields) { String cacheKey = buildClientCacheKey(VectorStore.class, embeddingModel, type); return Singleton.get(cacheKey, (Func0) () -> { if (type == SimpleVectorStore.class) { @@ -234,23 +244,10 @@ public class AiModelFactoryImpl implements AiModelFactory { if (type == QdrantVectorStore.class) { return buildQdrantVectorStore(embeddingModel); } + if (type == RedisVectorStore.class) { + return buildRedisVectorStore(embeddingModel, metadataFields); + } throw new IllegalArgumentException(StrUtil.format("未知类型({})", type)); - // TODO @芋艿:先临时使用 store - // TODO @芋艿:@xin:后续看看,是不是切到阿里云之类的 - // String prefix = StrUtil.format("{}#{}:", platform.getPlatform(), apiKey); - // var config = RedisVectorStore.RedisVectorStoreConfig.builder() - // .withIndexName(cacheKey) - // .withPrefix(prefix) - // .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", - // Schema.FieldType.NUMERIC)) - // .build(); - // RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); - // RedisVectorStore redisVectorStore = new RedisVectorStore(config, - // embeddingModel, - // new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), - // true); - // redisVectorStore.afterPropertiesSet(); - // return redisVectorStore; }); } @@ -469,21 +466,65 @@ public class AiModelFactoryImpl implements AiModelFactory { return vectorStore; } + /** + * 参考 {@link QdrantVectorStoreAutoConfiguration} 的 vectorStore 方法 + */ + @SneakyThrows private QdrantVectorStore buildQdrantVectorStore(EmbeddingModel embeddingModel) { QdrantVectorStoreAutoConfiguration configuration = new QdrantVectorStoreAutoConfiguration(); - QdrantVectorStoreProperties vectorStoreProperties = SpringUtil.getBean(QdrantVectorStoreProperties.class); + QdrantVectorStoreProperties properties = SpringUtil.getBean(QdrantVectorStoreProperties.class); // 参考 QdrantVectorStoreAutoConfiguration 实现,创建 QdrantClient 对象 QdrantGrpcClient.Builder grpcClientBuilder = QdrantGrpcClient.newBuilder( - vectorStoreProperties.getHost(), vectorStoreProperties.getPort(), vectorStoreProperties.isUseTls()); - if (StrUtil.isNotEmpty(vectorStoreProperties.getApiKey())) { - grpcClientBuilder.withApiKey(vectorStoreProperties.getApiKey()); + properties.getHost(), properties.getPort(), properties.isUseTls()); + if (StrUtil.isNotEmpty(properties.getApiKey())) { + grpcClientBuilder.withApiKey(properties.getApiKey()); } QdrantClient qdrantClient = new QdrantClient(grpcClientBuilder.build()); - // 参考 QdrantVectorStoreAutoConfiguration 实现,实现 batchingStrategy - BatchingStrategy batchingStrategy = ReflectUtil.invoke(configuration, "batchingStrategy"); - // 创建 QdrantVectorStore 对象 - ObjectProvider observationRegistry = new ObjectProvider<>() { + QdrantVectorStore vectorStore = configuration.vectorStore(embeddingModel, properties, qdrantClient, + getObservationRegistry(), getCustomObservationConvention(), + ReflectUtil.invoke(configuration, "batchingStrategy")); + // 初始化索引 + vectorStore.afterPropertiesSet(); + return vectorStore; + } + + /** + * 参考 {@link RedisVectorStoreAutoConfiguration} 的 vectorStore 方法 + */ + private RedisVectorStore buildRedisVectorStore(EmbeddingModel embeddingModel, + Map> 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(ReflectUtil.invoke(configuration, "batchingStrategy")) + .build(); + // 初始化索引 + redisVectorStore.afterPropertiesSet(); + return redisVectorStore; + } + + private static ObjectProvider getObservationRegistry() { + return new ObjectProvider<>() { @Override public ObservationRegistry getObject() throws BeansException { @@ -491,16 +532,15 @@ public class AiModelFactoryImpl implements AiModelFactory { } }; - ObjectProvider customObservationConvention = new ObjectProvider<>() { + } + private static ObjectProvider getCustomObservationConvention() { + return new ObjectProvider<>() { @Override public VectorStoreObservationConvention getObject() throws BeansException { return new DefaultVectorStoreObservationConvention(); } - }; - return configuration.vectorStore(embeddingModel, vectorStoreProperties, qdrantClient, - observationRegistry, customObservationConvention, batchingStrategy); } } diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 08888ffb9d..4120a73467 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -149,10 +149,12 @@ spring: ai: vectorstore: # 向量存储 redis: - index: default-index - prefix: "default:" + initialize-schema: true + index: knowledge_index # Redis 中向量索引的名称:用于存储和检索向量数据的索引标识符,所有相关的向量搜索操作都会基于这个索引进行 + prefix: "knowledge_segment:" # Redis 中存储向量数据的键名前缀:这个前缀会添加到每个存储在 Redis 中的向量数据键名前,每个 document 都是一个 hash 结构 qdrant: - collection-name: knowledge_segment + initialize-schema: true + collection-name: knowledge_segment # Qdrant 中向量集合的名称:用于存储向量数据的集合标识符,所有相关的向量操作都会在这个集合中进行 host: 127.0.0.1 port: 6334 use-tls: false From f2ee2008e6d84bf9d30113592a239803ff0fa5d5 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 9 Mar 2025 09:27:33 +0800 Subject: [PATCH 32/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E5=A2=9E=E5=8A=A0=20MilvusVectorS?= =?UTF-8?q?tore=20=E5=90=91=E9=87=8F=E5=BA=93=E7=9A=84=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/service/model/AiModelServiceImpl.java | 12 ++-- .../yudao-spring-boot-starter-ai/pom.xml | 1 - .../ai/config/YudaoAiAutoConfiguration.java | 20 ++++--- .../ai/core/factory/AiModelFactoryImpl.java | 55 +++++++++++++++++-- .../src/main/resources/application.yaml | 9 ++- 5 files changed, 75 insertions(+), 22 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java index d6649a4b36..b73567a1c5 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java @@ -17,7 +17,7 @@ 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 org.springframework.ai.vectorstore.qdrant.QdrantVectorStore; +import org.springframework.ai.vectorstore.milvus.MilvusVectorStore; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -114,8 +114,7 @@ public class AiModelServiceImpl implements AiModelService { } @Override - public List getModelListByStatusAndType(Integer status, Integer type, - String platform) { + public List getModelListByStatusAndType(Integer status, Integer type, String platform) { return modelMapper.selectListByStatusAndType(status, type, platform); } @@ -163,9 +162,10 @@ public class AiModelServiceImpl implements AiModelService { platform, apiKey.getApiKey(), apiKey.getUrl(), model.getModel()); // 创建或获取 VectorStore 对象 -// return modelFactory.getOrCreateVectorStore(SimpleVectorStore.class, embeddingModel, metadataFields); - return modelFactory.getOrCreateVectorStore(QdrantVectorStore.class, embeddingModel, metadataFields); -// return modelFactory.getOrCreateVectorStore(RedisVectorStore.class, embeddingModel, metadataFields); + // return modelFactory.getOrCreateVectorStore(SimpleVectorStore.class, embeddingModel, metadataFields); + // return modelFactory.getOrCreateVectorStore(QdrantVectorStore.class, embeddingModel, metadataFields); + // return modelFactory.getOrCreateVectorStore(RedisVectorStore.class, embeddingModel, metadataFields); + return modelFactory.getOrCreateVectorStore(MilvusVectorStore.class, embeddingModel, metadataFields); } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index ffcf3941e9..2d1a0af3aa 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -70,7 +70,6 @@ ${spring-ai.groupId} spring-ai-qdrant-store ${spring-ai.version} - diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 38e6eb2e33..bea8e5bc0d 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -11,8 +11,12 @@ 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.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi; @@ -22,7 +26,6 @@ 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; -import org.springframework.context.annotation.Lazy; /** * 芋道 AI 自动配置 @@ -33,6 +36,7 @@ import org.springframework.context.annotation.Lazy; @EnableConfigurationProperties({YudaoAiProperties.class, QdrantVectorStoreProperties.class, // 解析 Qdrant 配置 RedisVectorStoreProperties.class, // 解析 Redis 配置 + MilvusVectorStoreProperties.class, MilvusServiceClientProperties.class // 解析 Milvus 配置 }) @Slf4j public class YudaoAiAutoConfiguration { @@ -193,18 +197,16 @@ public class YudaoAiAutoConfiguration { return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl()); } - // ========== rag 相关 ========== - // TODO @xin 免费版本 -// @Bean -// @Lazy // TODO 芋艿:临时注释,避免无法启动」 -// public TransformersEmbeddingModel transformersEmbeddingClient() { -// return new TransformersEmbeddingModel(MetadataMode.EMBED); -// } + // ========== RAG 相关 ========== @Bean - @Lazy // TODO 芋艿:临时注释,避免无法启动 public TokenCountEstimator tokenCountEstimator() { return new JTokkitTokenCountEstimator(); } + @Bean + public BatchingStrategy batchingStrategy() { + return new TokenCountBatchingStrategy(); + } + } \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index f7c77c45b8..7953157009 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -5,7 +5,6 @@ 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.ReflectUtil; import cn.hutool.core.util.RuntimeUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; @@ -29,6 +28,7 @@ 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; @@ -38,6 +38,10 @@ import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionPr 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.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; @@ -47,6 +51,7 @@ import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiConnectionProperties; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; 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.ollama.OllamaChatModel; @@ -66,6 +71,7 @@ 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; @@ -247,6 +253,9 @@ public class AiModelFactoryImpl implements AiModelFactory { if (type == RedisVectorStore.class) { return buildRedisVectorStore(embeddingModel, metadataFields); } + if (type == MilvusVectorStore.class) { + return buildMilvusVectorStore(embeddingModel); + } throw new IllegalArgumentException(StrUtil.format("未知类型({})", type)); }); } @@ -482,8 +491,7 @@ public class AiModelFactoryImpl implements AiModelFactory { QdrantClient qdrantClient = new QdrantClient(grpcClientBuilder.build()); // 创建 QdrantVectorStore 对象 QdrantVectorStore vectorStore = configuration.vectorStore(embeddingModel, properties, qdrantClient, - getObservationRegistry(), getCustomObservationConvention(), - ReflectUtil.invoke(configuration, "batchingStrategy")); + getObservationRegistry(), getCustomObservationConvention(), getBatchingStrategy()); // 初始化索引 vectorStore.afterPropertiesSet(); return vectorStore; @@ -516,13 +524,48 @@ public class AiModelFactoryImpl implements AiModelFactory { })) .observationRegistry(getObservationRegistry().getObject()) .customObservationConvention(getCustomObservationConvention().getObject()) - .batchingStrategy(ReflectUtil.invoke(configuration, "batchingStrategy")) + .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 getObservationRegistry() { return new ObjectProvider<>() { @@ -543,4 +586,8 @@ public class AiModelFactoryImpl implements AiModelFactory { }; } + private static BatchingStrategy getBatchingStrategy() { + return SpringUtil.getBean(BatchingStrategy.class); + } + } diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 4120a73467..419fab471b 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -157,8 +157,13 @@ spring: collection-name: knowledge_segment # Qdrant 中向量集合的名称:用于存储向量数据的集合标识符,所有相关的向量操作都会在这个集合中进行 host: 127.0.0.1 port: 6334 - use-tls: false - api-key: + milvus: + initialize-schema: true + database-name: default # Milvus 中数据库的名称 + collection-name: knowledge_segment # Milvus 中集合的名称:用于存储向量数据的集合标识符,所有相关的向量操作都会在这个集合中进行 + client: + host: 127.0.0.1 + port: 19530 qianfan: # 文心一言 api-key: x0cuLZ7XsaTCU08vuJWO87Lg secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK From f286a8139205815665ecc1c3090ae79e368360ee Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 9 Mar 2025 09:43:12 +0800 Subject: [PATCH 33/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E3=80=91AI=EF=BC=9A=E5=8D=87=E7=BA=A7=20tika=20?= =?UTF-8?q?=E7=89=88=E6=9C=AC=EF=BC=8C=E8=A7=A3=E5=86=B3=20pdf=20=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-dependencies/pom.xml | 2 +- .../java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index d6c0a642fc..54252350d6 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -60,7 +60,7 @@ 2.14.5 3.11.1 0.1.55 - 2.9.2 + 3.1.0 2.7.0 3.0.6 4.1.116.Final diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index b1a3a3b507..a96d305492 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -19,7 +19,7 @@ import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; public class AiUtils { public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) { - //noinspection EnhancedSwitchMigration + // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: // TODO @芋艿:tongyi 暂时没 maxTokens 选项 From 9126691ac0f699c8f4971b2b04f62ba539b2a962 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 9 Mar 2025 11:51:19 +0800 Subject: [PATCH 34/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E8=81=8A=E5=A4=A9=E8=A7=92?= =?UTF-8?q?=E8=89=B2=EF=BC=8C=E6=96=B0=E5=A2=9E=20document=20=E7=9A=84?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AiKnowledgeDocumentController.java | 13 +++++++ .../admin/model/AiChatRoleController.java | 2 +- .../model/vo/chatRole/AiChatRoleRespVO.java | 6 ++- .../vo/chatRole/AiChatRoleSaveMyReqVO.java | 5 +++ .../vo/chatRole/AiChatRoleSaveReqVO.java | 5 +++ .../ai/dal/dataobject/model/AiChatRoleDO.java | 13 +++++++ .../knowledge/AiKnowledgeDocumentMapper.java | 7 +++- .../knowledge/AiKnowledgeDocumentService.java | 8 ++++ .../AiKnowledgeDocumentServiceImpl.java | 6 +++ .../ai/service/model/AiChatRoleService.java | 8 ++-- .../service/model/AiChatRoleServiceImpl.java | 37 +++++++++++++++++-- 11 files changed, 100 insertions(+), 10 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java index cca856fe17..c52cfe2cf7 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -10,8 +11,10 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowl import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; +import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; @@ -22,6 +25,7 @@ import org.springframework.web.bind.annotation.*; import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @Tag(name = "管理后台 - AI 知识库文档") @RestController @@ -91,4 +95,13 @@ public class AiKnowledgeDocumentController { return success(true); } + @GetMapping("/simple-list") + @Operation(summary = "获得知识库文档的精简列表") + public CommonResult> getKnowledgeDocumentSimpleList() { + List list = documentService + .getKnowledgeDocumentListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, document -> new AiKnowledgeDocumentRespVO() + .setId(document.getId()).setName(document.getName()))); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java index 02f698b944..5714c5fedd 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java @@ -75,7 +75,7 @@ public class AiChatRoleController { @GetMapping("/category-list") @Operation(summary = "获得聊天角色的分类列表") public CommonResult> getChatRoleCategoryList() { - return success(chatRoleService.getChatRoleCategoryList()); + return success(chatRoleService.getChatRoleCategoryList()); } // ========== 角色管理 ========== diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java index d47c66d2bd..cbbed87c04 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java @@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; +import java.util.List; @Schema(description = "管理后台 - AI 聊天角色 Response VO") @Data @@ -20,7 +21,7 @@ public class AiChatRoleRespVO implements VO { private Long userId; @Schema(description = "模型编号", example = "17640") - @Trans(type = TransType.SIMPLE, target = AiModelDO.class, fields = {"name", "model"}, refs = {"modelName", "model"}) + @Trans(type = TransType.SIMPLE, target = AiModelDO.class, fields = { "name", "model" }, refs = { "modelName", "model" }) private Long modelId; @Schema(description = "模型名字", example = "张三") private String modelName; @@ -45,6 +46,9 @@ public class AiChatRoleRespVO implements VO { @Schema(description = "角色设定", requiredMode = Schema.RequiredMode.REQUIRED) private String systemMessage; + @Schema(description = "知识库文档编号列表", example = "1,2,3") + private List documentIds; + @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Boolean publicStatus; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java index 4673901d38..5f802202a8 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java @@ -5,6 +5,8 @@ import jakarta.validation.constraints.NotEmpty; import lombok.Data; import org.hibernate.validator.constraints.URL; +import java.util.List; + @Schema(description = "管理后台 - AI 聊天角色新增/修改【我的】 Request VO") @Data public class AiChatRoleSaveMyReqVO { @@ -29,4 +31,7 @@ public class AiChatRoleSaveMyReqVO { @NotEmpty(message = "角色设定不能为空") private String systemMessage; + @Schema(description = "知识库文档编号列表", example = "1,2,3") + private List documentIds; + } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java index bdda027ef2..e7ba7fd2d1 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java @@ -7,6 +7,8 @@ import lombok.*; import jakarta.validation.constraints.*; import org.hibernate.validator.constraints.URL; +import java.util.List; + @Schema(description = "管理后台 - AI 聊天角色新增/修改 Request VO") @Data public class AiChatRoleSaveReqVO { @@ -42,6 +44,9 @@ public class AiChatRoleSaveReqVO { @NotEmpty(message = "角色设定不能为空") private String systemMessage; + @Schema(description = "知识库文档编号列表", example = "1,2,3") + private List documentIds; + @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "是否公开不能为空") private Boolean publicStatus; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java index fd35b85795..006057e353 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java @@ -2,11 +2,16 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.model; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; +import java.util.List; + /** * AI 聊天角色 DO * @@ -62,6 +67,14 @@ public class AiChatRoleDO extends BaseDO { */ private Long modelId; + /** + * 知识库文档编号列表 + * + * 关联 {@link AiKnowledgeDocumentDO#getId()} 字段 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List documentIds; + /** * 是否公开 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java index eeacca6345..11a76cc57b 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java @@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import org.apache.ibatis.annotations.Mapper; import java.util.Collection; +import java.util.List; /** * AI 知识库文档 Mapper @@ -26,9 +27,13 @@ public interface AiKnowledgeDocumentMapper extends BaseMapperX ids) { - update( new LambdaUpdateWrapper() + update(new LambdaUpdateWrapper() .setSql(" retrieval_count = retrieval_count + 1") .in(AiKnowledgeDocumentDO::getId, ids)); } + default List selectListByStatus(Integer status) { + return selectList(AiKnowledgeDocumentDO::getStatus, status); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java index 8ff137b331..e1a45efc1a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -115,4 +115,12 @@ public interface AiKnowledgeDocumentService { return convertMap(getKnowledgeDocumentList(ids), AiKnowledgeDocumentDO::getId); } + /** + * 获取指定状态的文档列表 + * + * @param status 状态 + * @return 文档列表 + */ + List getKnowledgeDocumentListByStatus(Integer status); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 48dd78dbb9..3e47720711 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -7,6 +7,7 @@ import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; @@ -210,4 +211,9 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic return knowledgeDocumentMapper.selectByIds(ids); } + @Override + public List getKnowledgeDocumentListByStatus(Integer status) { + return knowledgeDocumentMapper.selectListByStatus(status); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java index 81c8d259b6..e7fecf6ac7 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java @@ -32,7 +32,7 @@ public interface AiChatRoleService { * 创建【我的】聊天角色 * * @param createReqVO 创建信息 - * @param userId 用户编号 + * @param userId 用户编号 * @return 编号 */ Long createChatRoleMy(AiChatRoleSaveMyReqVO createReqVO, Long userId); @@ -48,7 +48,7 @@ public interface AiChatRoleService { * 创建【我的】聊天角色 * * @param updateReqVO 更新信息 - * @param userId 用户编号 + * @param userId 用户编号 */ void updateChatRoleMy(AiChatRoleSaveMyReqVO updateReqVO, Long userId); @@ -62,7 +62,7 @@ public interface AiChatRoleService { /** * 删除【我的】聊天角色 * - * @param id 编号 + * @param id 编号 * @param userId 用户编号 */ void deleteChatRoleMy(Long id, Long userId); @@ -106,7 +106,7 @@ public interface AiChatRoleService { * 获得【我的】聊天角色分页 * * @param pageReqVO 分页查询 - * @param userId 用户编号 + * @param userId 用户编号 * @return 聊天角色分页 */ PageResult getChatRoleMyPage(AiChatRolePageReqVO pageReqVO, Long userId); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java index 2cf4d46d1c..ce93d4e1de 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java @@ -5,12 +5,14 @@ import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRolePageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveMyReqVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.dal.mysql.model.AiChatRoleMapper; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -35,8 +37,15 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { @Resource private AiChatRoleMapper chatRoleMapper; + @Resource + private AiKnowledgeDocumentService knowledgeDocumentService; + @Override public Long createChatRole(AiChatRoleSaveReqVO createReqVO) { + // 校验文档 + validateDocuments(createReqVO.getDocumentIds()); + + // 保存角色 AiChatRoleDO chatRole = BeanUtils.toBean(createReqVO, AiChatRoleDO.class); chatRoleMapper.insert(chatRole); return chatRole.getId(); @@ -44,6 +53,10 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { @Override public Long createChatRoleMy(AiChatRoleSaveMyReqVO createReqVO, Long userId) { + // 校验文档 + validateDocuments(createReqVO.getDocumentIds()); + + // 保存角色 AiChatRoleDO chatRole = BeanUtils.toBean(createReqVO, AiChatRoleDO.class).setUserId(userId) .setStatus(CommonStatusEnum.ENABLE.getStatus()).setPublicStatus(false); chatRoleMapper.insert(chatRole); @@ -54,7 +67,10 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { public void updateChatRole(AiChatRoleSaveReqVO updateReqVO) { // 校验存在 validateChatRoleExists(updateReqVO.getId()); - // 更新 + // 校验文档 + validateDocuments(updateReqVO.getDocumentIds()); + + // 更新角色 AiChatRoleDO updateObj = BeanUtils.toBean(updateReqVO, AiChatRoleDO.class); chatRoleMapper.updateById(updateObj); } @@ -66,12 +82,27 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { if (ObjectUtil.notEqual(chatRole.getUserId(), userId)) { throw exception(CHAT_ROLE_NOT_EXISTS); } + // 校验文档 + validateDocuments(updateReqVO.getDocumentIds()); // 更新 AiChatRoleDO updateObj = BeanUtils.toBean(updateReqVO, AiChatRoleDO.class); chatRoleMapper.updateById(updateObj); } + /** + * 校验文档是否存在 + * + * @param documentIds 文档编号列表 + */ + private void validateDocuments(List documentIds) { + if (CollUtil.isEmpty(documentIds)) { + return; + } + // 校验文档是否存在 + documentIds.forEach(knowledgeDocumentService::validateKnowledgeDocumentExists); + } + @Override public void deleteChatRole(Long id) { // 校验存在 @@ -134,7 +165,8 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { @Override public List getChatRoleCategoryList() { List list = chatRoleMapper.selectListGroupByCategory(CommonStatusEnum.ENABLE.getStatus()); - return convertList(list, AiChatRoleDO::getCategory, role -> role != null && StrUtil.isNotBlank(role.getCategory())); + return convertList(list, AiChatRoleDO::getCategory, + role -> role != null && StrUtil.isNotBlank(role.getCategory())); } @Override @@ -143,4 +175,3 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { } } - From 3bb57edc85c48dcb6475ce7c7b98395a3682054b Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 9 Mar 2025 12:00:04 +0800 Subject: [PATCH 35/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E8=81=8A=E5=A4=A9=E8=A7=92?= =?UTF-8?q?=E8=89=B2=EF=BC=8C=E6=96=B0=E5=A2=9E=20knowledge=20=E7=9A=84?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../knowledge/AiKnowledgeController.java | 12 +++++++++ .../AiKnowledgeDocumentController.java | 19 +------------- .../model/vo/chatRole/AiChatRoleRespVO.java | 4 +-- .../vo/chatRole/AiChatRoleSaveMyReqVO.java | 4 +-- .../vo/chatRole/AiChatRoleSaveReqVO.java | 4 +-- .../ai/dal/dataobject/model/AiChatRoleDO.java | 8 +++--- .../mysql/knowledge/AiKnowledgeMapper.java | 6 +++++ .../knowledge/AiKnowledgeDocumentService.java | 8 ------ .../AiKnowledgeDocumentServiceImpl.java | 6 ----- .../service/knowledge/AiKnowledgeService.java | 10 +++++++ .../knowledge/AiKnowledgeServiceImpl.java | 7 +++++ .../service/model/AiChatRoleServiceImpl.java | 26 +++++++++---------- 12 files changed, 59 insertions(+), 55 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java index f3dc6f022e..39fb8d9430 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -17,7 +18,10 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.List; + import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; // TODO @芋艿:增加权限标识 @Tag(name = "管理后台 - AI 知识库") @@ -62,4 +66,12 @@ public class AiKnowledgeController { return success(true); } + @GetMapping("/simple-list") + @Operation(summary = "获得知识库的精简列表") + public CommonResult> getKnowledgeSimpleList() { + List list = knowledgeService.getKnowledgeSimpleListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, knowledge -> new AiKnowledgeRespVO() + .setId(knowledge.getId()).setName(knowledge.getName()))); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java index c52cfe2cf7..68fe49a8a7 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeDocumentController.java @@ -1,20 +1,13 @@ package cn.iocoder.yudao.module.ai.controller.admin.knowledge; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentRespVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; -import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateStatusReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.*; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; -import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; @@ -25,7 +18,6 @@ import org.springframework.web.bind.annotation.*; import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @Tag(name = "管理后台 - AI 知识库文档") @RestController @@ -95,13 +87,4 @@ public class AiKnowledgeDocumentController { return success(true); } - @GetMapping("/simple-list") - @Operation(summary = "获得知识库文档的精简列表") - public CommonResult> getKnowledgeDocumentSimpleList() { - List list = documentService - .getKnowledgeDocumentListByStatus(CommonStatusEnum.ENABLE.getStatus()); - return success(convertList(list, document -> new AiKnowledgeDocumentRespVO() - .setId(document.getId()).setName(document.getName()))); - } - } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java index cbbed87c04..908e9dcce4 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java @@ -46,8 +46,8 @@ public class AiChatRoleRespVO implements VO { @Schema(description = "角色设定", requiredMode = Schema.RequiredMode.REQUIRED) private String systemMessage; - @Schema(description = "知识库文档编号列表", example = "1,2,3") - private List documentIds; + @Schema(description = "引用的知识库编号列表", example = "1,2,3") + private List knowledgeIds; @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Boolean publicStatus; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java index 5f802202a8..a104421f5d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java @@ -31,7 +31,7 @@ public class AiChatRoleSaveMyReqVO { @NotEmpty(message = "角色设定不能为空") private String systemMessage; - @Schema(description = "知识库文档编号列表", example = "1,2,3") - private List documentIds; + @Schema(description = "引用的知识库编号列表", example = "1,2,3") + private List knowledgeIds; } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java index e7ba7fd2d1..6851564e06 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java @@ -44,8 +44,8 @@ public class AiChatRoleSaveReqVO { @NotEmpty(message = "角色设定不能为空") private String systemMessage; - @Schema(description = "知识库文档编号列表", example = "1,2,3") - private List documentIds; + @Schema(description = "引用的知识库编号列表", example = "1,2,3") + private List knowledgeIds; @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "是否公开不能为空") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java index 006057e353..81a679a2f3 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.model; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; -import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; @@ -68,12 +68,12 @@ public class AiChatRoleDO extends BaseDO { private Long modelId; /** - * 知识库文档编号列表 + * 引用的知识库编号列表 * - * 关联 {@link AiKnowledgeDocumentDO#getId()} 字段 + * 关联 {@link AiKnowledgeDO#getId()} 字段 */ @TableField(typeHandler = LongListTypeHandler.class) - private List documentIds; + private List knowledgeIds; /** * 是否公开 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java index 0d6022ed07..3433c0b973 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeMapper.java @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnow import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import org.apache.ibatis.annotations.Mapper; +import java.util.List; + /** * AI 知识库 Mapper * @@ -23,4 +25,8 @@ public interface AiKnowledgeMapper extends BaseMapperX { .orderByDesc(AiKnowledgeDO::getId)); } + default List selectListByStatus(Integer status) { + return selectList(AiKnowledgeDO::getStatus, status); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java index e1a45efc1a..8ff137b331 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -115,12 +115,4 @@ public interface AiKnowledgeDocumentService { return convertMap(getKnowledgeDocumentList(ids), AiKnowledgeDocumentDO::getId); } - /** - * 获取指定状态的文档列表 - * - * @param status 状态 - * @return 文档列表 - */ - List getKnowledgeDocumentListByStatus(Integer status); - } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 3e47720711..48dd78dbb9 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -7,7 +7,6 @@ import cn.hutool.http.HttpUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentCreateListReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; @@ -211,9 +210,4 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic return knowledgeDocumentMapper.selectByIds(ids); } - @Override - public List getKnowledgeDocumentListByStatus(Integer status) { - return knowledgeDocumentMapper.selectListByStatus(status); - } - } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java index d1a7f7d1c4..5336570d27 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java @@ -5,6 +5,8 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnow import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; +import java.util.List; + /** * AI 知识库-基础信息 Service 接口 * @@ -50,4 +52,12 @@ public interface AiKnowledgeService { */ PageResult getKnowledgePage(AiKnowledgePageReqVO pageReqVO); + /** + * 获得指定状态的知识库列表 + * + * @param status 状态 + * @return 知识库列表 + */ + List getKnowledgeSimpleListByStatus(Integer status); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java index 0bd4029850..448a0d6ba1 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java @@ -12,6 +12,8 @@ import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.List; + import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_NOT_EXISTS; @@ -76,4 +78,9 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { return knowledgeMapper.selectPage(pageReqVO); } + @Override + public List getKnowledgeSimpleListByStatus(Integer status) { + return knowledgeMapper.selectListByStatus(status); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java index ce93d4e1de..f0e07f2856 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java @@ -5,14 +5,13 @@ import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRolePageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveMyReqVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.dal.mysql.model.AiChatRoleMapper; -import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -23,7 +22,8 @@ import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; -import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_ROLE_DISABLE; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_ROLE_NOT_EXISTS; /** * AI 聊天角色 Service 实现类 @@ -38,12 +38,12 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { private AiChatRoleMapper chatRoleMapper; @Resource - private AiKnowledgeDocumentService knowledgeDocumentService; + private AiKnowledgeService knowledgeService; @Override public Long createChatRole(AiChatRoleSaveReqVO createReqVO) { // 校验文档 - validateDocuments(createReqVO.getDocumentIds()); + validateDocuments(createReqVO.getKnowledgeIds()); // 保存角色 AiChatRoleDO chatRole = BeanUtils.toBean(createReqVO, AiChatRoleDO.class); @@ -54,7 +54,7 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { @Override public Long createChatRoleMy(AiChatRoleSaveMyReqVO createReqVO, Long userId) { // 校验文档 - validateDocuments(createReqVO.getDocumentIds()); + validateDocuments(createReqVO.getKnowledgeIds()); // 保存角色 AiChatRoleDO chatRole = BeanUtils.toBean(createReqVO, AiChatRoleDO.class).setUserId(userId) @@ -68,7 +68,7 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { // 校验存在 validateChatRoleExists(updateReqVO.getId()); // 校验文档 - validateDocuments(updateReqVO.getDocumentIds()); + validateDocuments(updateReqVO.getKnowledgeIds()); // 更新角色 AiChatRoleDO updateObj = BeanUtils.toBean(updateReqVO, AiChatRoleDO.class); @@ -83,7 +83,7 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { throw exception(CHAT_ROLE_NOT_EXISTS); } // 校验文档 - validateDocuments(updateReqVO.getDocumentIds()); + validateDocuments(updateReqVO.getKnowledgeIds()); // 更新 AiChatRoleDO updateObj = BeanUtils.toBean(updateReqVO, AiChatRoleDO.class); @@ -91,16 +91,16 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { } /** - * 校验文档是否存在 + * 校验知识库是否存在 * - * @param documentIds 文档编号列表 + * @param knowledgeIds 知识库编号列表 */ - private void validateDocuments(List documentIds) { - if (CollUtil.isEmpty(documentIds)) { + private void validateDocuments(List knowledgeIds) { + if (CollUtil.isEmpty(knowledgeIds)) { return; } // 校验文档是否存在 - documentIds.forEach(knowledgeDocumentService::validateKnowledgeDocumentExists); + knowledgeIds.forEach(knowledgeService::validateKnowledgeExists); } @Override From cddaca586360a3cb924190c9ade7c5a55a1a420c Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 9 Mar 2025 19:02:17 +0800 Subject: [PATCH 36/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E8=81=8A=E5=A4=A9=E6=97=B6?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E7=9F=A5=E8=AF=86=E5=BA=93=E7=9A=84?= =?UTF-8?q?=E6=8B=BC=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/module/ai/enums/AiChatRoleEnum.java | 6 +- .../dataobject/chat/AiChatConversationDO.java | 11 +- .../dal/dataobject/chat/AiChatMessageDO.java | 9 +- .../chat/AiChatConversationServiceImpl.java | 2 +- .../chat/AiChatMessageServiceImpl.java | 169 +++++++++++------- 5 files changed, 111 insertions(+), 86 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java index 6cb98c5629..1479274959 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java @@ -35,11 +35,7 @@ public enum AiChatRoleEnum { ### 微信 除此之外不要任何解释性语句。 """), - - AI_KNOWLEDGE_ROLE("知识库助手", """ - 给你提供一些数据参考:{info},请回答我的问题。 - 请你跟进数据参考与工具返回结果回复用户的请求。 - """); + ; /** * 角色名 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java index a9c956deae..358e994104 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java @@ -1,9 +1,8 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -65,14 +64,6 @@ public class AiChatConversationDO extends BaseDO { */ private Long roleId; - // TODO @芋艿:可优化,绑定多个知识库。前提,spring ai 支持 RerankModel 的封装 - /** - * 知识库编号 - *

- * 关联 {@link AiKnowledgeDO#getId()} - */ - private Long knowledgeId; - /** * 模型编号 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java index a026ec8193..d121d85785 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java @@ -1,14 +1,14 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.*; import org.springframework.ai.chat.messages.MessageType; @@ -71,13 +71,12 @@ public class AiChatMessageDO extends BaseDO { */ private Long roleId; - /** - * 段落编号数组 + * 知识库段落编号数组 * * 关联 {@link AiKnowledgeSegmentDO#getId()} 字段 */ - @TableField(typeHandler = JacksonTypeHandler.class) + @TableField(typeHandler = LongListTypeHandler.class) private List segmentIds; /** diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java index 6483166a7f..6c35571c8f 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java @@ -68,7 +68,7 @@ public class AiChatConversationServiceImpl implements AiChatConversationService // 2. 创建 AiChatConversationDO 聊天对话 AiChatConversationDO conversation = new AiChatConversationDO().setUserId(userId).setPinned(false) - .setModelId(model.getId()).setModel(model.getModel()).setKnowledgeId(createReqVO.getKnowledgeId()) + .setModelId(model.getId()).setModel(model.getModel()) .setTemperature(model.getTemperature()).setMaxTokens(model.getMaxTokens()).setMaxContexts(model.getMaxContexts()); if (role != null) { conversation.setTitle(role.getName()).setRoleId(role.getId()).setSystemMessage(role.getSystemMessage()); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java index 51bcc45d44..b95f7cd214 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -14,12 +14,14 @@ import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessage import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper; -import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum; import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; +import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; +import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; import cn.iocoder.yudao.module.ai.service.model.AiModelService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; @@ -32,13 +34,13 @@ import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.StreamingChatModel; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.chat.prompt.PromptTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Flux; import java.time.LocalDateTime; import java.util.*; +import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error; @@ -56,12 +58,21 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_N @Slf4j public class AiChatMessageServiceImpl implements AiChatMessageService { + /** + * 知识库转 {@link UserMessage} 的内容模版 + */ + private static final String KNOWLEDGE_USER_MESSAGE_TEMPLATE = "使用 标记中的内容作为本次对话的参考:\n\n" + + "%s\n\n" + // 多个 的拼接 + "回答要求:\n- 避免提及你是从 获取的知识。"; + @Resource private AiChatMessageMapper chatMessageMapper; @Resource private AiChatConversationService chatConversationService; @Resource + private AiChatRoleService chatRoleService; + @Resource private AiModelService modalService; @Resource private AiKnowledgeSegmentService knowledgeSegmentService; @@ -69,118 +80,143 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { @Transactional(rollbackFor = Exception.class) public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) { // 1.1 校验对话存在 - AiChatConversationDO conversation = chatConversationService.validateChatConversationExists(sendReqVO.getConversationId()); + AiChatConversationDO conversation = chatConversationService + .validateChatConversationExists(sendReqVO.getConversationId()); if (ObjUtil.notEqual(conversation.getUserId(), userId)) { throw exception(CHAT_CONVERSATION_NOT_EXISTS); } List historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId()); // 1.2 校验模型 AiModelDO model = modalService.validateModel(conversation.getModelId()); - ChatModel chatModel = modalService.getChatModel(model.getKeyId()); + ChatModel chatModel = modalService.getChatModel(model.getId()); - // 2. 插入 user 发送消息 + // 2. 知识库找回 + List knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), + conversation); + + // 3. 插入 user 发送消息 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, - userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext()); + userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), + null); // 3.1 插入 assistant 接收消息 AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, - userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext()); + userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), + knowledgeSegments); - // 3.2 召回段落 - List segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId()); - - // 3.3 创建 chat 需要的 Prompt - Prompt prompt = buildPrompt(conversation, historyMessages, segmentList, model, sendReqVO); + // 3.2 创建 chat 需要的 Prompt + Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); ChatResponse chatResponse = chatModel.call(prompt); - // 3.4 段式返回 + // 3.3 段式返回 String newContent = chatResponse.getResult().getOutput().getText(); - chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId)).setContent(newContent)); - return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) - .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent)); + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent)); + return new AiChatMessageSendRespVO() + .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) + .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) + .setContent(newContent)); } @Override - public Flux> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId) { + public Flux> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, + Long userId) { // 1.1 校验对话存在 - AiChatConversationDO conversation = chatConversationService.validateChatConversationExists(sendReqVO.getConversationId()); + AiChatConversationDO conversation = chatConversationService + .validateChatConversationExists(sendReqVO.getConversationId()); if (ObjUtil.notEqual(conversation.getUserId(), userId)) { throw exception(CHAT_CONVERSATION_NOT_EXISTS); } List historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId()); // 1.2 校验模型 AiModelDO model = modalService.validateModel(conversation.getModelId()); - StreamingChatModel chatModel = modalService.getChatModel(model.getKeyId()); + StreamingChatModel chatModel = modalService.getChatModel(model.getId()); - // 2. 插入 user 发送消息 + // 2. 知识库找回 + List knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), + conversation); + + // 3. 插入 user 发送消息 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, - userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext()); + userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), + null); - // 3.1 插入 assistant 接收消息 + // 4.1 插入 assistant 接收消息 AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, - userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext()); + userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), + knowledgeSegments); - // 3.2 召回段落 - List segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId()); - - // 3.3 构建 Prompt,并进行调用 - Prompt prompt = buildPrompt(conversation, historyMessages, segmentList, model, sendReqVO); + // 4.2 构建 Prompt,并进行调用 + Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); Flux streamResponse = chatModel.stream(prompt); - // 3.4 流式返回 + // 4.3 流式返回 StringBuffer contentBuffer = new StringBuffer(); return streamResponse.map(chunk -> { String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 contentBuffer.append(newContent); // 响应结果 - return success(new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) - .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent))); + return success(new AiChatMessageSendRespVO() + .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) + .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) + .setContent(newContent))); }).doOnComplete(() -> { // 忽略租户,因为 Flux 异步无法透传租户 - TenantUtils.executeIgnore(() -> - chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId)) - .setContent(contentBuffer.toString()))); + TenantUtils.executeIgnore(() -> chatMessageMapper.updateById( + new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString()))); }).doOnError(throwable -> { log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable); // 忽略租户,因为 Flux 异步无法透传租户 - TenantUtils.executeIgnore(() -> - chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage()))); + TenantUtils.executeIgnore(() -> chatMessageMapper.updateById( + new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage()))); }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR))); } - private List recallSegment(String content, Long knowledgeId) { - if (Objects.isNull(knowledgeId)) { + private List recallKnowledgeSegment(String content, + AiChatConversationDO conversation) { + // 1. 查询聊天角色 + if (conversation == null || conversation.getRoleId() == null) { return Collections.emptyList(); } -// return knowledgeSegmentService.similaritySearch(new AiKnowledgeSegmentSearchReqVO().setKnowledgeId(knowledgeId).setContent(content)); - return null; - } - - private Prompt buildPrompt(AiChatConversationDO conversation, List messages, List segmentList, - AiModelDO model, AiChatMessageSendReqVO sendReqVO) { - // 1. 构建 Prompt Message 列表 - List chatMessages = new ArrayList<>(); - - // 1.1 召回内容消息构建 - if (CollUtil.isNotEmpty(segmentList)) { - PromptTemplate promptTemplate = new PromptTemplate(AiChatRoleEnum.AI_KNOWLEDGE_ROLE.getSystemMessage()); - StringBuilder infoBuilder = StrUtil.builder(); - segmentList.forEach(segment -> infoBuilder.append(System.lineSeparator()).append(segment.getContent())); - Message message = promptTemplate.createMessage(Map.of("info", infoBuilder.toString())); - chatMessages.add(message); + AiChatRoleDO role = chatRoleService.getChatRole(conversation.getRoleId()); + if (role == null || CollUtil.isEmpty(role.getKnowledgeIds())) { + return Collections.emptyList(); } - // 1.2 system context 角色设定 + // 2. 遍历找回 + List knowledgeSegments = new ArrayList<>(); + for (Long knowledgeId : role.getKnowledgeIds()) { + knowledgeSegments.addAll(knowledgeSegmentService.searchKnowledgeSegment(new AiKnowledgeSegmentSearchReqBO() + .setKnowledgeId(knowledgeId).setContent(content))); + } + return knowledgeSegments; + } + + private Prompt buildPrompt(AiChatConversationDO conversation, List messages, + List knowledgeSegments, + AiModelDO model, AiChatMessageSendReqVO sendReqVO) { + List chatMessages = new ArrayList<>(); + // 1.1 System Context 角色设定 if (StrUtil.isNotBlank(conversation.getSystemMessage())) { chatMessages.add(new SystemMessage(conversation.getSystemMessage())); } - // 1.3 history message 历史消息 + + // 1.2 历史 history message 历史消息 List contextMessages = filterContextMessages(messages, conversation, sendReqVO); - contextMessages.forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent()))); - // 1.4 user message 新发送消息 + contextMessages + .forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent()))); + + // 1.3 当前 user message 新发送消息 chatMessages.add(new UserMessage(sendReqVO.getContent())); + // 1.4 知识库,通过 UserMessage 实现 + if (CollUtil.isNotEmpty(knowledgeSegments)) { + String reference = knowledgeSegments.stream() + .map(segment -> "\n" + segment.getContent() + "") + .collect(Collectors.joining("\n\n")); + chatMessages.add(new UserMessage(String.format(KNOWLEDGE_USER_MESSAGE_TEMPLATE, reference))); + } + // 2. 构建 ChatOptions 对象 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), @@ -199,8 +235,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { * @return 消息上下文 */ private List filterContextMessages(List messages, - AiChatConversationDO conversation, - AiChatMessageSendReqVO sendReqVO) { + AiChatConversationDO conversation, + AiChatMessageSendReqVO sendReqVO) { if (conversation.getMaxContexts() == null || ObjUtil.notEqual(sendReqVO.getUseContext(), Boolean.TRUE)) { return Collections.emptyList(); } @@ -211,7 +247,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { continue; } AiChatMessageDO userMessage = CollUtil.get(messages, i - 1); - if (userMessage == null || ObjUtil.notEqual(assistantMessage.getReplyId(), userMessage.getId()) + if (userMessage == null + || ObjUtil.notEqual(assistantMessage.getReplyId(), userMessage.getId()) || StrUtil.isEmpty(assistantMessage.getContent())) { continue; } @@ -228,11 +265,13 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } private AiChatMessageDO createChatMessage(Long conversationId, Long replyId, - AiModelDO model, Long userId, Long roleId, - MessageType messageType, String content, Boolean useContext) { + AiModelDO model, Long userId, Long roleId, + MessageType messageType, String content, Boolean useContext, + List knowledgeSegments) { AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId) .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId) - .setType(messageType.getValue()).setContent(content).setUseContext(useContext); + .setType(messageType.getValue()).setContent(content).setUseContext(useContext) + .setSegmentIds(convertList(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getId)); message.setCreateTime(LocalDateTime.now()); chatMessageMapper.insert(message); return message; From b709af11a138b9ebadc0877980a38071304ebe6a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 9 Mar 2025 21:00:34 +0800 Subject: [PATCH 37/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E8=81=8A=E5=A4=A9=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E8=BF=94=E5=9B=9E=E6=97=B6=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=20segments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/chat/AiChatMessageController.java | 51 +++++++++++++++++-- .../chat/vo/message/AiChatMessageRespVO.java | 25 +++++++++ .../knowledge/AiKnowledgeController.java | 1 - .../dal/dataobject/chat/AiChatMessageDO.java | 18 +++---- .../chat/AiChatMessageServiceImpl.java | 2 +- .../AiKnowledgeDocumentServiceImpl.java | 5 +- .../knowledge/AiKnowledgeSegmentService.java | 22 ++++++++ .../AiKnowledgeSegmentServiceImpl.java | 15 ++++-- 8 files changed, 116 insertions(+), 23 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java index 43ae9a40db..b4fa8ab88c 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java @@ -12,9 +12,13 @@ import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessage import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService; import cn.iocoder.yudao.module.ai.service.chat.AiChatMessageService; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -32,7 +36,7 @@ import java.util.List; import java.util.Map; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - 聊天消息") @@ -47,6 +51,10 @@ public class AiChatMessageController { private AiChatConversationService chatConversationService; @Resource private AiChatRoleService chatRoleService; + @Resource + private AiKnowledgeSegmentService knowledgeSegmentService; + @Resource + private AiKnowledgeDocumentService knowledgeDocumentService; @Operation(summary = "发送消息(段式)", description = "一次性返回,响应较慢") @PostMapping("/send") @@ -56,7 +64,8 @@ public class AiChatMessageController { @Operation(summary = "发送消息(流式)", description = "流式返回,响应较快") @PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public Flux> sendChatMessageStream(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) { + public Flux> sendChatMessageStream( + @Valid @RequestBody AiChatMessageSendReqVO sendReqVO) { return chatMessageService.sendChatMessageStream(sendReqVO, getLoginUserId()); } @@ -69,8 +78,38 @@ public class AiChatMessageController { if (conversation == null || ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) { return success(Collections.emptyList()); } + // 1. 获取消息列表 List messageList = chatMessageService.getChatMessageListByConversationId(conversationId); - return success(BeanUtils.toBean(messageList, AiChatMessageRespVO.class)); + if (CollUtil.isEmpty(messageList)) { + return success(Collections.emptyList()); + } + + // 2. 拼接数据,主要是知识库段落信息 + Map segmentMap = knowledgeSegmentService.getKnowledgeSegmentMap(convertListByFlatMap(messageList, + message -> CollUtil.isEmpty(message.getSegmentIds()) ? null : message.getSegmentIds().stream())); + Map documentMap = knowledgeDocumentService.getKnowledgeDocumentMap( + convertList(segmentMap.values(), AiKnowledgeSegmentDO::getDocumentId)); + List messageVOList = BeanUtils.toBean(messageList, AiChatMessageRespVO.class); + for (int i = 0; i < messageList.size(); i++) { + AiChatMessageDO message = messageList.get(i); + if (CollUtil.isEmpty(message.getSegmentIds())) { + continue; + } + // 设置知识库段落信息 + messageVOList.get(i).setSegments(convertList(message.getSegmentIds(), segmentId -> { + AiKnowledgeSegmentDO segment = segmentMap.get(segmentId); + if (segment == null) { + return null; + } + AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId()); + if (document == null) { + return null; + } + return new AiChatMessageRespVO.KnowledgeSegment().setId(segment.getId()).setContent(segment.getContent()) + .setDocumentId(segment.getDocumentId()).setDocumentName(document.getName()); + })); + } + return success(messageVOList); } @Operation(summary = "删除消息") @@ -84,7 +123,8 @@ public class AiChatMessageController { @Operation(summary = "删除指定对话的消息") @DeleteMapping("/delete-by-conversation-id") @Parameter(name = "conversationId", required = true, description = "对话编号", example = "1024") - public CommonResult deleteChatMessageByConversationId(@RequestParam("conversationId") Long conversationId) { + public CommonResult deleteChatMessageByConversationId( + @RequestParam("conversationId") Long conversationId) { chatMessageService.deleteChatMessageByConversationId(conversationId, getLoginUserId()); return success(true); } @@ -103,7 +143,8 @@ public class AiChatMessageController { Map roleMap = chatRoleService.getChatRoleMap( convertSet(pageResult.getList(), AiChatMessageDO::getRoleId)); return success(BeanUtils.toBean(pageResult, AiChatMessageRespVO.class, - respVO -> MapUtils.findAndThen(roleMap, respVO.getRoleId(), role -> respVO.setRoleName(role.getName())))); + respVO -> MapUtils.findAndThen(roleMap, respVO.getRoleId(), + role -> respVO.setRoleName(role.getName())))); } @Operation(summary = "管理员删除消息") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java index 9b358df6f2..5d44e4f967 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; +import java.util.List; @Schema(description = "管理后台 - AI 聊天消息 Response VO") @Data @@ -39,6 +40,12 @@ public class AiChatMessageRespVO { @Schema(description = "是否携带上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") private Boolean useContext; + @Schema(description = "知识库段落编号数组", example = "[1,2,3]") + private List segmentIds; + + @Schema(description = "知识库段落数组") + private List segments; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51") private LocalDateTime createTime; @@ -47,4 +54,22 @@ public class AiChatMessageRespVO { @Schema(description = "角色名字", example = "小黄") private String roleName; + @Schema(description = "知识库段落", example = "Java 开发手册") + @Data + public static class KnowledgeSegment { + + @Schema(description = "段落编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "切片内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 开发手册") + private String content; + + @Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") + private Long documentId; + + @Schema(description = "文档名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "产品使用手册") + private String documentName; + + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java index 39fb8d9430..7dd2e1647e 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java @@ -23,7 +23,6 @@ import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; -// TODO @芋艿:增加权限标识 @Tag(name = "管理后台 - AI 知识库") @RestController @RequestMapping("/ai/knowledge") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java index d121d85785..94f764c85e 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java @@ -20,7 +20,7 @@ import java.util.List; * @since 2024/4/14 17:35 * @since 2024/4/14 17:35 */ -@TableName("ai_chat_message") +@TableName(value = "ai_chat_message", autoResultMap = true) @KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @@ -71,14 +71,6 @@ public class AiChatMessageDO extends BaseDO { */ private Long roleId; - /** - * 知识库段落编号数组 - * - * 关联 {@link AiKnowledgeSegmentDO#getId()} 字段 - */ - @TableField(typeHandler = LongListTypeHandler.class) - private List segmentIds; - /** * 模型标志 * @@ -102,4 +94,12 @@ public class AiChatMessageDO extends BaseDO { */ private Boolean useContext; + /** + * 知识库段落编号数组 + * + * 关联 {@link AiKnowledgeSegmentDO#getId()} 字段 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List segmentIds; + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java index b95f7cd214..cd6c39d405 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -212,7 +212,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { // 1.4 知识库,通过 UserMessage 实现 if (CollUtil.isNotEmpty(knowledgeSegments)) { String reference = knowledgeSegments.stream() - .map(segment -> "\n" + segment.getContent() + "") + .map(segment -> "" + segment.getContent() + "") .collect(Collectors.joining("\n\n")); chatMessages.add(new UserMessage(String.format(KNOWLEDGE_USER_MESSAGE_TEMPLATE, reference))); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 48dd78dbb9..2d78f94f34 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -26,6 +26,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -205,9 +206,9 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic @Override public List getKnowledgeDocumentList(Collection ids) { if (CollUtil.isEmpty(ids)) { - return new ArrayList<>(); + return Collections.emptyList(); } - return knowledgeDocumentMapper.selectByIds(ids); + return knowledgeDocumentMapper.selectBatchIds(ids); } } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java index 15ab941fe8..32272abdb8 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java @@ -10,7 +10,11 @@ import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchR import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; import org.springframework.scheduling.annotation.Async; +import java.util.Collection; import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; /** * AI 知识库段落 Service 接口 @@ -27,6 +31,24 @@ public interface AiKnowledgeSegmentService { */ AiKnowledgeSegmentDO getKnowledgeSegment(Long id); + /** + * 获取知识库段落列表 + * + * @param ids 段落编号列表 + * @return 段落列表 + */ + List getKnowledgeSegmentList(Collection ids); + + /** + * 获取知识库段落 Map + * + * @param ids 段落编号列表 + * @return 段落 Map + */ + default Map getKnowledgeSegmentMap(Collection ids) { + return convertMap(getKnowledgeSegmentList(ids), AiKnowledgeSegmentDO::getId); + } + /** * 获取段落分页 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index efdd86203f..94b735b3c5 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -30,10 +30,7 @@ import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @@ -322,7 +319,15 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService @Override public AiKnowledgeSegmentDO getKnowledgeSegment(Long id) { - return validateKnowledgeSegmentExists(id); + return segmentMapper.selectById(id); + } + + @Override + public List getKnowledgeSegmentList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return segmentMapper.selectBatchIds(ids); } } From 32e1ef4da81848179bd5f7e6c098f1f0bc4c0ddc Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 9 Mar 2025 21:19:41 +0800 Subject: [PATCH 38/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E8=81=8A=E5=A4=A9=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E8=BF=94=E5=9B=9E=E6=97=B6=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=20segments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vo/message/AiChatMessageSendRespVO.java | 7 +++ .../chat/AiChatMessageServiceImpl.java | 50 +++++++++++++------ 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java index 58ba056595..245a19f7cb 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; +import java.util.List; @Schema(description = "管理后台 - AI 聊天消息发送 Response VO") @Data @@ -28,6 +29,12 @@ public class AiChatMessageSendRespVO { @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊") private String content; + @Schema(description = "知识库段落编号数组", example = "[1,2,3]") + private List segmentIds; + + @Schema(description = "知识库段落数组") + private List segments; + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java index cd6c39d405..88f7144336 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -10,14 +10,17 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper; import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; @@ -76,6 +79,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { private AiModelService modalService; @Resource private AiKnowledgeSegmentService knowledgeSegmentService; + @Resource + private AiKnowledgeDocumentService knowledgeDocumentService; @Transactional(rollbackFor = Exception.class) public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) { @@ -108,18 +113,23 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); ChatResponse chatResponse = chatModel.call(prompt); - // 3.3 段式返回 + // 3.3 更新响应内容 String newContent = chatResponse.getResult().getOutput().getText(); chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent)); + // 3.4 响应结果 + List segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, + segment -> { + AiKnowledgeDocumentDO document = knowledgeDocumentService.getKnowledgeDocument(segment.getDocumentId()); + segment.setDocumentName(document != null ? document.getName() : null); + }); return new AiChatMessageSendRespVO() .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) - .setContent(newContent)); + .setContent(newContent).setSegments(segments)); } @Override - public Flux> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, - Long userId) { + public Flux> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId) { // 1.1 校验对话存在 AiChatConversationDO conversation = chatConversationService .validateChatConversationExists(sendReqVO.getConversationId()); @@ -132,8 +142,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { StreamingChatModel chatModel = modalService.getChatModel(model.getId()); // 2. 知识库找回 - List knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), - conversation); + List knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), conversation); // 3. 插入 user 发送消息 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, @@ -152,14 +161,23 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { // 4.3 流式返回 StringBuffer contentBuffer = new StringBuffer(); return streamResponse.map(chunk -> { + // 处理知识库的返回,只有首次才有 + List segments = null; + if (StrUtil.isEmpty(contentBuffer)) { + segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, + segment -> TenantUtils.executeIgnore(() -> { + AiKnowledgeDocumentDO document = knowledgeDocumentService.getKnowledgeDocument(segment.getDocumentId()); + segment.setDocumentName(document != null ? document.getName() : null); + })); + } + // 响应结果 String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 contentBuffer.append(newContent); - // 响应结果 return success(new AiChatMessageSendRespVO() .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) - .setContent(newContent))); + .setContent(newContent).setSegments(segments))); }).doOnComplete(() -> { // 忽略租户,因为 Flux 异步无法透传租户 TenantUtils.executeIgnore(() -> chatMessageMapper.updateById( @@ -173,7 +191,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } private List recallKnowledgeSegment(String content, - AiChatConversationDO conversation) { + AiChatConversationDO conversation) { // 1. 查询聊天角色 if (conversation == null || conversation.getRoleId() == null) { return Collections.emptyList(); @@ -193,8 +211,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } private Prompt buildPrompt(AiChatConversationDO conversation, List messages, - List knowledgeSegments, - AiModelDO model, AiChatMessageSendReqVO sendReqVO) { + List knowledgeSegments, + AiModelDO model, AiChatMessageSendReqVO sendReqVO) { List chatMessages = new ArrayList<>(); // 1.1 System Context 角色设定 if (StrUtil.isNotBlank(conversation.getSystemMessage())) { @@ -235,8 +253,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { * @return 消息上下文 */ private List filterContextMessages(List messages, - AiChatConversationDO conversation, - AiChatMessageSendReqVO sendReqVO) { + AiChatConversationDO conversation, + AiChatMessageSendReqVO sendReqVO) { if (conversation.getMaxContexts() == null || ObjUtil.notEqual(sendReqVO.getUseContext(), Boolean.TRUE)) { return Collections.emptyList(); } @@ -265,9 +283,9 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } private AiChatMessageDO createChatMessage(Long conversationId, Long replyId, - AiModelDO model, Long userId, Long roleId, - MessageType messageType, String content, Boolean useContext, - List knowledgeSegments) { + AiModelDO model, Long userId, Long roleId, + MessageType messageType, String content, Boolean useContext, + List knowledgeSegments) { AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId) .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId) .setType(messageType.getValue()).setContent(content).setUseContext(useContext) From d7e801c438d3e4c2e7f4e34cec224dd23b4a3efc Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 10 Mar 2025 09:12:24 +0800 Subject: [PATCH 39/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E7=9F=A5=E8=AF=86=E5=BA=93?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E7=9A=84=E6=A8=A1=E5=9E=8B=E5=8F=98=E5=8C=96?= =?UTF-8?q?=E6=97=B6=EF=BC=8C=E8=A7=A6=E5=8F=91=E9=87=8D=E7=B4=A2=E5=BC=95?= =?UTF-8?q?=EF=BC=88=E5=BC=82=E6=AD=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../knowledge/AiKnowledgeSegmentMapper.java | 7 +++++- .../knowledge/AiKnowledgeSegmentService.java | 17 +++++++++++++ .../AiKnowledgeSegmentServiceImpl.java | 24 +++++++++++++++++++ .../knowledge/AiKnowledgeServiceImpl.java | 12 ++++++++-- .../ai/service/model/AiModelServiceImpl.java | 6 ++--- 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java index a2a4e15b6d..00bacd9665 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java @@ -42,6 +42,11 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX selectListByKnowledgeIdAndStatus(Long knowledgeId, Integer status) { + return selectList(AiKnowledgeSegmentDO::getKnowledgeId, knowledgeId, + AiKnowledgeSegmentDO::getStatus, status); + } + default List selectProcessList(Collection documentIds) { MPJLambdaWrapper wrapper = new MPJLambdaWrapperX() .selectAs(AiKnowledgeSegmentDO::getDocumentId, AiKnowledgeSegmentProcessRespVO::getDocumentId) @@ -54,7 +59,7 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX ids) { - update( new LambdaUpdateWrapper() + update(new LambdaUpdateWrapper() .setSql(" retrieval_count = retrieval_count + 1") .in(AiKnowledgeSegmentDO::getId, ids)); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java index 32272abdb8..54f7217055 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentService.java @@ -98,6 +98,23 @@ public interface AiKnowledgeSegmentService { */ void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO); + /** + * 重新索引知识库下的所有文档段落 + * + * @param knowledgeId 知识库编号 + */ + void reindexKnowledgeSegmentByKnowledgeId(Long knowledgeId); + + /** + * 【异步】重新索引知识库下的所有文档段落 + * + * @param knowledgeId 知识库编号 + */ + @Async + default void reindexByKnowledgeIdAsync(Long knowledgeId) { + reindexKnowledgeSegmentByKnowledgeId(knowledgeId); + } + /** * 根据文档编号删除段落 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java index 94b735b3c5..20f881cf13 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java @@ -158,6 +158,30 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService } } + @Override + public void reindexKnowledgeSegmentByKnowledgeId(Long knowledgeId) { + // 1.1 校验知识库存在 + AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(knowledgeId); + // 1.2 获取知识库向量实例 + VectorStore vectorStore = getVectorStoreById(knowledge); + + // 2.1 查询知识库下的所有启用状态的段落 + List segments = segmentMapper.selectListByKnowledgeIdAndStatus( + knowledgeId, CommonStatusEnum.ENABLE.getStatus()); + if (CollUtil.isEmpty(segments)) { + return; + } + // 2.2 遍历所有段落,重新索引 + for (AiKnowledgeSegmentDO segment : segments) { + // 删除旧的向量 + deleteVectorStore(vectorStore, segment); + // 重新创建向量 + writeVectorStore(vectorStore, segment, new Document(segment.getContent())); + } + log.info("[reindexKnowledgeSegmentByKnowledgeId][知识库({}) 重新索引完成,共处理 {} 个段落]", + knowledgeId, segments.size()); + } + private void writeVectorStore(VectorStore vectorStore, AiKnowledgeSegmentDO segmentDO, Document segment) { // 1. 向量存储 // 为什么要 toString 呢?因为部分 VectorStore 实现,不支持 Long 类型,例如说 QdrantVectorStore diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java index 448a0d6ba1..59afd7d7bc 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java @@ -1,10 +1,13 @@ package cn.iocoder.yudao.module.ai.service.knowledge; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper; import cn.iocoder.yudao.module.ai.service.model.AiModelService; @@ -31,6 +34,8 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { @Resource private AiModelService modelService; + @Resource + private AiKnowledgeSegmentService knowledgeSegmentService; @Override public Long createKnowledge(AiKnowledgeSaveReqVO createReqVO) { @@ -47,7 +52,7 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { @Override public void updateKnowledge(AiKnowledgeSaveReqVO updateReqVO) { // 1.1 校验知识库存在 - validateKnowledgeExists(updateReqVO.getId()); + AiKnowledgeDO oldKnowledge = validateKnowledgeExists(updateReqVO.getId()); // 1.2 校验模型配置 AiModelDO model = modelService.validateModel(updateReqVO.getEmbeddingModelId()); @@ -56,7 +61,10 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { .setEmbeddingModel(model.getModel()); knowledgeMapper.updateById(updateObj); - // TODO @芋艿:如果模型变化,需要 reindex 所有的文档 + // 3. 如果模型变化,需要 reindex 所有的文档 + if (ObjUtil.notEqual(oldKnowledge.getEmbeddingModelId(), updateReqVO.getEmbeddingModelId())) { + knowledgeSegmentService.reindexByKnowledgeIdAsync(updateReqVO.getId()); + } } @Override diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java index b73567a1c5..b7e0c99d0f 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java @@ -16,8 +16,8 @@ import jakarta.annotation.Resource; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; +import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.VectorStore; -import org.springframework.ai.vectorstore.milvus.MilvusVectorStore; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -162,10 +162,10 @@ public class AiModelServiceImpl implements AiModelService { platform, apiKey.getApiKey(), apiKey.getUrl(), model.getModel()); // 创建或获取 VectorStore 对象 - // return modelFactory.getOrCreateVectorStore(SimpleVectorStore.class, embeddingModel, metadataFields); + return modelFactory.getOrCreateVectorStore(SimpleVectorStore.class, embeddingModel, metadataFields); // return modelFactory.getOrCreateVectorStore(QdrantVectorStore.class, embeddingModel, metadataFields); // return modelFactory.getOrCreateVectorStore(RedisVectorStore.class, embeddingModel, metadataFields); - return modelFactory.getOrCreateVectorStore(MilvusVectorStore.class, embeddingModel, metadataFields); +// return modelFactory.getOrCreateVectorStore(MilvusVectorStore.class, embeddingModel, metadataFields); } } \ No newline at end of file From 3a3607e1cbd63999422f6e732e395446ec8dbf4e Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 11 Mar 2025 07:49:54 +0800 Subject: [PATCH 40/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=96=B0=E5=A2=9E=20ZhiPuAiEmbedd?= =?UTF-8?q?ingModel=20=E5=90=91=E9=87=8F=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../knowledge/AiKnowledgeController.java | 1 - .../ai/service/model/AiModelServiceImpl.java | 6 ++--- .../ai/core/factory/AiModelFactoryImpl.java | 22 ++++++++++++++----- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java index 7dd2e1647e..c6e31f0e8d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java @@ -36,7 +36,6 @@ public class AiKnowledgeController { @Operation(summary = "获取知识库分页") @PreAuthorize("@ss.hasPermission('ai:knowledge:query')") public CommonResult> getKnowledgePage(@Valid AiKnowledgePageReqVO pageReqVO) { - PageResult pageResult = knowledgeService.getKnowledgePage(pageReqVO); return success(BeanUtils.toBean(pageResult, AiKnowledgeRespVO.class)); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java index b7e0c99d0f..b0e9e97172 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java @@ -163,9 +163,9 @@ public class AiModelServiceImpl implements AiModelService { // 创建或获取 VectorStore 对象 return modelFactory.getOrCreateVectorStore(SimpleVectorStore.class, embeddingModel, metadataFields); - // return modelFactory.getOrCreateVectorStore(QdrantVectorStore.class, embeddingModel, metadataFields); - // return modelFactory.getOrCreateVectorStore(RedisVectorStore.class, embeddingModel, metadataFields); -// return modelFactory.getOrCreateVectorStore(MilvusVectorStore.class, embeddingModel, metadataFields); +// return modelFactory.getOrCreateVectorStore(QdrantVectorStore.class, embeddingModel, metadataFields); +// return modelFactory.getOrCreateVectorStore(RedisVectorStore.class, embeddingModel, metadataFields); +// return modelFactory.getOrCreateVectorStore(MilvusVectorStore.class, embeddingModel, metadataFields); } } \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index 7953157009..180886b99c 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -77,6 +77,7 @@ import org.springframework.ai.vectorstore.observation.VectorStoreObservationConv import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore; import org.springframework.ai.vectorstore.redis.RedisVectorStore; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; +import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingModel; import org.springframework.ai.zhipuai.ZhiPuAiImageModel; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; @@ -231,7 +232,9 @@ public class AiModelFactoryImpl implements AiModelFactory { return buildTongYiEmbeddingModel(apiKey, model); case OLLAMA: return buildOllamaEmbeddingModel(url, model); - // TODO @芋艿:各个平台的向量化能力; + // TODO @芋艿:yiyan + case ZHI_PU: + return buildZhiPuEmbeddingModel(apiKey, url, model); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } @@ -240,8 +243,8 @@ public class AiModelFactoryImpl implements AiModelFactory { @Override public VectorStore getOrCreateVectorStore(Class type, - EmbeddingModel embeddingModel, - Map> metadataFields) { + EmbeddingModel embeddingModel, + Map> metadataFields) { String cacheKey = buildClientCacheKey(VectorStore.class, embeddingModel, type); return Singleton.get(cacheKey, (Func0) () -> { if (type == SimpleVectorStore.class) { @@ -425,17 +428,24 @@ public class AiModelFactoryImpl implements AiModelFactory { // ========== 各种创建 EmbeddingModel 的方法 ========== - // TODO @芋艿:需要测试下 /** * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeEmbeddingModel 方法 */ private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) { DashScopeApi dashScopeApi = new DashScopeApi(apiKey); - DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model) - .build(); + 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) { + url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); + ZhiPuAiApi zhiPuAiApi = new ZhiPuAiApi(url, apiKey); + return new ZhiPuAiEmbeddingModel(zhiPuAiApi); + } + private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) { OllamaApi ollamaApi = new OllamaApi(url); OllamaOptions ollamaOptions = OllamaOptions.builder().model(model).build(); From 20eb3013f273d018f84715b64bf10c30d022b8e4 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 11 Mar 2025 07:57:14 +0800 Subject: [PATCH 41/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=96=B0=E5=A2=9E=20QianFanEmbedd?= =?UTF-8?q?ingModel=20=E5=90=91=E9=87=8F=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/core/factory/AiModelFactoryImpl.java | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index 180886b99c..4bd519555c 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -64,6 +64,8 @@ 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; @@ -78,6 +80,7 @@ import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore; import org.springframework.ai.vectorstore.redis.RedisVectorStore; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingModel; +import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingOptions; import org.springframework.ai.zhipuai.ZhiPuAiImageModel; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; @@ -230,11 +233,16 @@ public class AiModelFactoryImpl implements AiModelFactory { switch (platform) { case TONG_YI: return buildTongYiEmbeddingModel(apiKey, model); - case OLLAMA: - return buildOllamaEmbeddingModel(url, model); - // TODO @芋艿:yiyan + case YI_YAN: + return buildYiYanEmbeddingModel(apiKey, model); case ZHI_PU: return buildZhiPuEmbeddingModel(apiKey, url, model); +// case OPENAI: +// return buildOpenAiChatModel(apiKey, url); +// case AZURE_OPENAI: +// return buildAzureOpenAiChatModel(apiKey, url); + case OLLAMA: + return buildOllamaEmbeddingModel(url, model); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } @@ -443,7 +451,21 @@ public class AiModelFactoryImpl implements AiModelFactory { private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) { url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); ZhiPuAiApi zhiPuAiApi = new ZhiPuAiApi(url, apiKey); - return new ZhiPuAiEmbeddingModel(zhiPuAiApi); + ZhiPuAiEmbeddingOptions zhiPuAiEmbeddingOptions = ZhiPuAiEmbeddingOptions.builder().model(model).build(); + return new ZhiPuAiEmbeddingModel(zhiPuAiApi, MetadataMode.EMBED, zhiPuAiEmbeddingOptions); + } + + /** + * 可参考 {@link QianFanAutoConfiguration} 的 qianFanEmbeddingModel 方法 + */ + private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) { + List 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) { From b9e8495712de03b9c7e215b43f772c0c9b85ef0a Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 11 Mar 2025 09:42:24 +0800 Subject: [PATCH 42/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=96=B0=E5=A2=9E=20AzureOpenAiEm?= =?UTF-8?q?beddingModel=E3=80=81OpenAiEmbeddingModel=20=E5=90=91=E9=87=8F?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/core/factory/AiModelFactoryImpl.java | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index 4bd519555c..b588027cde 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -35,6 +35,7 @@ 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.ollama.OllamaAutoConfiguration; import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration; @@ -49,6 +50,7 @@ import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStorePr import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration; import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiConnectionProperties; 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; @@ -59,6 +61,8 @@ 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; @@ -227,6 +231,7 @@ public class AiModelFactoryImpl implements AiModelFactory { } @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) () -> { @@ -237,10 +242,10 @@ public class AiModelFactoryImpl implements AiModelFactory { return buildYiYanEmbeddingModel(apiKey, model); case ZHI_PU: return buildZhiPuEmbeddingModel(apiKey, url, model); -// case OPENAI: -// return buildOpenAiChatModel(apiKey, url); -// case AZURE_OPENAI: -// return buildAzureOpenAiChatModel(apiKey, url); + case OPENAI: + return buildOpenAiEmbeddingModel(apiKey, url, model); + case AZURE_OPENAI: + return buildAzureOpenAiEmbeddingModel(apiKey, url, model); case OLLAMA: return buildOllamaEmbeddingModel(url, model); default: @@ -474,6 +479,33 @@ public class AiModelFactoryImpl implements AiModelFactory { 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 的方法 ========== /** From 99d5e7c5094875bd379f7cd98d05d387677eccae Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 11 Mar 2025 12:53:34 +0800 Subject: [PATCH 43/51] =?UTF-8?q?=E3=80=90=E4=BE=9D=E8=B5=96=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=E3=80=91AI=EF=BC=9Aspring-ai-alibaba-starter=20?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=E5=88=B0=E6=9C=80=E6=96=B0=EF=BC=8C=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=E6=B5=81=E5=BC=8F=E8=BF=94=E5=9B=9E=E6=8A=A5=E9=94=99?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml | 2 +- .../java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java | 3 +-- .../iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java | 1 + .../yudao/framework/ai/image/TongYiImagesModelTest.java | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index 2d1a0af3aa..5a10c137f9 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -49,7 +49,7 @@ com.alibaba.cloud.ai spring-ai-alibaba-starter - 1.0.0-M5.1 + ${spring-ai.version}.1 diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index a96d305492..cb968c3ffc 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -22,8 +22,7 @@ public class AiUtils { // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: - // TODO @芋艿:tongyi 暂时没 maxTokens 选项 - return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).build(); + return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens).build(); case YI_YAN: return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case ZHI_PU: diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java index c632acf20f..b51d556a36 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java @@ -28,6 +28,7 @@ public class TongYiChatModelTests { .withModel("qwen1.5-72b-chat") // 模型 // .withModel("deepseek-r1") // 模型(deepseek-r1) // .withModel("deepseek-v3") // 模型(deepseek-v3) +// .withModel("deepseek-r1-distill-qwen-1.5b") // 模型(deepseek-r1-distill-qwen-1.5b) .build()); @Test diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java index cc62f02fb6..ad4faaa46b 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.ai.image; import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; -import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.image.ImageOptions; @@ -25,7 +24,7 @@ public class TongYiImagesModelTest { public void imageCallTest() { // 准备参数 ImageOptions options = DashScopeImageOptions.builder() - .withModel(ImageSynthesis.Models.WANX_V1) + .withModel("wanx-v1") .withHeight(256).withWidth(256) .build(); ImagePrompt prompt = new ImagePrompt("中国长城!", options); From 07fe6167e09d5b4e4d5707a4d313b5b27f4cc3f2 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 11 Mar 2025 13:20:59 +0800 Subject: [PATCH 44/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=8E=A5=E5=85=A5=20minimax=20?= =?UTF-8?q?=E5=92=8C=20moonshot=20=E6=9C=88=E4=B9=8B=E6=9A=97=E7=81=AD?= =?UTF-8?q?=EF=BC=88kimi=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao-spring-boot-starter-ai/pom.xml | 29 ++++++--- .../ai/core/enums/AiPlatformEnum.java | 2 + .../ai/core/factory/AiModelFactoryImpl.java | 59 +++++++++++++++--- .../yudao/framework/ai/core/util/AiUtils.java | 6 ++ .../ai/chat/MiniMaxChatModelTests.java | 62 +++++++++++++++++++ .../ai/chat/MoonshotChatModelTests.java | 62 +++++++++++++++++++ .../src/main/resources/application.yaml | 4 ++ 7 files changed, 207 insertions(+), 17 deletions(-) create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/MiniMaxChatModelTests.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/MoonshotChatModelTests.java diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index 5a10c137f9..f37f3709c6 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -14,7 +14,6 @@ ${project.artifactId} AI 大模型拓展,接入国内外大模型 - org.springframework.ai 1.0.0-M6 @@ -26,22 +25,22 @@ - ${spring-ai.groupId} + org.springframework.ai spring-ai-openai-spring-boot-starter ${spring-ai.version} - ${spring-ai.groupId} + org.springframework.ai spring-ai-azure-openai-spring-boot-starter ${spring-ai.version} - ${spring-ai.groupId} + org.springframework.ai spring-ai-ollama-spring-boot-starter ${spring-ai.version} - ${spring-ai.groupId} + org.springframework.ai spring-ai-stability-ai-spring-boot-starter ${spring-ai.version} @@ -53,28 +52,38 @@ - ${spring-ai.groupId} + org.springframework.ai spring-ai-qianfan-spring-boot-starter ${spring-ai.version} - ${spring-ai.groupId} + org.springframework.ai spring-ai-zhipuai-spring-boot-starter ${spring-ai.version} + + org.springframework.ai + spring-ai-minimax-spring-boot-starter + ${spring-ai.version} + + + org.springframework.ai + spring-ai-moonshot-spring-boot-starter + ${spring-ai.version} + - ${spring-ai.groupId} + org.springframework.ai spring-ai-qdrant-store ${spring-ai.version} - ${spring-ai.groupId} + org.springframework.ai spring-ai-redis-store ${spring-ai.version} @@ -92,7 +101,7 @@ - ${spring-ai.groupId} + org.springframework.ai spring-ai-tika-document-reader ${spring-ai.version} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java index a154db8c88..5a8a5c4539 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java @@ -25,6 +25,8 @@ public enum AiPlatformEnum implements ArrayValuable { DOU_BAO("DouBao", "豆包"), // 字节 HUN_YUAN("HunYuan", "混元"), // 腾讯 SILICON_FLOW("SiliconFlow", "硅基流动"), // 硅基流动 + MINI_MAX("MiniMax", "MiniMax"), // 稀宇科技 + MOONSHOT("Moonshot", "月之暗灭"), // KIMI // ========== 国外平台 ========== diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index b588027cde..dbbbbe2241 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -36,6 +36,8 @@ import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfigur 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; @@ -48,7 +50,6 @@ import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStore 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.autoconfigure.zhipuai.ZhiPuAiConnectionProperties; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; import org.springframework.ai.chat.model.ChatModel; @@ -56,6 +57,12 @@ 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.MiniMaxEmbeddingModel; +import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; +import org.springframework.ai.minimax.api.MiniMaxApi; +import org.springframework.ai.moonshot.MoonshotChatModel; +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; @@ -130,6 +137,10 @@ public class AiModelFactoryImpl implements AiModelFactory { 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 OPENAI: @@ -162,6 +173,10 @@ public class AiModelFactoryImpl implements AiModelFactory { 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 OPENAI: @@ -242,6 +257,8 @@ public class AiModelFactoryImpl implements AiModelFactory { 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: @@ -365,8 +382,8 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiChatModel 方法 */ private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { - url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); - ZhiPuAiApi zhiPuAiApi = new ZhiPuAiApi(url, apiKey); + ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) + : new ZhiPuAiApi(url, apiKey); return new ZhiPuAiChatModel(zhiPuAiApi); } @@ -374,11 +391,29 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiImageModel 方法 */ private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { - url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); - ZhiPuAiImageApi zhiPuAiApi = new ZhiPuAiImageApi(url, apiKey, RestClient.builder()); + 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); + return new MiniMaxChatModel(miniMaxApi); + } + + /** + * 可参考 {@link MoonshotAutoConfiguration} 的 moonshotChatModel 方法 + */ + private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) { + MoonshotApi moonshotApi = StrUtil.isEmpty(url)? new MoonshotApi(apiKey) + : new MoonshotApi(url, apiKey); + return new MoonshotChatModel(moonshotApi); + } + /** * 可参考 {@link YudaoAiAutoConfiguration#xingHuoChatClient(YudaoAiProperties)} */ @@ -454,12 +489,22 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法 */ private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) { - url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); - ZhiPuAiApi zhiPuAiApi = new ZhiPuAiApi(url, apiKey); + 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 方法 */ diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index cb968c3ffc..c3a87358cd 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -6,6 +6,8 @@ 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; @@ -27,6 +29,10 @@ public class AiUtils { return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case ZHI_PU: return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); + case MINI_MAX: + return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); + case MOONSHOT: + return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case OPENAI: case DEEP_SEEK: // 复用 OpenAI 客户端 case DOU_BAO: // 复用 OpenAI 客户端 diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/MiniMaxChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/MiniMaxChatModelTests.java new file mode 100644 index 0000000000..80b60aea94 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/MiniMaxChatModelTests.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.minimax.MiniMaxChatModel; +import org.springframework.ai.minimax.MiniMaxChatOptions; +import org.springframework.ai.minimax.api.MiniMaxApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link MiniMaxChatModel} 的集成测试 + * + * @author 芋道源码 + */ +public class MiniMaxChatModelTests { + + private final MiniMaxChatModel chatModel = new MiniMaxChatModel( + new MiniMaxApi("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJHcm91cE5hbWUiOiLnjovmlofmlowiLCJVc2VyTmFtZSI6IueOi-aWh-aWjCIsIkFjY291bnQiOiIiLCJTdWJqZWN0SUQiOiIxODk3Mjg3MjQ5NDU2ODA4MzQ2IiwiUGhvbmUiOiIxNTYwMTY5MTM5OSIsIkdyb3VwSUQiOiIxODk3Mjg3MjQ5NDQ4NDE5NzM4IiwiUGFnZU5hbWUiOiIiLCJNYWlsIjoiIiwiQ3JlYXRlVGltZSI6IjIwMjUtMDMtMTEgMTI6NTI6MDIiLCJUb2tlblR5cGUiOjEsImlzcyI6Im1pbmltYXgifQ.aAuB7gWW_oA4IYhh-CF7c9MfWWxKN49B_HK-DYjXaDwwffhiG-H1571z1WQhp9QytWG-DqgLejneeSxkiq1wQIe3FsEP2wz4BmGBct31LehbJu8ehLxg_vg75Uod1nFAHbm5mZz6JSVLNIlSo87Xr3UtSzJhAXlapEkcqlA4YOzOpKrZ8l5_OJPTORTCmHWZYgJcRS-faNiH62ZnUEHUozesTFhubJHo5GfJCw_edlnmfSUocERV1BjWvenhZ9My-aYXNktcW9WaSj9l6gayV7A0Ium_PL55T9ln1PcI8gayiVUKJGJDoqNyF1AF9_aF9NOKtTnQzwNqnZdlTYH6hw"), // 密钥 + MiniMaxChatOptions.builder() + .model(MiniMaxApi.ChatModel.ABAB_6_5_G_Chat.getValue()) // 模型 + .build()); + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(response.getResult().getOutput()); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(response.getResult().getOutput()); + }).then().block(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/MoonshotChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/MoonshotChatModelTests.java new file mode 100644 index 0000000000..e3f644a6f7 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/MoonshotChatModelTests.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.moonshot.MoonshotChatModel; +import org.springframework.ai.moonshot.MoonshotChatOptions; +import org.springframework.ai.moonshot.api.MoonshotApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link org.springframework.ai.moonshot.MoonshotChatModel} 的集成测试 + * + * @author 芋道源码 + */ +public class MoonshotChatModelTests { + + private final MoonshotChatModel chatModel = new MoonshotChatModel( + new MoonshotApi("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA"), // 密钥 + MoonshotChatOptions.builder() + .model("moonshot-v1-8k") // 模型 + .build()); + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(response.getResult().getOutput()); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(response.getResult().getOutput()); + }).then().block(); + } + +} diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 419fab471b..8c906e3899 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -184,6 +184,10 @@ spring: api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx dashscope: # 通义千问 api-key: sk-71800982914041848008480000000000 + minimax: # Minimax:https://www.minimaxi.com/ + api-key: xxxx + moonshot: # 月之暗灭(KIMI) + api-key: sk-abc yudao: ai: From f7ab30c50a7b8a899dacaab08f94cbf086b9f1f2 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Wed, 12 Mar 2025 21:50:07 +0800 Subject: [PATCH 45/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=96=B0=E5=A2=9E=20Dify=E3=80=81?= =?UTF-8?q?FastGPT=20=E7=9A=84=E6=8E=A5=E5=85=A5=E4=BE=8B=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/ai/chat/DifyChatModelTests.java | 63 +++++++++++++++++++ .../ai/chat/FastGPTChatModelTests.java | 63 +++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DifyChatModelTests.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/FastGPTChatModelTests.java diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DifyChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DifyChatModelTests.java new file mode 100644 index 0000000000..8b02346bbc --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DifyChatModelTests.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.api.OpenAiApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * 基于 {@link OpenAiChatModel} 集成 Dify 测试 + * + * @author 芋道源码 + */ +public class DifyChatModelTests { + + private final OpenAiChatModel chatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl("http://127.0.0.1:3000") + .apiKey("app-4hy2d7fJauSbrKbzTKX1afuP") // apiKey + .build()) + .build(); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(response.getResult().getOutput()); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(response.getResult().getOutput()); + }).then().block(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/FastGPTChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/FastGPTChatModelTests.java new file mode 100644 index 0000000000..b58807b793 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/FastGPTChatModelTests.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.api.OpenAiApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * 基于 {@link OpenAiChatModel} 集成 FastGPT 测试 + * + * @author 芋道源码 + */ +public class FastGPTChatModelTests { + + private final OpenAiChatModel chatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl("https://cloud.fastgpt.cn/api") + .apiKey("fastgpt-aqcc61kFtF8CeaglnGAfQOCIDWwjGdJVJHv6hIlMo28otFlva2aZNK") // apiKey + .build()) + .build(); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(response.getResult().getOutput()); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(response.getResult().getOutput()); + }).then().block(); + } + +} From 25a0fe908a6a9a272fb76b5ef43058b421354c98 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 13 Mar 2025 12:51:50 +0800 Subject: [PATCH 46/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=96=B0=E5=A2=9E=20function=20ca?= =?UTF-8?q?ll=20=E7=A4=BA=E4=BE=8B=E3=80=82=E4=BC=9A=E7=BB=A7=E7=BB=AD?= =?UTF-8?q?=E5=AE=8C=E5=96=84=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/service/tool/ListDirTool.java | 95 +++++++++++++++++ .../module/ai/service/tool/ListDirToolB.java | 100 ++++++++++++++++++ .../ai/config/YudaoAiAutoConfiguration.java | 13 +-- .../ai/core/factory/AiModelFactoryImpl.java | 9 +- .../yudao/framework/ai/core/util/AiUtils.java | 14 ++- .../yudao/framework/ai/package-info.java | 7 +- 6 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirTool.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirToolB.java diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirTool.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirTool.java new file mode 100644 index 0000000000..4315e549ae --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirTool.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.ai.service.tool; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * 目录内容列表工具:列出指定目录的内容 + * + * @author 芋道源码 + */ +@Component +public class ListDirTool { + + /** + * 列出指定目录的内容 + * + * @param relativePath 要列出内容的目录路径,相对于工作区根目录 + * @return 目录内容列表 + */ + @Tool(name = "listDir", description = "列出指定目录的内容") + public Response listDir(@ToolParam(description = "要列出内容的目录路径,相对于工作区根目录") String relativePath) { + // 校验目录存在 + String path = StrUtil.blankToDefault(relativePath, "."); + Path dirPath = Paths.get(path); + if (!FileUtil.exist(dirPath.toString()) || !FileUtil.isDirectory(dirPath.toString())) { + return new Response(Collections.emptyList()); + } + // 列出目录内容 + File[] files = dirPath.toFile().listFiles(); + if (ArrayUtil.isEmpty(files)) { + return new Response(Collections.emptyList()); + } + return new Response(convertList(Arrays.asList(files), file -> new Response.File() + .setDirectory(file.isDirectory()).setName(file.getName()) + .setSize(file.isFile() ? FileUtil.readableFileSize(file.length()) : null) + .setLastModified( + LocalDateTimeUtil.format(LocalDateTimeUtil.of(file.lastModified()), NORM_DATETIME_PATTERN)))); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Response { + + /** + * 目录内容列表 + */ + private List files; + + @Data + public static class File { + + /** + * 是否为目录 + */ + private Boolean directory; + + /** + * 名称 + */ + private String name; + + /** + * 大小,仅对文件有效 + */ + private String size; + + /** + * 最后修改时间 + */ + private String lastModified; + + } + + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirToolB.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirToolB.java new file mode 100644 index 0000000000..3eafff4b2c --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirToolB.java @@ -0,0 +1,100 @@ +package cn.iocoder.yudao.module.ai.service.tool; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * 目录内容列表工具:列出指定目录的内容 + * + * @author 芋道源码 + */ +@Component("listDir") +public class ListDirToolB implements Function { + + @Data + @JsonClassDescription("列出指定目录的内容") + public static class Request { + + /** + * 要列出内容的目录路径 + */ + @JsonPropertyDescription("要列出内容的目录路径,例如说:/Users/yunai") + private String path; + + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Response { + + /** + * 目录内容列表 + */ + private List files; + + @Data + public static class File { + + /** + * 是否为目录 + */ + private Boolean directory; + + /** + * 名称 + */ + private String name; + + /** + * 大小,仅对文件有效 + */ + private String size; + + /** + * 最后修改时间 + */ + private String lastModified; + + } + + } + + @Override + public Response apply(Request request) { + // 校验目录存在 + String path = StrUtil.blankToDefault(request.getPath(), "."); + Path dirPath = Paths.get(path); + if (!FileUtil.exist(dirPath.toString()) || !FileUtil.isDirectory(dirPath.toString())) { + return new Response(Collections.emptyList()); + } + // 列出目录内容 + File[] files = dirPath.toFile().listFiles(); + if (ArrayUtil.isEmpty(files)) { + return new Response(Collections.emptyList()); + } + return new Response(convertList(Arrays.asList(files), file -> + new Response.File().setDirectory(file.isDirectory()).setName(file.getName()) + .setLastModified(LocalDateTimeUtil.format(LocalDateTimeUtil.of(file.lastModified()), NORM_DATETIME_PATTERN)))); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index bea8e5bc0d..720d933087 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -33,7 +33,7 @@ import org.springframework.context.annotation.Bean; * @author fansili */ @AutoConfiguration -@EnableConfigurationProperties({YudaoAiProperties.class, +@EnableConfigurationProperties({ YudaoAiProperties.class, QdrantVectorStoreProperties.class, // 解析 Qdrant 配置 RedisVectorStoreProperties.class, // 解析 Redis 配置 MilvusVectorStoreProperties.class, MilvusServiceClientProperties.class // 解析 Milvus 配置 @@ -139,15 +139,16 @@ public class YudaoAiAutoConfiguration { } // 特殊:由于混元大模型不提供 deepseek,而是通过知识引擎,所以需要区分下 URL if (StrUtil.isEmpty(properties.getBaseUrl())) { - properties.setBaseUrl(StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? - HunYuanChatModel.DEEP_SEEK_BASE_URL : HunYuanChatModel.BASE_URL); + 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()) + .baseUrl(properties.getBaseUrl()) + .apiKey(properties.getApiKey()) + .build()) .defaultOptions(OpenAiChatOptions.builder() .model(properties.getModel()) .temperature(properties.getTemperature()) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index dbbbbe2241..8565294b0c 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -61,6 +61,7 @@ import org.springframework.ai.minimax.MiniMaxChatModel; import org.springframework.ai.minimax.MiniMaxEmbeddingModel; import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; import org.springframework.ai.minimax.api.MiniMaxApi; +import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.moonshot.MoonshotChatModel; import org.springframework.ai.moonshot.api.MoonshotApi; import org.springframework.ai.ollama.OllamaChatModel; @@ -431,7 +432,7 @@ public class AiModelFactoryImpl implements AiModelFactory { 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).build(); + return OpenAiChatModel.builder().openAiApi(openAiApi).toolCallingManager(getToolCallingManager()).build(); } // TODO @芋艿:手头暂时没密钥,使用建议再测试下 @@ -465,7 +466,7 @@ public class AiModelFactoryImpl implements AiModelFactory { */ private static OllamaChatModel buildOllamaChatModel(String url) { OllamaApi ollamaApi = new OllamaApi(url); - return OllamaChatModel.builder().ollamaApi(ollamaApi).build(); + return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build(); } private StabilityAiImageModel buildStabilityAiImageModel(String apiKey, String url) { @@ -699,4 +700,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return SpringUtil.getBean(BatchingStrategy.class); } + private static ToolCallingManager getToolCallingManager() { + return SpringUtil.getBean(ToolCallingManager.class); + } + } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index c3a87358cd..b15fb2e54d 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -24,14 +24,18 @@ public class AiUtils { // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: + // TODO functions return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens).build(); case YI_YAN: return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case ZHI_PU: + // TODO functions return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case MINI_MAX: + // TODO functions return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case MOONSHOT: + // TODO functions return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case OPENAI: case DEEP_SEEK: // 复用 OpenAI 客户端 @@ -39,12 +43,18 @@ public class AiUtils { case HUN_YUAN: // 复用 OpenAI 客户端 case XING_HUO: // 复用 OpenAI 客户端 case SILICON_FLOW: // 复用 OpenAI 客户端 - return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); + return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) +// .toolNames() TODO + .toolNames("listDir") + .build(); case AZURE_OPENAI: // TODO 芋艿:貌似没 model 字段???! + // TODO 芋艿:.toolNames() TODO return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens).build(); case OLLAMA: - return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens).build(); + // TODO 芋艿:.toolNames() TODO + return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens) + .toolNames("listDir").build(); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/package-info.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/package-info.java index b02d1b3cef..e4655cd0b7 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/package-info.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/package-info.java @@ -4,7 +4,10 @@ * models 包路径: * 1. xinghuo 包:【讯飞】星火,自己实现 * 2. deepseek 包:【深度求索】DeepSeek,自己实现 - * 3. midjourney 包:Midjourney API,对接 https://github.com/novicezk/midjourney-proxy 实现 - * 4. suno 包:Suno API,对接 https://github.com/gcui-art/suno-api 实现 + * 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; \ No newline at end of file From ffe4afaaafea38e1c4ca54f1f64025b63fd23ac7 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 14 Mar 2025 13:26:23 +0800 Subject: [PATCH 47/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=96=B0=E5=A2=9E=20function=20ca?= =?UTF-8?q?ll=20=E7=A4=BA=E4=BE=8B=EF=BC=8C=E5=AE=8C=E6=88=90=E6=89=80?= =?UTF-8?q?=E6=9C=89=E6=A8=A1=E5=9E=8B=E7=9A=84=E6=B5=8B=E8=AF=95=20=3D=20?= =?UTF-8?q?=3D=20=E7=B4=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/AiChatMessageServiceImpl.java | 4 +- .../module/ai/service/tool/ListDirTool.java | 95 -------------- .../DirectoryListToolFunction.java} | 29 +++-- .../function/WeatherQueryToolFunction.java | 118 ++++++++++++++++++ .../module/ai/service/tool/package-info.java | 1 + .../ai/config/YudaoAiAutoConfiguration.java | 11 ++ .../ai/core/factory/AiModelFactoryImpl.java | 29 +++-- .../yudao/framework/ai/core/util/AiUtils.java | 34 ++--- 8 files changed, 186 insertions(+), 135 deletions(-) delete mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirTool.java rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/{ListDirToolB.java => function/DirectoryListToolFunction.java} (74%) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/WeatherQueryToolFunction.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/package-info.java diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java index 88f7144336..877ba77453 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.ai.core.util.AiUtils; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO; @@ -238,7 +239,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { // 2. 构建 ChatOptions 对象 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), - conversation.getTemperature(), conversation.getMaxTokens()); + conversation.getTemperature(), conversation.getMaxTokens(), + SetUtils.asSet("directory_list", "weather_query")); return new Prompt(chatMessages, chatOptions); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirTool.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirTool.java deleted file mode 100644 index 4315e549ae..0000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirTool.java +++ /dev/null @@ -1,95 +0,0 @@ -package cn.iocoder.yudao.module.ai.service.tool; - -import cn.hutool.core.date.LocalDateTimeUtil; -import cn.hutool.core.io.FileUtil; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.StrUtil; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.ai.tool.annotation.Tool; -import org.springframework.ai.tool.annotation.ToolParam; -import org.springframework.stereotype.Component; - -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; - -/** - * 目录内容列表工具:列出指定目录的内容 - * - * @author 芋道源码 - */ -@Component -public class ListDirTool { - - /** - * 列出指定目录的内容 - * - * @param relativePath 要列出内容的目录路径,相对于工作区根目录 - * @return 目录内容列表 - */ - @Tool(name = "listDir", description = "列出指定目录的内容") - public Response listDir(@ToolParam(description = "要列出内容的目录路径,相对于工作区根目录") String relativePath) { - // 校验目录存在 - String path = StrUtil.blankToDefault(relativePath, "."); - Path dirPath = Paths.get(path); - if (!FileUtil.exist(dirPath.toString()) || !FileUtil.isDirectory(dirPath.toString())) { - return new Response(Collections.emptyList()); - } - // 列出目录内容 - File[] files = dirPath.toFile().listFiles(); - if (ArrayUtil.isEmpty(files)) { - return new Response(Collections.emptyList()); - } - return new Response(convertList(Arrays.asList(files), file -> new Response.File() - .setDirectory(file.isDirectory()).setName(file.getName()) - .setSize(file.isFile() ? FileUtil.readableFileSize(file.length()) : null) - .setLastModified( - LocalDateTimeUtil.format(LocalDateTimeUtil.of(file.lastModified()), NORM_DATETIME_PATTERN)))); - } - - @Data - @AllArgsConstructor - @NoArgsConstructor - public static class Response { - - /** - * 目录内容列表 - */ - private List files; - - @Data - public static class File { - - /** - * 是否为目录 - */ - private Boolean directory; - - /** - * 名称 - */ - private String name; - - /** - * 大小,仅对文件有效 - */ - private String size; - - /** - * 最后修改时间 - */ - private String lastModified; - - } - - } - -} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirToolB.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/DirectoryListToolFunction.java similarity index 74% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirToolB.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/DirectoryListToolFunction.java index 3eafff4b2c..0ec86724ec 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirToolB.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/DirectoryListToolFunction.java @@ -1,10 +1,11 @@ -package cn.iocoder.yudao.module.ai.service.tool; +package cn.iocoder.yudao.module.ai.service.tool.function; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import lombok.AllArgsConstructor; import lombok.Data; @@ -12,8 +13,6 @@ import lombok.NoArgsConstructor; import org.springframework.stereotype.Component; import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -23,21 +22,22 @@ import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; /** - * 目录内容列表工具:列出指定目录的内容 + * 工具:列出指定目录的文件列表 * * @author 芋道源码 */ -@Component("listDir") -public class ListDirToolB implements Function { +@Component("directory_list") +public class DirectoryListToolFunction implements Function { @Data - @JsonClassDescription("列出指定目录的内容") + @JsonClassDescription("列出指定目录的文件列表") public static class Request { /** - * 要列出内容的目录路径 + * 目录路径 */ - @JsonPropertyDescription("要列出内容的目录路径,例如说:/Users/yunai") + @JsonProperty(required = true, value = "path") + @JsonPropertyDescription("目录路径,例如说:/Users/yunai") private String path; } @@ -48,7 +48,7 @@ public class ListDirToolB implements Function files; @@ -82,13 +82,12 @@ public class ListDirToolB implements Function { + + private static final String[] WEATHER_CONDITIONS = { "晴朗", "多云", "阴天", "小雨", "大雨", "雷雨", "小雪", "大雪" }; + + @Data + @JsonClassDescription("查询指定城市的天气信息") + public static class Request { + + /** + * 城市名称 + */ + @JsonProperty(required = true, value = "city") + @JsonPropertyDescription("城市名称,例如:北京、上海、广州") + private String city; + + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Response { + + /** + * 城市名称 + */ + private String city; + + /** + * 天气信息 + */ + private WeatherInfo weatherInfo; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class WeatherInfo { + + /** + * 温度(摄氏度) + */ + private Integer temperature; + + /** + * 天气状况 + */ + private String condition; + + /** + * 湿度百分比 + */ + private Integer humidity; + + /** + * 风速(km/h) + */ + private Integer windSpeed; + + /** + * 查询时间 + */ + private String queryTime; + + } + + } + + @Override + public Response apply(Request request) { + // 检查城市名称是否为空 + if (StrUtil.isBlank(request.getCity())) { + return new Response("未知城市", null); + } + + // 获取天气数据 + String city = request.getCity(); + Response.WeatherInfo weatherInfo = generateMockWeatherInfo(); + return new Response(city, weatherInfo); + } + + /** + * 生成模拟的天气数据 + * 在实际应用中,应替换为真实 API 调用 + */ + private Response.WeatherInfo generateMockWeatherInfo() { + int temperature = RandomUtil.randomInt(-5, 30); + int humidity = RandomUtil.randomInt(1, 100); + int windSpeed = RandomUtil.randomInt(1, 30); + String condition = RandomUtil.randomEle(WEATHER_CONDITIONS); + return new Response.WeatherInfo(temperature, condition, humidity, windSpeed, + LocalDateTimeUtil.format(LocalDateTime.now(), NORM_DATETIME_PATTERN)); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/package-info.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/package-info.java new file mode 100644 index 0000000000..ba65093ecc --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.module.ai.service.tool; \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 720d933087..ef3314a48b 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -1,6 +1,7 @@ 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.deepseek.DeepSeekChatModel; @@ -17,6 +18,7 @@ import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStore 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; @@ -70,6 +72,7 @@ public class YudaoAiAutoConfiguration { .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) + .toolCallingManager(getToolCallingManager()) .build(); return new DeepSeekChatModel(openAiChatModel); } @@ -96,6 +99,7 @@ public class YudaoAiAutoConfiguration { .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) + .toolCallingManager(getToolCallingManager()) .build(); return new DouBaoChatModel(openAiChatModel); } @@ -122,6 +126,7 @@ public class YudaoAiAutoConfiguration { .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) + .toolCallingManager(getToolCallingManager()) .build(); return new SiliconFlowChatModel(openAiChatModel); } @@ -155,6 +160,7 @@ public class YudaoAiAutoConfiguration { .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) + .toolCallingManager(getToolCallingManager()) .build(); return new HunYuanChatModel(openAiChatModel); } @@ -181,6 +187,7 @@ public class YudaoAiAutoConfiguration { .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) + .toolCallingManager(getToolCallingManager()) .build(); return new XingHuoChatModel(openAiChatModel); } @@ -210,4 +217,8 @@ public class YudaoAiAutoConfiguration { return new TokenCountBatchingStrategy(); } + private static ToolCallingManager getToolCallingManager() { + return SpringUtil.getBean(ToolCallingManager.class); + } + } \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index 8565294b0c..356715be26 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -23,6 +23,7 @@ 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; @@ -58,11 +59,14 @@ 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; @@ -90,10 +94,7 @@ import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservat 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.ZhiPuAiChatModel; -import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingModel; -import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingOptions; -import org.springframework.ai.zhipuai.ZhiPuAiImageModel; +import org.springframework.ai.zhipuai.*; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; import org.springframework.beans.BeansException; @@ -110,6 +111,7 @@ 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 模型工厂的实现类 @@ -308,7 +310,9 @@ public class AiModelFactoryImpl implements AiModelFactory { */ private static DashScopeChatModel buildTongYiChatModel(String key) { DashScopeApi dashScopeApi = new DashScopeApi(key); - return new DashScopeChatModel(dashScopeApi); + DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL) + .withTemperature(0.7).build(); + return new DashScopeChatModel(dashScopeApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); } /** @@ -385,7 +389,8 @@ public class AiModelFactoryImpl implements AiModelFactory { private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) : new ZhiPuAiApi(url, apiKey); - return new ZhiPuAiChatModel(zhiPuAiApi); + ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); + return new ZhiPuAiChatModel(zhiPuAiApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); } /** @@ -403,7 +408,8 @@ public class AiModelFactoryImpl implements AiModelFactory { private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) { MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey) : new MiniMaxApi(url, apiKey); - return new MiniMaxChatModel(miniMaxApi); + MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); + return new MiniMaxChatModel(miniMaxApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); } /** @@ -412,7 +418,8 @@ public class AiModelFactoryImpl implements AiModelFactory { private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) { MoonshotApi moonshotApi = StrUtil.isEmpty(url)? new MoonshotApi(apiKey) : new MoonshotApi(url, apiKey); - return new MoonshotChatModel(moonshotApi); + MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build(); + return new MoonshotChatModel(moonshotApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); } /** @@ -449,7 +456,7 @@ public class AiModelFactoryImpl implements AiModelFactory { // 获取 AzureOpenAiChatProperties 对象 AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class); return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, - null, null, null); + getToolCallingManager(), null, null); } /** @@ -704,4 +711,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return SpringUtil.getBean(ToolCallingManager.class); } + private static FunctionCallbackResolver getFunctionCallbackResolver() { + return SpringUtil.getBean(FunctionCallbackResolver.class); + } + } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index b15fb2e54d..becc54ee43 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -13,6 +13,8 @@ import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.qianfan.QianFanChatOptions; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; +import java.util.Set; + /** * Spring AI 工具类 * @@ -21,22 +23,27 @@ import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; 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 toolNames) { // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: - // TODO functions - return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens).build(); + 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: - // TODO functions - return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); + return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) + .functions(toolNames).build(); case MINI_MAX: - // TODO functions - return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); + return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) + .functions(toolNames).build(); case MOONSHOT: - // TODO functions - return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); + return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) + .functions(toolNames).build(); case OPENAI: case DEEP_SEEK: // 复用 OpenAI 客户端 case DOU_BAO: // 复用 OpenAI 客户端 @@ -44,17 +51,14 @@ public class AiUtils { case XING_HUO: // 复用 OpenAI 客户端 case SILICON_FLOW: // 复用 OpenAI 客户端 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) -// .toolNames() TODO - .toolNames("listDir") - .build(); + .toolNames(toolNames).build(); case AZURE_OPENAI: // TODO 芋艿:貌似没 model 字段???! - // TODO 芋艿:.toolNames() TODO - return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens).build(); + return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens) + .toolNames(toolNames).build(); case OLLAMA: - // TODO 芋艿:.toolNames() TODO return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens) - .toolNames("listDir").build(); + .toolNames(toolNames).build(); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } From e869aaee836cb96554e83cfd6a4b0088b76aa125 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 14 Mar 2025 19:29:36 +0800 Subject: [PATCH 48/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E5=A2=9E=E5=8A=A0=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E7=9A=84=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/enums/ErrorCodeConstants.java | 34 ++++---- .../admin/model/AiToolController.java | 73 ++++++++++++++++ .../admin/model/vo/tool/AiToolPageReqVO.java | 36 ++++++++ .../admin/model/vo/tool/AiToolRespVO.java | 27 ++++++ .../admin/model/vo/tool/AiToolSaveReqVO.java | 27 ++++++ .../ai/dal/dataobject/model/AiToolDO.java | 50 +++++++++++ .../ai/dal/mysql/model/AiToolMapper.java | 27 ++++++ .../ai/service/model/AiToolService.java | 54 ++++++++++++ .../ai/service/model/AiToolServiceImpl.java | 86 +++++++++++++++++++ .../tool}/DirectoryListToolFunction.java | 2 +- .../tool}/WeatherQueryToolFunction.java | 2 +- .../module/ai/service/tool/package-info.java | 1 - 12 files changed, 401 insertions(+), 18 deletions(-) create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiToolController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolPageReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolRespVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolSaveReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiToolMapper.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/{tool/function => model/tool}/DirectoryListToolFunction.java (97%) rename yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/{tool/function => model/tool}/WeatherQueryToolFunction.java (98%) delete mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/package-info.java diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java index 9530d5bf57..8b235f9ac5 100644 --- a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -32,29 +32,33 @@ public interface ErrorCodeConstants { ErrorCode CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "对话生成异常!"); // ========== API 绘画 1-040-005-000 ========== - ErrorCode IMAGE_NOT_EXISTS = new ErrorCode(1_022_005_000, "图片不存在!"); - ErrorCode IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_022_005_001, "Midjourney 提交失败!原因:{}"); - ErrorCode IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_022_005_002, "Midjourney 按钮 customId 不存在! {}"); + ErrorCode IMAGE_NOT_EXISTS = new ErrorCode(1_040_005_000, "图片不存在!"); + ErrorCode IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_040_005_001, "Midjourney 提交失败!原因:{}"); + ErrorCode IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_040_005_002, "Midjourney 按钮 customId 不存在! {}"); // ========== API 音乐 1-040-006-000 ========== - ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!"); + ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_040_006_000, "音乐不存在!"); - // ========== API 写作 1-022-007-000 ========== - ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_022_007_000, "作文不存在!"); - ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "写作生成异常!"); + // ========== API 写作 1-040-007-000 ========== + ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_040_007_000, "作文不存在!"); + ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_040_07_001, "写作生成异常!"); // ========== API 思维导图 1-040-008-000 ========== ErrorCode MIND_MAP_NOT_EXISTS = new ErrorCode(1_040_008_000, "思维导图不存在!"); - // ========== API 知识库 1-022-008-000 ========== - ErrorCode KNOWLEDGE_NOT_EXISTS = new ErrorCode(1_022_008_000, "知识库不存在!"); + // ========== API 知识库 1-040-009-000 ========== + ErrorCode KNOWLEDGE_NOT_EXISTS = new ErrorCode(1_040_009_000, "知识库不存在!"); - ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_022_008_101, "文档不存在!"); - ErrorCode KNOWLEDGE_DOCUMENT_FILE_EMPTY = new ErrorCode(1_022_008_102, "文档内容为空!"); - ErrorCode KNOWLEDGE_DOCUMENT_FILE_DOWNLOAD_FAIL = new ErrorCode(1_022_008_102, "文件下载失败!"); - ErrorCode KNOWLEDGE_DOCUMENT_FILE_READ_FAIL = new ErrorCode(1_022_008_102, "文档加载失败!"); + ErrorCode KNOWLEDGE_DOCUMENT_NOT_EXISTS = new ErrorCode(1_040_009_101, "文档不存在!"); + ErrorCode KNOWLEDGE_DOCUMENT_FILE_EMPTY = new ErrorCode(1_040_009_102, "文档内容为空!"); + ErrorCode KNOWLEDGE_DOCUMENT_FILE_DOWNLOAD_FAIL = new ErrorCode(1_040_009_102, "文件下载失败!"); + ErrorCode KNOWLEDGE_DOCUMENT_FILE_READ_FAIL = new ErrorCode(1_040_009_102, "文档加载失败!"); - ErrorCode KNOWLEDGE_SEGMENT_NOT_EXISTS = new ErrorCode(1_022_008_202, "段落不存在!"); - ErrorCode KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG = new ErrorCode(1_022_008_203, "内容 Token 数为 {},超过最大限制 {}"); + ErrorCode KNOWLEDGE_SEGMENT_NOT_EXISTS = new ErrorCode(1_040_009_202, "段落不存在!"); + ErrorCode KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG = new ErrorCode(1_040_009_203, "内容 Token 数为 {},超过最大限制 {}"); + + // ========== AI 工具 1-040-010-000 ========== + ErrorCode TOOL_NOT_EXISTS = new ErrorCode(1_040_010_000, "工具不存在"); + ErrorCode TOOL_NAME_NOT_EXISTS = new ErrorCode(1_040_010_001, "工具({})找不到 Bean"); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiToolController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiToolController.java new file mode 100644 index 0000000000..4e532642e7 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiToolController.java @@ -0,0 +1,73 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; +import cn.iocoder.yudao.module.ai.service.model.AiToolService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + + +@Tag(name = "管理后台 - AI 工具") +@RestController +@RequestMapping("/ai/tool") +@Validated +public class AiToolController { + + @Resource + private AiToolService toolService; + + @PostMapping("/create") + @Operation(summary = "创建AI 工具") + @PreAuthorize("@ss.hasPermission('ai:tool:create')") + public CommonResult createTool(@Valid @RequestBody AiToolSaveReqVO createReqVO) { + return success(toolService.createTool(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新AI 工具") + @PreAuthorize("@ss.hasPermission('ai:tool:update')") + public CommonResult updateTool(@Valid @RequestBody AiToolSaveReqVO updateReqVO) { + toolService.updateTool(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除AI 工具") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('ai:tool:delete')") + public CommonResult deleteTool(@RequestParam("id") Long id) { + toolService.deleteTool(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得AI 工具") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:tool:query')") + public CommonResult getTool(@RequestParam("id") Long id) { + AiToolDO tool = toolService.getTool(id); + return success(BeanUtils.toBean(tool, AiToolRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得AI 工具分页") + @PreAuthorize("@ss.hasPermission('ai:tool:query')") + public CommonResult> getToolPage(@Valid AiToolPageReqVO pageReqVO) { + PageResult pageResult = toolService.getToolPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiToolRespVO.class)); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolPageReqVO.java new file mode 100644 index 0000000000..f3e01ee60c --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolPageReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - AI 工具分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AiToolPageReqVO extends PageParam { + + @Schema(description = "工具名称", example = "王五") + private String name; + + @Schema(description = "工具描述", example = "你猜") + private String description; + + @Schema(description = "状态", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolRespVO.java new file mode 100644 index 0000000000..6d5a02e687 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolRespVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - AI 工具 Response VO") +@Data +public class AiToolRespVO { + + @Schema(description = "工具编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "19661") + private Long id; + + @Schema(description = "工具名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + private String name; + + @Schema(description = "工具描述", example = "你猜") + private String description; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolSaveReqVO.java new file mode 100644 index 0000000000..c85cfc33e7 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolSaveReqVO.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +@Schema(description = "管理后台 - AI 工具新增/修改 Request VO") +@Data +public class AiToolSaveReqVO { + + @Schema(description = "工具编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "19661") + private Long id; + + @Schema(description = "工具名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + @NotEmpty(message = "工具名称不能为空") + private String name; + + @Schema(description = "工具描述", example = "你猜") + private String description; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java new file mode 100644 index 0000000000..41e5bcc06b --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.model; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.ai.service.model.tool.DirectoryListToolFunction; +import cn.iocoder.yudao.module.ai.service.model.tool.WeatherQueryToolFunction; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * AI 工具 DO + * + * @author 芋道源码 + */ +@TableName("ai_tool") +@KeySequence("ai_tool_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiToolDO extends BaseDO { + + /** + * 工具编号 + */ + @TableId + private Long id; + /** + * 工具名称 + * + * 对应 Bean 的名字,例如说: + * 1. {@link DirectoryListToolFunction} 的 Bean 名字是 directory_list + * 2. {@link WeatherQueryToolFunction} 的 Bean 名字是 weather_query + */ + private String name; + /** + * 工具描述 + */ + private String description; + /** + * 状态 + * + * 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiToolMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiToolMapper.java new file mode 100644 index 0000000000..3b7a0f07ab --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiToolMapper.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.model; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolPageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * AI 工具 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface AiToolMapper extends BaseMapperX { + + default PageResult selectPage(AiToolPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(AiToolDO::getName, reqVO.getName()) + .eqIfPresent(AiToolDO::getDescription, reqVO.getDescription()) + .eqIfPresent(AiToolDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(AiToolDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(AiToolDO::getId)); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolService.java new file mode 100644 index 0000000000..feac364644 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolService.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.ai.service.model; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; +import jakarta.validation.Valid; + +/** + * AI 工具 Service 接口 + * + * @author 芋道源码 + */ +public interface AiToolService { + + /** + * 创建AI 工具 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createTool(@Valid AiToolSaveReqVO createReqVO); + + /** + * 更新AI 工具 + * + * @param updateReqVO 更新信息 + */ + void updateTool(@Valid AiToolSaveReqVO updateReqVO); + + /** + * 删除AI 工具 + * + * @param id 编号 + */ + void deleteTool(Long id); + + /** + * 获得AI 工具 + * + * @param id 编号 + * @return AI 工具 + */ + AiToolDO getTool(Long id); + + /** + * 获得AI 工具分页 + * + * @param pageReqVO 分页查询 + * @return AI 工具分页 + */ + PageResult getToolPage(AiToolPageReqVO pageReqVO); + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java new file mode 100644 index 0000000000..1ab17477f9 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.ai.service.model; + +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; +import cn.iocoder.yudao.module.ai.dal.mysql.model.AiToolMapper; +import jakarta.annotation.Resource; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.TOOL_NAME_NOT_EXISTS; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.TOOL_NOT_EXISTS; + +/** + * AI 工具 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class AiToolServiceImpl implements AiToolService { + + @Resource + private AiToolMapper toolMapper; + + @Override + public Long createTool(AiToolSaveReqVO createReqVO) { + // 校验名称是否存在 + validateToolNameExists(createReqVO.getName()); + + // 插入 + AiToolDO tool = BeanUtils.toBean(createReqVO, AiToolDO.class); + toolMapper.insert(tool); + return tool.getId(); + } + + @Override + public void updateTool(AiToolSaveReqVO updateReqVO) { + // 1.1 校验存在 + validateToolExists(updateReqVO.getId()); + // 1.2 校验名称是否存在 + validateToolNameExists(updateReqVO.getName()); + + // 2. 更新 + AiToolDO updateObj = BeanUtils.toBean(updateReqVO, AiToolDO.class); + toolMapper.updateById(updateObj); + } + + @Override + public void deleteTool(Long id) { + // 校验存在 + validateToolExists(id); + // 删除 + toolMapper.deleteById(id); + } + + private void validateToolExists(Long id) { + if (toolMapper.selectById(id) == null) { + throw exception(TOOL_NOT_EXISTS); + } + } + + private void validateToolNameExists(String name) { + try { + SpringUtil.getBean(name); + } catch (NoSuchBeanDefinitionException e) { + throw exception(TOOL_NAME_NOT_EXISTS, name); + } + } + + @Override + public AiToolDO getTool(Long id) { + return toolMapper.selectById(id); + } + + @Override + public PageResult getToolPage(AiToolPageReqVO pageReqVO) { + return toolMapper.selectPage(pageReqVO); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/DirectoryListToolFunction.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java similarity index 97% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/DirectoryListToolFunction.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java index 0ec86724ec..787b2e7728 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/DirectoryListToolFunction.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.ai.service.tool.function; +package cn.iocoder.yudao.module.ai.service.model.tool; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.io.FileUtil; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/WeatherQueryToolFunction.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java similarity index 98% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/WeatherQueryToolFunction.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java index cce6e6d43d..99262fafad 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/WeatherQueryToolFunction.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.ai.service.tool.function; +package cn.iocoder.yudao.module.ai.service.model.tool; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.util.RandomUtil; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/package-info.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/package-info.java deleted file mode 100644 index ba65093ecc..0000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package cn.iocoder.yudao.module.ai.service.tool; \ No newline at end of file From 1e8845ce6d229130ae4a804e578d52cf20f5fa15 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 14 Mar 2025 21:11:28 +0800 Subject: [PATCH 49/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E5=A2=9E=E5=8A=A0=20AI=20?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=EF=BC=8C=E4=B8=8E=20tool=20=E7=9A=84?= =?UTF-8?q?=E6=89=93=E9=80=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/model/AiToolController.java | 23 +++++--- .../model/vo/chatRole/AiChatRoleRespVO.java | 3 ++ .../vo/chatRole/AiChatRoleSaveMyReqVO.java | 3 ++ .../vo/chatRole/AiChatRoleSaveReqVO.java | 3 ++ .../ai/dal/dataobject/model/AiChatRoleDO.java | 7 +++ .../ai/dal/mysql/model/AiToolMapper.java | 8 +++ .../chat/AiChatMessageServiceImpl.java | 54 ++++++++++++------- .../service/model/AiChatRoleServiceImpl.java | 23 ++++++++ .../ai/service/model/AiToolService.java | 40 +++++++++++--- .../ai/service/model/AiToolServiceImpl.java | 16 +++++- 10 files changed, 147 insertions(+), 33 deletions(-) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiToolController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiToolController.java index 4e532642e7..e98f87e0b5 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiToolController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiToolController.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.ai.controller.admin.model; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -17,8 +18,10 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import java.util.List; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @Tag(name = "管理后台 - AI 工具") @RestController @@ -30,14 +33,14 @@ public class AiToolController { private AiToolService toolService; @PostMapping("/create") - @Operation(summary = "创建AI 工具") + @Operation(summary = "创建工具") @PreAuthorize("@ss.hasPermission('ai:tool:create')") public CommonResult createTool(@Valid @RequestBody AiToolSaveReqVO createReqVO) { return success(toolService.createTool(createReqVO)); } @PutMapping("/update") - @Operation(summary = "更新AI 工具") + @Operation(summary = "更新工具") @PreAuthorize("@ss.hasPermission('ai:tool:update')") public CommonResult updateTool(@Valid @RequestBody AiToolSaveReqVO updateReqVO) { toolService.updateTool(updateReqVO); @@ -45,7 +48,7 @@ public class AiToolController { } @DeleteMapping("/delete") - @Operation(summary = "删除AI 工具") + @Operation(summary = "删除工具") @Parameter(name = "id", description = "编号", required = true) @PreAuthorize("@ss.hasPermission('ai:tool:delete')") public CommonResult deleteTool(@RequestParam("id") Long id) { @@ -54,7 +57,7 @@ public class AiToolController { } @GetMapping("/get") - @Operation(summary = "获得AI 工具") + @Operation(summary = "获得工具") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('ai:tool:query')") public CommonResult getTool(@RequestParam("id") Long id) { @@ -63,11 +66,19 @@ public class AiToolController { } @GetMapping("/page") - @Operation(summary = "获得AI 工具分页") + @Operation(summary = "获得工具分页") @PreAuthorize("@ss.hasPermission('ai:tool:query')") public CommonResult> getToolPage(@Valid AiToolPageReqVO pageReqVO) { PageResult pageResult = toolService.getToolPage(pageReqVO); return success(BeanUtils.toBean(pageResult, AiToolRespVO.class)); } + @GetMapping("/simple-list") + @Operation(summary = "获得工具列表") + public CommonResult> getToolSimpleList() { + List list = toolService.getToolListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(convertList(list, tool -> new AiToolRespVO() + .setId(tool.getId()).setName(tool.getName()))); + } + } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java index 908e9dcce4..51e44ed760 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java @@ -49,6 +49,9 @@ public class AiChatRoleRespVO implements VO { @Schema(description = "引用的知识库编号列表", example = "1,2,3") private List knowledgeIds; + @Schema(description = "引用的工具编号列表", example = "1,2,3") + private List toolIds; + @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Boolean publicStatus; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java index a104421f5d..009e8d8afb 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java @@ -34,4 +34,7 @@ public class AiChatRoleSaveMyReqVO { @Schema(description = "引用的知识库编号列表", example = "1,2,3") private List knowledgeIds; + @Schema(description = "引用的工具编号列表", example = "1,2,3") + private List toolIds; + } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java index 6851564e06..3c72cf9834 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java @@ -47,6 +47,9 @@ public class AiChatRoleSaveReqVO { @Schema(description = "引用的知识库编号列表", example = "1,2,3") private List knowledgeIds; + @Schema(description = "引用的工具编号列表", example = "1,2,3") + private List toolIds; + @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "是否公开不能为空") private Boolean publicStatus; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java index 81a679a2f3..4a65796a96 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java @@ -74,6 +74,13 @@ public class AiChatRoleDO extends BaseDO { */ @TableField(typeHandler = LongListTypeHandler.class) private List knowledgeIds; + /** + * 引用的工具编号列表 + * + * 关联 {@link AiToolDO#getId()} 字段 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List toolIds; /** * 是否公开 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiToolMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiToolMapper.java index 3b7a0f07ab..d5d296692a 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiToolMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiToolMapper.java @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolPageReqVO import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; import org.apache.ibatis.annotations.Mapper; +import java.util.List; + /** * AI 工具 Mapper * @@ -24,4 +26,10 @@ public interface AiToolMapper extends BaseMapperX { .orderByDesc(AiToolDO::getId)); } + default List selectListByStatus(Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(AiToolDO::getStatus, status) + .orderByDesc(AiToolDO::getId)); + } + } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java index 877ba77453..f310ba69fd 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -7,7 +7,6 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.ai.core.util.AiUtils; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO; @@ -19,6 +18,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper; import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; @@ -27,6 +27,7 @@ import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchR import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; import cn.iocoder.yudao.module.ai.service.model.AiModelService; +import cn.iocoder.yudao.module.ai.service.model.AiToolService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.messages.Message; @@ -50,6 +51,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_NOT_EXIST; @@ -82,6 +84,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { private AiKnowledgeSegmentService knowledgeSegmentService; @Resource private AiKnowledgeDocumentService knowledgeDocumentService; + @Resource + private AiToolService toolService; @Transactional(rollbackFor = Exception.class) public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) { @@ -118,11 +122,13 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { String newContent = chatResponse.getResult().getOutput().getText(); chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent)); // 3.4 响应结果 - List segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, + List segments = BeanUtils.toBean(knowledgeSegments, + AiChatMessageRespVO.KnowledgeSegment.class, segment -> { - AiKnowledgeDocumentDO document = knowledgeDocumentService.getKnowledgeDocument(segment.getDocumentId()); - segment.setDocumentName(document != null ? document.getName() : null); - }); + AiKnowledgeDocumentDO document = knowledgeDocumentService + .getKnowledgeDocument(segment.getDocumentId()); + segment.setDocumentName(document != null ? document.getName() : null); + }); return new AiChatMessageSendRespVO() .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) @@ -130,7 +136,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } @Override - public Flux> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId) { + public Flux> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, + Long userId) { // 1.1 校验对话存在 AiChatConversationDO conversation = chatConversationService .validateChatConversationExists(sendReqVO.getConversationId()); @@ -143,7 +150,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { StreamingChatModel chatModel = modalService.getChatModel(model.getId()); // 2. 知识库找回 - List knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), conversation); + List knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), + conversation); // 3. 插入 user 发送消息 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, @@ -167,7 +175,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { if (StrUtil.isEmpty(contentBuffer)) { segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, segment -> TenantUtils.executeIgnore(() -> { - AiKnowledgeDocumentDO document = knowledgeDocumentService.getKnowledgeDocument(segment.getDocumentId()); + AiKnowledgeDocumentDO document = knowledgeDocumentService + .getKnowledgeDocument(segment.getDocumentId()); segment.setDocumentName(document != null ? document.getName() : null); })); } @@ -192,7 +201,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } private List recallKnowledgeSegment(String content, - AiChatConversationDO conversation) { + AiChatConversationDO conversation) { // 1. 查询聊天角色 if (conversation == null || conversation.getRoleId() == null) { return Collections.emptyList(); @@ -212,8 +221,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } private Prompt buildPrompt(AiChatConversationDO conversation, List messages, - List knowledgeSegments, - AiModelDO model, AiChatMessageSendReqVO sendReqVO) { + List knowledgeSegments, + AiModelDO model, AiChatMessageSendReqVO sendReqVO) { List chatMessages = new ArrayList<>(); // 1.1 System Context 角色设定 if (StrUtil.isNotBlank(conversation.getSystemMessage())) { @@ -236,11 +245,18 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { chatMessages.add(new UserMessage(String.format(KNOWLEDGE_USER_MESSAGE_TEMPLATE, reference))); } - // 2. 构建 ChatOptions 对象 + // 2.1 查询 tool 工具 + Set toolNames = null; + if (conversation.getRoleId() != null) { + AiChatRoleDO chatRole = chatRoleService.getChatRole(conversation.getRoleId()); + if (chatRole != null && CollUtil.isNotEmpty(chatRole.getToolIds())) { + toolNames = convertSet(toolService.getToolList(chatRole.getToolIds()), AiToolDO::getName); + } + } + // 2.2 构建 ChatOptions 对象 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), - conversation.getTemperature(), conversation.getMaxTokens(), - SetUtils.asSet("directory_list", "weather_query")); + conversation.getTemperature(), conversation.getMaxTokens(), toolNames); return new Prompt(chatMessages, chatOptions); } @@ -255,8 +271,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { * @return 消息上下文 */ private List filterContextMessages(List messages, - AiChatConversationDO conversation, - AiChatMessageSendReqVO sendReqVO) { + AiChatConversationDO conversation, + AiChatMessageSendReqVO sendReqVO) { if (conversation.getMaxContexts() == null || ObjUtil.notEqual(sendReqVO.getUseContext(), Boolean.TRUE)) { return Collections.emptyList(); } @@ -285,9 +301,9 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } private AiChatMessageDO createChatMessage(Long conversationId, Long replyId, - AiModelDO model, Long userId, Long roleId, - MessageType messageType, String content, Boolean useContext, - List knowledgeSegments) { + AiModelDO model, Long userId, Long roleId, + MessageType messageType, String content, Boolean useContext, + List knowledgeSegments) { AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId) .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId) .setType(messageType.getValue()).setContent(content).setUseContext(useContext) diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java index f0e07f2856..b0005c3af5 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java @@ -39,11 +39,15 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { @Resource private AiKnowledgeService knowledgeService; + @Resource + private AiToolService toolService; @Override public Long createChatRole(AiChatRoleSaveReqVO createReqVO) { // 校验文档 validateDocuments(createReqVO.getKnowledgeIds()); + // 校验工具 + validateTools(createReqVO.getToolIds()); // 保存角色 AiChatRoleDO chatRole = BeanUtils.toBean(createReqVO, AiChatRoleDO.class); @@ -55,6 +59,8 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { public Long createChatRoleMy(AiChatRoleSaveMyReqVO createReqVO, Long userId) { // 校验文档 validateDocuments(createReqVO.getKnowledgeIds()); + // 校验工具 + validateTools(createReqVO.getToolIds()); // 保存角色 AiChatRoleDO chatRole = BeanUtils.toBean(createReqVO, AiChatRoleDO.class).setUserId(userId) @@ -69,6 +75,8 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { validateChatRoleExists(updateReqVO.getId()); // 校验文档 validateDocuments(updateReqVO.getKnowledgeIds()); + // 校验工具 + validateTools(updateReqVO.getToolIds()); // 更新角色 AiChatRoleDO updateObj = BeanUtils.toBean(updateReqVO, AiChatRoleDO.class); @@ -84,6 +92,8 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { } // 校验文档 validateDocuments(updateReqVO.getKnowledgeIds()); + // 校验工具 + validateTools(updateReqVO.getToolIds()); // 更新 AiChatRoleDO updateObj = BeanUtils.toBean(updateReqVO, AiChatRoleDO.class); @@ -103,6 +113,19 @@ public class AiChatRoleServiceImpl implements AiChatRoleService { knowledgeIds.forEach(knowledgeService::validateKnowledgeExists); } + /** + * 校验工具是否存在 + * + * @param toolIds 工具编号列表 + */ + private void validateTools(List toolIds) { + if (CollUtil.isEmpty(toolIds)) { + return; + } + // 遍历校验每个工具是否存在 + toolIds.forEach(toolService::validateToolExists); + } + @Override public void deleteChatRole(Long id) { // 校验存在 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolService.java index feac364644..fb23224a83 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolService.java @@ -6,6 +6,9 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolSaveReqVO import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; import jakarta.validation.Valid; +import java.util.Collection; +import java.util.List; + /** * AI 工具 Service 接口 * @@ -14,7 +17,7 @@ import jakarta.validation.Valid; public interface AiToolService { /** - * 创建AI 工具 + * 创建工具 * * @param createReqVO 创建信息 * @return 编号 @@ -22,33 +25,56 @@ public interface AiToolService { Long createTool(@Valid AiToolSaveReqVO createReqVO); /** - * 更新AI 工具 + * 更新工具 * * @param updateReqVO 更新信息 */ void updateTool(@Valid AiToolSaveReqVO updateReqVO); /** - * 删除AI 工具 + * 删除工具 * * @param id 编号 */ void deleteTool(Long id); /** - * 获得AI 工具 + * 校验工具是否存在 * * @param id 编号 - * @return AI 工具 + */ + void validateToolExists(Long id); + + /** + * 获得工具 + * + * @param id 编号 + * @return 工具 */ AiToolDO getTool(Long id); /** - * 获得AI 工具分页 + * 获得工具列表 + * + * @param ids 编号列表 + * @return 工具列表 + */ + List getToolList(Collection ids); + + /** + * 获得工具分页 * * @param pageReqVO 分页查询 - * @return AI 工具分页 + * @return 工具分页 */ PageResult getToolPage(AiToolPageReqVO pageReqVO); + /** + * 获得工具列表 + * + * @param status 状态 + * @return 工具列表 + */ + List getToolListByStatus(Integer status); + } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java index 1ab17477f9..59f8f74d1f 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java @@ -12,6 +12,9 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import java.util.Collection; +import java.util.List; + import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.TOOL_NAME_NOT_EXISTS; import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.TOOL_NOT_EXISTS; @@ -59,7 +62,8 @@ public class AiToolServiceImpl implements AiToolService { toolMapper.deleteById(id); } - private void validateToolExists(Long id) { + @Override + public void validateToolExists(Long id) { if (toolMapper.selectById(id) == null) { throw exception(TOOL_NOT_EXISTS); } @@ -78,9 +82,19 @@ public class AiToolServiceImpl implements AiToolService { return toolMapper.selectById(id); } + @Override + public List getToolList(Collection ids) { + return toolMapper.selectBatchIds(ids); + } + @Override public PageResult getToolPage(AiToolPageReqVO pageReqVO) { return toolMapper.selectPage(pageReqVO); } + @Override + public List getToolListByStatus(Integer status) { + return toolMapper.selectListByStatus(status); + } + } \ No newline at end of file From 9dbadbbb5d2956d68b62060bf92fec00f200a58f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Fri, 14 Mar 2025 22:12:31 +0800 Subject: [PATCH 50/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=9B=B4=E6=96=B0=E6=9C=80?= =?UTF-8?q?=E6=96=B0=E7=9A=84=20AI=20=E4=BB=8B=E7=BB=8D=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .image/common/ai-feature.png | Bin 21387 -> 25498 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.image/common/ai-feature.png b/.image/common/ai-feature.png index fadc1cc5d660ef1e3a03fcdb472cba4187ff33b3..552ed59b424610bf8438003a1c839e197bda7eed 100644 GIT binary patch literal 25498 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 ArvLx| literal 21387 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 z zNfB?3CskY?FaP9V8K0y&&A1%0R!aJK{5+SRAIttG4o6%_{I=g+NW5_8F9Y1`)bBkQ zTi7X829muV%RYT@D?6AjD@?9L{)I{&-iya+md*-{H_kp+n25SI^e(nB!WMMmF6QW0 zV`@2V`PtKF)j2j{+4Q^ z$$`t1>B3AXaz5|G<6%&&oVbBEb#t$M%OV{)G{0EcO@;{jET1tfpTyI0IW)s2-NF*H zx7@2XXM)y5$I@Q9#U6Dl?VJ2e2#=pzu9WwuBo5+nWSVbI+WM)w4;k2LtIdnMrbV$r z_Fx3H#)}53%+c)C@`b4Eq_e14XebiK}_?4YDML^vLd3sVh1Odn8Z5;VMXIz+so)T^@Jz~nb{y;ilG>_ z!|)U+Bb+b3#+rBh>woS5SUAPLGpX?0@(#7Z&A{0{=3KP?n&SS+$RYc$gQ3N&S=$`; z^_Lq===5~bQgHiQ$wze|WeGRWDmaX@a=S_31KkumV3omD>^G4XydCTW)!)`}Gg4zd zbQ3&@1F~xu9O6K)*$!O=5Y-;o`qv3KSBDqBJ5}J%Y2vE_EPZDnqcqSDNq-y5ufmze z-g95S|5L&xtOgV5vcOKb?3-=bsg$Z$ZJ50-D+1%M>;1|eko{9TcQ)tHW-69~%krk( z{4!(Ip~3Nt-+2FF9SMgHGCH$kT6h!5o+FPQ@E3w(JBi)7cMMrtp6Cgx z#}*=QQ^BZQ*~J4NcUIP!Dq*=3uPXJBv{JHd#5!P9(YJ=HAm>SnkliFTV=7fA)YY$& zoIZOi;J8k1%>S~tRL$b)bGO}ZUf3f9rtdvF172;nyD&Wkemg4t5!zP*Pd=4;o%fCi zECk$o7VByNbQ6|)0q(B1>R&Nw&h{}q>-ZUWWJdN8@+Gflm)EJm0s0drlqw;o75;u8 z2NkX8WrU#3Xu(6%lh(W|qvY_9N6 z5%sRRC3Yq2-Jcm@Oah6@Gu^aqZUxLQAMp#neII!oM?{}W7cTzBlIFr*s$|jDzs{DO zmDA4`l;BzMwi8>2Iy)JBA>>D-VK>TmNq_{^AWI>BDSx zwYs0#dbkMN*$n>=t+C4qIFO9bJz?wI4r1JLALb_kd>b$L{`^blI#lBi#e4xXIwA8HGMrUc@ru>5$TQA+l zcU23Yy8TEbe?}I34*~3!XC&sQi{3pCHXr%!7oxE!Ng&=(6+qVi_7YhP!H!I@1gW}( zB9*NRrlQnu3h-ZUMcl4PPYbqzO`TTRob3W<%6)$F%k1`@tT#1G-rlv4MwGxJZi57K zYEO;=E5>X5kY1f1{;0>m<&Hsn~7~n zJD-CHO1$y0mxXV>_=Ia_Asu~Edw+#<=&h>Wn(D&R9T5rEhw}tZc3qhd6_UQ~W3@+2 zfMy}v&so<8$I2qv9I~%8rq4d1I{Np`);dflHtTYXB62IAV!p7g;?IpW>TsOom+2{AjKh-!ZR&HbfBDrE>>fgrK`nfJ-h(3*8*I*&t^O1kJ z?(5=gC6I$%rq!wkYQ`7w!qXSCFeTj<)ClZ-6N#K?h%J;c(ZyuO`IR-V`sKLm8@86R zUilEo&)v1rpvIgBg>QI#36ccG+Jq4TiDYMmD*UOnAuL}AAHG5`WiWnSj07knibyjH zfH>fDl5w%M1|Pw9Pj|B)lMyUr3RVc$ejH&V&n>0Aee?%0l19m^V*u-Wg#y%7#pE2a zLtuO982*)yk@73g@((fZz?zl`@H0`J&moVt9f%6lxai(B&V)s>XKG|XK96sxc$%Meq9PEuGGTJAhIVG`10%Z+puw5 zXgZFbBgiJxb_tv?TSw79c?V8XR3w14Fx~*P3QOSEZILTq|Cd{iEP&Kn1`60&k6W;OAQf^TyU_C+1|Z$qYJLQ_#qJdc8_OX zYXEtxAcgcyZSmaOn`-gQ4u2mDun)e*$giZ0%uARf|9LKY-rTN&LB^TTM5ew#T{lBo zk5ax;GA39ldRhx$wEq;{kzcJQ7x(>Co+SrzY0CU;I)E(R(S?!3^0#hh>lIF$Gl|%~ zaE2QukxYCVO9y5-P(mdR>8+v?^L#oa9M1;CidDHApOYo=MM)98|NOu}P5v|7c7c~Q zI}vaLXMG&#Cc!6C|4j7$g;3~yLJ<(k&sIjF_Qt)4jV0da&4{{!t6fpk;-FgQ$u5uW z3q140tmTe-NokJPH?e(}+>a?=2K^q(t9(%9^xO!C@heoNiHFyk?44!pi&;jMIHuVI zvC8lhvR!-reDeWhgq;)mn=w7xpFhrJ^CoRbvJn97oUdWYy6b%+v41>R&WjmXI%m4t z$UdEp9L?`_#-I!mW9Id63uIL<;#3}zQJLVkjz-+HUqju}y*FnwPa__g*5|s4Y+R0} z%^^A2SgP#!-^ZBTB=_O>WaLcp4->h_U?)MV7Ps3R2Zg-6^<6eqZZy|hbCbl(+1g4q z`abq9^davv=8%LFY?J6mHm-n(!(C0wJ=&ZSe@Ps00&|ArIwt*l1lIV7d?i4?&mDd; zsno~fz&2OQh%Il8pU-E=QP})?O`em2Fr<*1f;SI#@c2tgs$-BW!zs>44gNZo*)R(& zh@Rx%$+4#!V1qtX0-4rA={k?r2u>!$Xya8wUVKTep`U@ zoLq*|XmbXSC-S)!gjjaIhbT*j4K>rpbm;wpwhZw`Bp^XJcc_X5D&UN$tJl-Vo%dxU zSN?+ROuy28-MA<|^`0^Gy^ZR7MJBooT3IE-)NlLLWVSK` z*7Nrvx?nqmhrhX{UDqQX2V;-v?6nq23bT_MWL!8EyhyCF)j+8Rym@Tt8Mv;jQB0C% zEjPs%h*7H#$*=+F@L%Fae{k`6IQeJ$Ov^YBHv=}8lGr<;D4?38cc_aXaTIr$u%I5x z=w^t3&6!dZSAEd*bsHsnUJ(43Rq-8}KJM-+VqoL7M(4qRKFHma1LMn&6fBD1NoVa& zp6YqVPkJCT^EVbjs~I}NZ;d{1MWhhsS_Ie~#5q%jzr!F}e!5IHDEsgcGo7NE(mS%< z4vb{-ZQ1Gr_2q`H3X@lTz<{TI45FG4nsHvZ-0yoX2c$F&iE@sc^8HOhYqj8da-2}; z{~mN%fV}U><#bR|>EVXbU1-8lQHUsQxVju)G~z*#&Wi9_ezMNe7bIT=6xT(7NkxVD ztuD$qyd%lS2D7z536O0=2%`#<3?a_mBQIp>YoN_caSg%3MJb-MT#?GjIM9%1|MOG` zt0q)`I__=j8WA8%e=@u%#;wS%UKxU5j&^{UKAtN~ba3+7yynRxz6SEZKji=SlW&!) z3@^M(6Qd7;vm`(!kM(X;d*I2?x;5()4v*BHpVfHhtow4fo%i>TpO;0)G~<`=sAsqH z)@FOotwr4Z7({i#7Ca?MF4JjphhsaR}r{De~GM zKtQU%agaq?*s1}F?j?{F4dMQ$DC0}5Z9?C(N(@^a{+t9fH(f9fi~Q4pOYl~!5I+0K z$KIE>e(rG)x1E$ze1HR2F>-Ypillj>wT#hi#uQNz5^X$X`)Sw5*46#eyFs?oO z91pq3RybvSz<>vDiLW1aSB{?jVwpP8mO!Wbhn}AsSLpY`>&)cixm6*f#bAhARwCH4 zLxzsZ?x%>>bk2f-td}xZCST{XelGu~{di7$7jCswUIX=($CO`Uy+-+Z37leXK6X2+ zpnk9XOuMZD6EB1J3Q9)^s`*>==%}hpk{%3h*#UL_EFMyt%knZt)HWqX7=`)Z_gs%; z*wNl_cq_~pc;bC(6Rk-lsWNhXK*PUSfhB47$!wAuq%_j<8`uuZk2?4xoJw)BYVVoR z#0}NQ`50tcg?Y$DzN9!;;Vz>1VQd1~JK`a;)Yk)16x##CdpAP*(6OpCkG-2_}! zX)cl9C1U@Eqi*OUZWW+c$D2GqU;XiJA`Ya&uPnV-WNa`3NHZ5YL_Y}iAcU!5S5&ao zJ8b~tS){}#_4C}>udvkaYz=k<_}n#ju@b!h)(U_{|m| zWOfs!NTBk-43<2g%|W05a1a>FaySq|EI9}umJhcs!2uD7O71_i!-IE+LmC_e^Evfv zF1;KC*WL9hAhXUWXYF}6k(bF~0ao)*wzn`eLG@Bn^9ze39dGMA0b=O)RNifZVRmN@ z7B%dcCkuk*Gg-WFxi2zkLu$aUU`bZ}SByGLxsJVmMno}K@`*ov(roBPRI{lOLo8d^ zN{%Lyf=C4$z>(_B4`?W(>Y&8r@w>i>QCh#GTK)c)MYv`MKkAYEcmn~!!e=Xy){AWgHUhP=gjSdRXA3h zr96+G&c)`@Ou|R4u$`X>#GqI|M@?{Mno^XNBH*<0PX!w=g0I1^s3C^xDYP;C0jlx@ zT+DEuEi!n(aA7w1EW|@A%#m^}gqEi%dgA9WjSDNOQn!l*ijTJ7A82ZTq}V`GF9Yp8?~G zG51dao44!I#&T^A`2-5X*d8w8deNxYX}on++lU z^x_0hi(0^vP9HA}(fk>Sz?%__>t~yikVn)!Z)Z>arOPW;yilD31&Y6MExG4JC&u_1 zLVi=xwHo0YXM!(Qu0x9xPhaQY_8K?ta2sM&vNR=F4A?UNYSkb!2xjAB?#6B1Co%qr zv}<`1waG}Cy6`ygE0zKfPvE$|rU?p19A*X1oSerX{%WE5nvdj64$B88hjB9}3%*BG z-j^W{4w*`4sLo=11bXiUqMfyTMb^+`Ze1bd$zlJuWDZ$bjHxR7LwjHwCBgh0CBYc zluDMS?)|)u0xxBgIb~+6DalzC^3$_8d@$+1H0#Y6tDjc2LmP=ihIaY9;9cq1 z|Gm8;;TwZY$*-^KB^v8jDYc5!8gMSsEYxKBlFp1%EDwCasreGj&4o$E&iPYSfGMid z(c+`cpX{zr61xDnGs{OdC!g!$7a!t*GOhJM_*RSRGu|97r*~(Z-aSSze77c(zE*)K z2Oip&VkzyqAA7anTmEItM9|1!L}hV;r>E~k!X|#;TfF#03LGD34?QrfvVp(joL`c$ zybcx-8pZ|+dF=a|$bXfDN&@hVpZIE9@-b8pOAQu>`@NsxlCW!6CP;ro`KTz&vGmz5 zoj&fJ?7UIIlPvL6rewJI8t>tX$n1twmoCA!zg z^{zlx*Z~p793>~<^Oy-6xK6E(u@*&a6YT%$<;tU>e!o8xmLwD(`RbhSIln)CfBw$-{q@}E zxzFp~bMN!qbI(1`xlg(^X69$X^RLh5`SZOlLilpxWu5mTP+J1|MZ%9cPt!ze{j0 z=Ly~dG`Fg4nUmLLzN`k%>USdw4vnPhBAzGj@INM8S;0B`=J-cGhyY)5U%A!;y2yqz zCTlhu;edvp&?L&-6z2x@!-x%liJ%sz*Ie@w}9bE>X}qX>bt0^^L|Ql**?A zvuOB-WFHKn9Lg(GNM+vHi=#bBv~;viU>TtB)5Ccz`N~ZTNxYtKSyVvAv%`D|IUUjP zJ@a%C)L7e6&bx`H)w`_O#zr(uy162(xA&xG-z^mDH#+QI{=;OxC?)N~7N}nM3cP}A z#zFqJCA_<~^f6+B-u^L6s`laG0Fs-{Adz-lHaXzu{6lq>RQgkN;X$*j=1G@*zq%*LHgVA;U(7=erHrWu8dhse~33P0b>Prl$eupY5ap`GM z1|Y>6cIqgs&0L}x0@YT%%M&ar^mm_%BMijqt8J7lK@AQrymn*0k$37mLM( zX>3D>6>=9QE_)IYuSEJiXNv458()9^K0x!`>UwqH{)sxLx(Dm7I>3NW5#J8&H3AP_ z?RN?XV;j>yT`DeJDw9PmI)_4&NW`dSBc-6cR6?3~Q3h8HMKaRh|eJdNMe$(KVTtHMD%(q}rlRG~KTaeArKPsDFu4Y6v_(U7${ zkI2ZX{{Cjr>_~Qi2`~}`Cie9iEvJ+Z1vc4p>-Ev3CIx1r^DAvrjfNb{Ft*BLAxaO1 zvw1kR*NPc&o(>)=iRNRQ%TS;q9kK9TDP()Y)j)vj&ee0Dr8uB4As%OH8*R`fFCqc z*dM<@T_0NVOrQ11p?_bEAibCkac{>2BN?|xHq$< z+1dCd_HVUpq(~B1QUW_;kgviJKlAo`EWB%d<9#OFkJ~U72K^dxEp&e`1bY_00Pot% z7`xqJIuUlVt6%I*1h*>tbZBqx@>tU`F6`tjKT-n0I$U4Ay55RMyK3`@RHwOk{-}#+ zcM*b$a|b5Il>x;SQ$@+^*Mzlg`BV+{<(I@UzECrS;-U#TwX|xa6T+rzp5u%0Bwm7S zJD}oB4_?%$C?`HPT#TJTdu7GYUd(E4mkM>6!`;g}E8|h8J6a_5a{RU&n>j<^S14pV z9R>NE!4vAj`>)h%U!!81;1yofnt?!P(Fcc$kHe67=C01Q;A*2%A18uF*#q6OW28yH zVWq`)!XQjT1KqpztzgN~J8ttsd6Wb)dZi8O?2YK$C<(xh@C~{rR3>Oa%os5b3>21H zz#1hmU}G{I#@l4LQ5shya6<=4j&1_2zIk>?D5-z2m9z9-{lnDKUEnA5BkdSv=$mel z_H1v&IKuCD7-qqKFAp4jBI0ZGdpRQMf?=gNch2cucm#5=D?3cE6De++_H}z%)b3lO`6O}hK-d-Q$}66lD5Ho0hNLT zF1uQDf!96yr&VwzRLT+bdr!HZw}SVp_2-2vVXJsp?Crkh-LLQTrm9K*Z0G4ifr6X% z`Ab*01H*|X8XDrbPfW)2jEC4_ri+zRjt-Uo0@4G{=kQ>CRGKaPozb>C$Qd6^YF;<` zwUwo>0O0fj9(E|2pyk{=vHhK#`*>(N`odL$=qH+V;zx znZ{~WmK+Y=T0=)B*t>;}c#}t*=F~3L22R}o7G~>JYdYM3+pfR_Mt#xdpuCKO=f=YQ z&R0@&4y;v94^T{Rm2LxjTfrLgC1%zoNT7%{QrLPX(e?iV!oy_6jlc$0%F6gssCsO@ zKOowA9LHW6$8CC;{Z9BtmWBNLan3`^J{QX~hF9QF)ycF2&68mMdN#yP(ek2o2Tv)YVoHFpd-jH0yFZ^onVRyaE&s^DH(-rtI&4GFTH>khG}aqvII9to zF?5!}sFc|(i{6#q_4m4GD7w;tGg-{xS0#&@zB5xu!W0Q*Zs`$Z>I!i68S7Doci(( zO7byy89Uh~*hvMgQHM9Pmgk}eqq~;iPYFVXA_*fF&l4xGZ)^ z{>^8_VkB)vTahw$ub?aAx_@NRRWbYunQ42)jeh^&OoD-|TM@r)!AAt$yc6}Xw6gJA zMrcMh8k-&niiq-26=ih@WcoT%_dP0zimP@)ATa$yh|k-=#1*+>+ow?tzzm6e)ihq zuCQAcJuZ*qr=2L0oT1BziMNcu`Kpd}reE*ATvtZ8xp<41wdu{+XG^=I&WB7H5mvn% zMX|wexJ1MGdYx-w7<aD zcY13j#ZeOqIm)rVz}|56tLNJ@=|4LNG`HSB`#VW(plt_O(;V~v5lV|BrG0{|^~@9m z@Xwan_hu>Rr5P)?bo)+mtISQ^6sCChf;BV{_W9{B+*C#o3rP|Q3GPJov!wFHC7}hW z`7x^OX=VDS+b$_eY`h0+Sm4j~-S0b&Iqs%)5e>$|#ERwT*$--!mIMRI^;AkN&9YZ) zK`zR~ql)n@FLO#dYAVDHn6L*RdZg(FMLe~%P??R;pe1=k<$9@)LXFkqOJ5m<1yH;s z`!cu7gPow~&gZ9luHMw=1RnAdM6;|?Ggtk~JHlc5|MvVyb zF$(#!DbOIlIOKX<8~W^}uH83&+cWwewBo7sTKnkGmdsK_@Zw-k`v?6uoDqjxUk+dG z6d=RYkE(fz5pP9ISyqH*H|?MMRAV<5rp_pA3EPMkvpZcaD+zByp(E= zRYts5tUYP?>q~H_Di9I^-1{_dtJ=3unagaw%c1uVOW~P6Ft02xmm#comEpis*)?jh z6w39Dao42ei5;w?fFn zL@7hln{;k7R?L>-p;mCmZ~f75sxH)UzHjuHgS@|^T*-m Date: Fri, 14 Mar 2025 22:53:29 +0800 Subject: [PATCH 51/51] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=9B=B4=E6=96=B0=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=20SQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/mysql/ruoyi-vue-pro.sql | 87 ++++++++++++------- .../admin/chat/AiChatMessageController.java | 6 +- .../admin/mindmap/vo/AiMindMapPageReqVO.java | 2 - .../admin/model/vo/tool/AiToolPageReqVO.java | 2 - .../admin/write/vo/AiWritePageReqVO.java | 2 - .../dataobject/chat/AiChatConversationDO.java | 1 - .../dal/dataobject/chat/AiChatMessageDO.java | 1 - .../ai/dal/dataobject/model/AiApiKeyDO.java | 1 - .../ai/dal/dataobject/model/AiChatRoleDO.java | 1 - .../ai/dal/dataobject/model/AiModelDO.java | 1 - .../ai/dal/dataobject/model/AiToolDO.java | 2 - 11 files changed, 56 insertions(+), 50 deletions(-) diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql index 0c810bef85..b1a07fc61c 100644 --- a/sql/mysql/ruoyi-vue-pro.sql +++ b/sql/mysql/ruoyi-vue-pro.sql @@ -11,7 +11,7 @@ Target Server Version : 80200 (8.2.0) File Encoding : 65001 - Date: 31/12/2024 09:16:18 + Date: 14/03/2025 22:52:31 */ SET NAMES utf8mb4; @@ -91,7 +91,7 @@ CREATE TABLE `infra_api_error_log` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 21226 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志'; +) ENGINE = InnoDB AUTO_INCREMENT = 21417 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志'; -- ---------------------------- -- Records of infra_api_error_log @@ -128,7 +128,7 @@ CREATE TABLE `infra_codegen_column` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2483 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义'; +) ENGINE = InnoDB AUTO_INCREMENT = 2538 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义'; -- ---------------------------- -- Records of infra_codegen_column @@ -166,7 +166,7 @@ CREATE TABLE `infra_codegen_table` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 187 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义'; +) ENGINE = InnoDB AUTO_INCREMENT = 191 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义'; -- ---------------------------- -- Records of infra_codegen_table @@ -250,7 +250,7 @@ CREATE TABLE `infra_file` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1577 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表'; +) ENGINE = InnoDB AUTO_INCREMENT = 1655 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表'; -- ---------------------------- -- Records of infra_file @@ -444,7 +444,7 @@ CREATE TABLE `system_dict_data` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1683 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表'; +) ENGINE = InnoDB AUTO_INCREMENT = 1694 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表'; -- ---------------------------- -- Records of system_dict_data @@ -831,7 +831,7 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1551, 2, '描述模式', '2', 'ai_generate_mode', 0, '', '', '', '1', '2024-06-27 22:46:37', '1', '2024-06-28 01:22:24', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1552, 8, 'Suno', 'Suno', 'ai_platform', 0, '', '', '', '1', '2024-06-29 09:13:36', '1', '2024-06-29 09:13:41', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1553, 9, 'DeepSeek', 'DeepSeek', 'ai_platform', 0, '', '', '', '1', '2024-07-06 12:04:30', '1', '2024-07-06 12:05:20', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1554, 10, '智谱', 'ZhiPu', 'ai_platform', 0, '', '', '', '1', '2024-07-06 18:00:35', '1', '2024-07-06 18:00:35', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1554, 13, '智谱', 'ZhiPu', 'ai_platform', 0, '', '', '', '1', '2024-07-06 18:00:35', '1', '2025-02-24 20:18:41', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1555, 4, '长', '4', 'ai_write_length', 0, '', '', '', '1', '2024-07-07 15:49:03', '1', '2024-07-07 15:49:03', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1556, 5, '段落', '5', 'ai_write_format', 0, '', '', '', '1', '2024-07-07 15:49:54', '1', '2024-07-07 15:49:54', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1557, 6, '文章', '6', 'ai_write_format', 0, '', '', '', '1', '2024-07-07 15:50:05', '1', '2024-07-07 15:50:05', b'0'); @@ -888,13 +888,23 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1673, 2, 'OPC UA', '2', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:31', '1', '2024-09-06 22:26:31', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1674, 3, 'ZigBee', '3', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:39', '1', '2024-09-06 22:26:39', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1675, 4, 'BLE', '4', 'iot_protocol_type', 0, '', '', '', '1', '2024-09-06 22:26:48', '1', '2024-09-06 22:26:48', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1676, 0, '未激活', '0', 'iot_device_status', 0, '', '', '', '1', '2024-09-21 08:13:34', '1', '2024-09-21 08:13:34', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1677, 1, '在线', '1', 'iot_device_status', 0, '', '', '', '1', '2024-09-21 08:13:48', '1', '2024-09-21 08:13:48', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1678, 2, '离线', '2', 'iot_device_status', 0, '', '', '', '1', '2024-09-21 08:13:59', '1', '2024-09-21 08:13:59', b'0'); -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1679, 3, '已禁用', '3', 'iot_device_status', 0, '', '', '', '1', '2024-09-21 08:14:13', '1', '2024-09-21 08:14:13', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1676, 0, '未激活', '0', 'iot_device_state', 0, '', '', '', '1', '2024-09-21 08:13:34', '1', '2025-01-28 16:09:27', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1677, 1, '在线', '1', 'iot_device_state', 0, '', '', '', '1', '2024-09-21 08:13:48', '1', '2025-01-28 16:09:26', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1678, 2, '离线', '2', 'iot_device_state', 0, '', '', '', '1', '2024-09-21 08:13:59', '1', '2025-01-28 16:09:25', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1680, 1, '属性', '1', 'iot_product_function_type', 0, '', '', '', '1', '2024-09-29 20:03:01', '1', '2024-09-29 20:09:41', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1681, 2, '服务', '2', 'iot_product_function_type', 0, '', '', '', '1', '2024-09-29 20:03:11', '1', '2024-09-29 20:08:23', b'0'); INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1682, 3, '事件', '3', 'iot_product_function_type', 0, '', '', '', '1', '2024-09-29 20:03:20', '1', '2024-09-29 20:08:20', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1683, 10, '字节豆包', 'DouBao', 'ai_platform', 0, '', '', '', '1', '2025-02-23 19:51:40', '1', '2025-02-23 19:52:02', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1684, 11, '腾讯混元', 'HunYuan', 'ai_platform', 0, '', '', '', '1', '2025-02-23 20:58:04', '1', '2025-02-23 20:58:04', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1685, 12, '硅基流动', 'SiliconFlow', 'ai_platform', 0, '', '', '', '1', '2025-02-24 20:19:09', '1', '2025-02-24 20:19:09', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1686, 1, '聊天', '1', 'ai_model_type', 0, '', '', '', '1', '2025-03-03 12:26:34', '1', '2025-03-03 12:26:34', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1687, 2, '图像', '2', 'ai_model_type', 0, '', '', '', '1', '2025-03-03 12:27:23', '1', '2025-03-03 12:27:23', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1688, 3, '音频', '3', 'ai_model_type', 0, '', '', '', '1', '2025-03-03 12:27:51', '1', '2025-03-03 12:27:51', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1689, 4, '视频', '4', 'ai_model_type', 0, '', '', '', '1', '2025-03-03 12:28:03', '1', '2025-03-03 12:28:03', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1690, 5, '向量', '5', 'ai_model_type', 0, '', '', '', '1', '2025-03-03 12:28:15', '1', '2025-03-03 12:28:15', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1691, 6, '重排', '6', 'ai_model_type', 0, '', '', '', '1', '2025-03-03 12:28:26', '1', '2025-03-03 12:28:26', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1692, 14, 'MiniMax', 'MiniMax', 'ai_platform', 0, '', '', '', '1', '2025-03-11 20:04:51', '1', '2025-03-11 20:04:51', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1693, 15, '月之暗灭', 'Moonshot', 'ai_platform', 0, '', '', '', '1', '2025-03-11 20:05:08', '1', '2025-03-11 20:05:08', b'0'); COMMIT; -- ---------------------------- @@ -914,7 +924,7 @@ CREATE TABLE `system_dict_type` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 640 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表'; +) ENGINE = InnoDB AUTO_INCREMENT = 641 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表'; -- ---------------------------- -- Records of system_dict_type @@ -1014,13 +1024,14 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (628, 'AI 写作类型', 'ai_write_type', 0, '', '1', '2024-07-10 21:25:29', '1', '2024-07-10 21:25:29', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (629, 'BPM 流程模型类型', 'bpm_model_type', 0, '', '1', '2024-08-26 15:21:43', '1', '2024-08-26 15:21:43', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (630, 'IOT 接入网关协议', 'iot_protocol_type', 0, '', '1', '2024-09-06 22:20:17', '1', '2024-09-06 22:20:17', b'0', '1970-01-01 00:00:00'); -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (631, 'IOT 设备状态', 'iot_device_status', 0, '', '1', '2024-09-21 08:12:55', '1', '2024-09-21 08:12:55', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (631, 'IOT 设备状态', 'iot_device_state', 0, '', '1', '2024-09-21 08:12:55', '1', '2025-01-28 16:09:42', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (632, 'IOT 物模型功能类型', 'iot_product_function_type', 0, '', '1', '2024-09-29 20:02:36', '1', '2024-09-29 20:09:26', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (634, 'IOT 数据格式', 'iot_data_format', 0, '', '1', '2024-08-10 11:52:58', '1', '2024-09-06 14:30:14', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (635, 'IOT 产品设备类型', 'iot_product_device_type', 0, '', '1', '2024-08-10 11:54:30', '1', '2024-08-10 04:06:56', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (637, 'IOT 产品状态', 'iot_product_status', 0, '', '1', '2024-08-10 12:06:09', '1', '2024-08-10 12:06:09', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (638, 'IOT 数据校验级别', 'iot_validate_type', 0, '', '1', '2024-09-06 20:05:13', '1', '2024-09-06 20:05:13', b'0', '1970-01-01 00:00:00'); INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (639, 'IOT 联网方式', 'iot_net_type', 0, '', '1', '2024-09-06 22:04:13', '1', '2024-09-06 22:04:13', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (640, 'AI 模型类型', 'ai_model_type', 0, '', '1', '2025-03-03 12:24:07', '1', '2025-03-03 12:24:07', b'0', '1970-01-01 00:00:00'); COMMIT; -- ---------------------------- @@ -1044,7 +1055,7 @@ CREATE TABLE `system_login_log` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 3415 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录'; +) ENGINE = InnoDB AUTO_INCREMENT = 3442 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录'; -- ---------------------------- -- Records of system_login_log @@ -1175,7 +1186,7 @@ CREATE TABLE `system_menu` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2913 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表'; +) ENGINE = InnoDB AUTO_INCREMENT = 2925 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表'; -- ---------------------------- -- Records of system_menu @@ -1373,7 +1384,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1241, '文件配置删除', 'infra:file-config:delete', 3, 4, 1237, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '', '2022-04-20 17:03:10', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1242, '文件配置导出', 'infra:file-config:export', 3, 5, 1237, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '', '2022-04-20 17:03:10', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1243, '文件管理', '', 2, 6, 2, 'file', 'ep:files', NULL, '', 0, b'1', b'1', b'1', '1', '2022-03-16 23:47:40', '1', '2024-04-23 00:02:11', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1254, '作者动态', '', 1, 0, 0, 'https://www.iocoder.cn', 'ep:avatar', NULL, NULL, 0, b'1', b'1', b'1', '1', '2022-04-23 01:03:15', '1', '2024-09-06 09:19:42', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1254, '作者动态', '', 1, 0, 0, 'https://www.iocoder.cn', 'ep:avatar', NULL, NULL, 0, b'1', b'1', b'1', '1', '2022-04-23 01:03:15', '104', '2025-01-04 10:59:37', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1255, '数据源配置', '', 2, 1, 2, 'data-source-config', 'ep:data-analysis', 'infra/dataSourceConfig/index', 'InfraDataSourceConfig', 0, b'1', b'1', b'1', '', '2022-04-27 14:37:32', '1', '2024-02-29 08:51:25', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1256, '数据源配置查询', 'infra:data-source-config:query', 3, 1, 1255, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-04-27 14:37:32', '', '2022-04-27 14:37:32', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1257, '数据源配置创建', 'infra:data-source-config:create', 3, 2, 1255, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-04-27 14:37:32', '', '2022-04-27 14:37:32', b'0'); @@ -1978,11 +1989,11 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2763, 'API 密钥创建', 'ai:api-key:create', 3, 2, 2761, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-09 14:52:56', '1', '2024-05-13 20:36:26', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2764, 'API 密钥更新', 'ai:api-key:update', 3, 3, 2761, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-09 14:52:56', '1', '2024-05-13 20:36:42', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2765, 'API 密钥删除', 'ai:api-key:delete', 3, 4, 2761, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-09 14:52:56', '1', '2024-05-13 20:36:48', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2767, '聊天模型', '', 2, 0, 2760, 'chat-model', 'fa-solid:abacus', 'ai/model/chatModel/index.vue', 'AiChatModel', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2024-05-10 22:44:16', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2768, '聊天模型查询', 'ai:chat-model:query', 3, 1, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2024-05-13 20:37:02', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2769, '聊天模型创建', 'ai:chat-model:create', 3, 2, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2024-05-13 20:37:12', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2770, '聊天模型更新', 'ai:chat-model:update', 3, 3, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2024-05-13 20:37:18', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2771, '聊天模型删除', 'ai:chat-model:delete', 3, 4, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2024-05-13 20:37:23', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2767, '模型配置', '', 2, 0, 2760, 'model', 'fa-solid:abacus', 'ai/model/model/index.vue', 'AiModel', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2025-03-03 09:57:41', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2768, '聊天模型查询', 'ai:model:query', 3, 1, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2025-03-03 09:19:46', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2769, '聊天模型创建', 'ai:model:create', 3, 2, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2025-03-03 09:20:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2770, '聊天模型更新', 'ai:model:update', 3, 3, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2025-03-03 09:20:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2771, '聊天模型删除', 'ai:model:delete', 3, 4, 2767, '', '', '', '', 0, b'1', b'1', b'1', '', '2024-05-10 14:42:48', '1', '2025-03-03 09:20:27', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2773, '聊天角色', '', 2, 0, 2760, 'chat-role', 'fa:user-secret', 'ai/model/chatRole/index.vue', 'AiChatRole', 0, b'1', b'1', b'1', '', '2024-05-13 12:39:28', '1', '2024-05-13 20:41:45', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2774, '聊天角色查询', 'ai:chat-role:query', 3, 1, 2773, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-05-13 12:39:28', '', '2024-05-13 12:39:28', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2775, '聊天角色创建', 'ai:chat-role:create', 3, 2, 2773, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-05-13 12:39:28', '', '2024-05-13 12:39:28', b'0'); @@ -2008,7 +2019,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2795, 'AI 写作删除', 'ai:write:delete', 3, 4, 2793, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '', '2024-07-10 13:24:34', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2796, 'AI 音乐', '', 2, 4, 2758, 'music', 'fa:music', 'ai/music/index/index.vue', 'AiMusic', 0, b'1', b'1', b'1', '1', '2024-07-17 09:21:12', '1', '2024-07-29 21:11:52', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2797, '客服中心', '', 2, 100, 2362, 'kefu', 'fa-solid:user-alt', 'mall/promotion/kefu/index', 'KeFu', 0, b'1', b'1', b'1', '1', '2024-07-17 23:49:05', '1', '2024-07-17 23:49:16', b'0'); -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2798, 'AI 思维导图', '', 2, 5, 2758, 'mind-map', 'fa:sitemap', 'ai/mindmap/index/index.vue', 'AiMindMap', 0, b'1', b'1', b'1', '1', '2024-07-29 21:31:59', '1', '2024-07-29 21:33:20', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2798, 'AI 思维导图', '', 2, 6, 2758, 'mind-map', 'fa:sitemap', 'ai/mindmap/index/index.vue', 'AiMindMap', 0, b'1', b'1', b'1', '1', '2024-07-29 21:31:59', '1', '2025-03-02 18:57:31', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2799, '导图管理', '', 2, 14, 2760, 'mind-map', 'fa:map', 'ai/mindmap/manager/index', 'AiMindMapManager', 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '1', '2024-08-10 17:24:28', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2800, '思维导图查询', 'ai:mind-map:query', 3, 1, 2799, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '', '2024-08-10 09:15:09', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2801, '思维导图删除', 'ai:mind-map:delete', 3, 4, 2799, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-08-10 09:15:09', '', '2024-08-10 09:15:09', b'0'); @@ -2045,6 +2056,18 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2910, 'IoT 产品物模型删除', 'iot:think-model-function:delete', 3, 4, 2906, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-09-25 22:12:09', '', '2024-09-25 22:12:09', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2911, 'IoT 产品物模型导出', 'iot:think-model-function:export', 3, 5, 2906, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-09-25 22:12:09', '', '2024-09-25 22:12:09', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2912, '创建推广员', 'trade:brokerage-user:create', 3, 7, 2346, '', '', '', '', 0, b'1', b'1', b'1', '1', '2024-12-01 14:32:39', '1', '2024-12-01 14:32:39', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2913, '流程清理', 'bpm:model:clean', 3, 7, 1193, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-17 19:32:06', '1', '2025-01-17 19:32:06', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2914, '积分商城活动关闭', 'promotion:point-activity:close', 3, 6, 2808, '', '', '', '', 0, b'1', b'1', b'1', '1', '2025-01-23 20:23:34', '1', '2025-01-23 20:23:34', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2915, 'AI 知识库', '', 2, 5, 2758, 'knowledge', 'ep:notebook', 'ai/knowledge/knowledge/index', 'AiKnowledge', 0, b'1', b'1', b'1', '', '2025-02-28 07:04:21', '1', '2025-03-02 18:58:37', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2916, 'AI 知识库查询', 'ai:knowledge:query', 3, 1, 2915, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-02-28 07:04:21', '', '2025-02-28 07:04:21', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2917, 'AI 知识库创建', 'ai:knowledge:create', 3, 2, 2915, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-02-28 07:04:21', '', '2025-02-28 07:04:21', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2918, 'AI 知识库更新', 'ai:knowledge:update', 3, 3, 2915, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-02-28 07:04:21', '', '2025-02-28 07:04:21', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2919, 'AI 知识库删除', 'ai:knowledge:delete', 3, 4, 2915, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-02-28 07:04:21', '', '2025-02-28 07:04:21', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2920, '工具管理', '', 2, 0, 2760, 'tool', 'fa-solid:tools', 'ai/model/tool/index.vue', 'AiTool', 0, b'1', b'1', b'1', '', '2025-03-14 11:19:29', '1', '2025-03-14 19:20:18', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2921, '工具查询', 'ai:tool:query', 3, 1, 2920, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-14 11:19:29', '', '2025-03-14 11:19:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2922, '工具创建', 'ai:tool:create', 3, 2, 2920, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-14 11:19:29', '', '2025-03-14 11:19:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2923, '工具更新', 'ai:tool:update', 3, 3, 2920, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-14 11:19:29', '', '2025-03-14 11:19:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2924, '工具删除', 'ai:tool:delete', 3, 4, 2920, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2025-03-14 11:19:29', '', '2025-03-14 11:19:29', b'0'); COMMIT; -- ---------------------------- @@ -2166,7 +2189,7 @@ CREATE TABLE `system_oauth2_access_token` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_access_token`(`access_token` ASC) USING BTREE, INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 12055 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌'; +) ENGINE = InnoDB AUTO_INCREMENT = 13666 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌'; -- ---------------------------- -- Records of system_oauth2_access_token @@ -2288,7 +2311,7 @@ CREATE TABLE `system_oauth2_refresh_token` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1711 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌'; +) ENGINE = InnoDB AUTO_INCREMENT = 1732 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌'; -- ---------------------------- -- Records of system_oauth2_refresh_token @@ -2322,7 +2345,7 @@ CREATE TABLE `system_operate_log` ( `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 9064 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本'; +) ENGINE = InnoDB AUTO_INCREMENT = 9065 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本'; -- ---------------------------- -- Records of system_operate_log @@ -2783,9 +2806,7 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2139, 2, 1011, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2140, 2, 1012, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2141, 2, 1013, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2142, 2, 1014, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2143, 2, 1015, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2144, 2, 1016, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2145, 2, 1017, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2146, 2, 1018, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2147, 2, 1019, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); @@ -3305,7 +3326,7 @@ CREATE TABLE `system_sms_code` ( `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE, INDEX `idx_mobile`(`mobile` ASC) USING BTREE COMMENT '手机号' -) ENGINE = InnoDB AUTO_INCREMENT = 645 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码'; +) ENGINE = InnoDB AUTO_INCREMENT = 646 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码'; -- ---------------------------- -- Records of system_sms_code @@ -3346,7 +3367,7 @@ CREATE TABLE `system_sms_log` ( `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 1241 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志'; +) ENGINE = InnoDB AUTO_INCREMENT = 1255 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志'; -- ---------------------------- -- Records of system_sms_log @@ -3588,7 +3609,7 @@ CREATE TABLE `system_user_role` ( `deleted` bit(1) NULL DEFAULT b'0' COMMENT '是否删除', `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 47 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户和角色关联表'; +) ENGINE = InnoDB AUTO_INCREMENT = 48 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户和角色关联表'; -- ---------------------------- -- Records of system_user_role @@ -3604,12 +3625,12 @@ INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_t INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (15, 111, 110, '110', '2022-02-23 13:14:38', '110', '2022-02-23 13:14:38', b'0', 121); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (16, 113, 111, '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0', 122); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (18, 1, 2, '1', '2022-05-12 20:39:29', '1', '2022-05-12 20:39:29', b'0', 1); -INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (20, 104, 101, '1', '2022-05-28 15:43:57', '1', '2022-05-28 15:43:57', b'0', 1); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (22, 115, 2, '1', '2022-07-21 22:08:30', '1', '2022-07-21 22:08:30', b'0', 1); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (35, 112, 1, '1', '2024-03-15 20:00:24', '1', '2024-03-15 20:00:24', b'0', 1); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (36, 118, 1, '1', '2024-03-17 09:12:08', '1', '2024-03-17 09:12:08', b'0', 1); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (38, 114, 101, '1', '2024-03-24 22:23:03', '1', '2024-03-24 22:23:03', b'0', 1); INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (46, 117, 1, '1', '2024-10-02 10:16:11', '1', '2024-10-02 10:16:11', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (47, 104, 2, '1', '2025-01-04 10:40:33', '1', '2025-01-04 10:40:33', b'0', 1); COMMIT; -- ---------------------------- @@ -3644,10 +3665,10 @@ CREATE TABLE `system_users` ( -- Records of system_users -- ---------------------------- BEGIN; -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1,2]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/bf2002b38950c904243be7c825d3f82e29f25a44526583c3fde2ebdff3a87f75.png', 0, '0:0:0:0:0:0:0:1', '2024-12-28 20:29:58', 'admin', '2021-01-05 17:03:47', NULL, '2024-12-28 20:29:58', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1,2]', 'aoteman@126.com', '18818260277', 2, 'http://test.yudao.iocoder.cn/bf2002b38950c904243be7c825d3f82e29f25a44526583c3fde2ebdff3a87f75.png', 0, '0:0:0:0:0:0:0:1', '2025-03-13 12:44:59', 'admin', '2021-01-05 17:03:47', NULL, '2025-03-13 12:44:59', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$04$IgUse/ibRzAZ3rngCThmtemJeoh15Ux1TQ2hIMe4iwt/K3LcFHEda', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-11-02 14:00:46', '', '2021-01-07 09:07:17', NULL, '2024-11-02 14:00:46', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$04$fUBSmjKCPYAUmnMzOb6qE.eZCGPhHi1JmAKclODbfS/O7fHOl2bH6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '0:0:0:0:0:0:0:1', '2024-08-11 17:48:12', '', '2021-01-13 23:50:35', NULL, '2024-08-11 17:48:12', b'0', 1); -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$jDFLttgfik0QqJKAbfhMa.2A9xXoZmAIxakdFJUzkX.MgBKT6ddo6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2024-09-17 15:05:43', '', '2021-01-21 02:13:53', NULL, '2024-09-17 15:05:43', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$BrwaYn303hjA/6TnXqdGoOLhyHOAA0bVrAFu6.1dJKycqKUnIoRz2', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2025-01-04 10:40:49', '', '2021-01-21 02:13:53', NULL, '2025-01-04 10:40:49', b'0', 1); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (107, 'admin107', '$2a$10$dYOOBKMO93v/.ReCqzyFg.o67Tqk.bbc2bhrpyBGkIw9aypCtr2pm', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 22:59:33', '1', '2022-02-27 08:26:51', b'0', 118); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (108, 'admin108', '$2a$10$y6mfvKoNYL1GXWak8nYwVOH.kCWqjactkzdoIDgiKl93WN3Ejg.Lu', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 23:00:50', '1', '2022-02-27 08:26:53', b'0', 119); INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, 'admin109', '$2a$10$JAqvH0tEc0I7dfDVBI7zyuB4E3j.uH6daIjV53.vUS6PknFkDJkuK', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 23:11:50', '1', '2022-02-27 08:26:56', b'0', 120); diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java index b4fa8ab88c..bfd1e41caf 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java @@ -64,8 +64,7 @@ public class AiChatMessageController { @Operation(summary = "发送消息(流式)", description = "流式返回,响应较快") @PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public Flux> sendChatMessageStream( - @Valid @RequestBody AiChatMessageSendReqVO sendReqVO) { + public Flux> sendChatMessageStream(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) { return chatMessageService.sendChatMessageStream(sendReqVO, getLoginUserId()); } @@ -123,8 +122,7 @@ public class AiChatMessageController { @Operation(summary = "删除指定对话的消息") @DeleteMapping("/delete-by-conversation-id") @Parameter(name = "conversationId", required = true, description = "对话编号", example = "1024") - public CommonResult deleteChatMessageByConversationId( - @RequestParam("conversationId") Long conversationId) { + public CommonResult deleteChatMessageByConversationId(@RequestParam("conversationId") Long conversationId) { chatMessageService.deleteChatMessageByConversationId(conversationId, getLoginUserId()); return success(true); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java index c123ab70e2..f7769b4e6b 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java @@ -13,8 +13,6 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_ @Schema(description = "管理后台 - AI 思维导图分页 Request VO") @Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) public class AiMindMapPageReqVO extends PageParam { @Schema(description = "用户编号", example = "4325") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolPageReqVO.java index f3e01ee60c..dc8b04c507 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolPageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/tool/AiToolPageReqVO.java @@ -15,8 +15,6 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_ @Schema(description = "管理后台 - AI 工具分页 Request VO") @Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) public class AiToolPageReqVO extends PageParam { @Schema(description = "工具名称", example = "王五") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java index 047380e422..04f99ae13c 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java @@ -13,8 +13,6 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_ @Schema(description = "管理后台 - AI 写作分页 Request VO") @Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) public class AiWritePageReqVO extends PageParam { @Schema(description = "用户编号", example = "28404") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java index 358e994104..23aec276db 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java @@ -21,7 +21,6 @@ import java.time.LocalDateTime; @TableName("ai_chat_conversation") @KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java index 94f764c85e..2364d750cb 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java @@ -23,7 +23,6 @@ import java.util.List; @TableName(value = "ai_chat_message", autoResultMap = true) @KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiApiKeyDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiApiKeyDO.java index e251d55c85..346811f0d5 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiApiKeyDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiApiKeyDO.java @@ -16,7 +16,6 @@ import lombok.*; @TableName("ai_api_key") @KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java index 4a65796a96..bb6a3ca48d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java @@ -21,7 +21,6 @@ import java.util.List; @TableName(value = "ai_chat_role", autoResultMap = true) @KeySequence("ai_chat_role_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java index f4ff09212a..b39320291b 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiModelDO.java @@ -20,7 +20,6 @@ import lombok.*; @TableName("ai_model") @KeySequence("ai_model_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java index 41e5bcc06b..7773e978cc 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java @@ -16,8 +16,6 @@ import lombok.*; @TableName("ai_tool") @KeySequence("ai_tool_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data -@EqualsAndHashCode(callSuper = true) -@ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor