Commit 471968eaf23dcfce6b3f7ba36ddccedbff5d639d

Authored by YunaiV
1 parent 41071fc6

【代码优化】AI:新增 AIUtils,用于对接 spring ai 各种对象的构建

yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java
@@ -26,9 +26,10 @@ public class AiWriteController { @@ -26,9 +26,10 @@ public class AiWriteController {
26 private AiWriteService writeService; 26 private AiWriteService writeService;
27 27
28 @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) 28 @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
29 - @PermitAll  
30 @Operation(summary = "写作生成(流式)", description = "流式返回,响应较快") 29 @Operation(summary = "写作生成(流式)", description = "流式返回,响应较快")
  30 + @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
31 public Flux<CommonResult<String>> generateWriteContent(@RequestBody @Valid AiWriteGenerateReqVO generateReqVO) { 31 public Flux<CommonResult<String>> generateWriteContent(@RequestBody @Valid AiWriteGenerateReqVO generateReqVO) {
32 return writeService.generateWriteContent(generateReqVO, getLoginUserId()); 32 return writeService.generateWriteContent(generateReqVO, getLoginUserId());
33 } 33 }
  34 +
34 } 35 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
@@ -4,8 +4,7 @@ import cn.hutool.core.collection.CollUtil; @@ -4,8 +4,7 @@ import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.util.ObjUtil; 4 import cn.hutool.core.util.ObjUtil;
5 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
6 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; 6 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
7 -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;  
8 -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions; 7 +import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
9 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 8 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
10 import cn.iocoder.yudao.framework.common.pojo.PageResult; 9 import cn.iocoder.yudao.framework.common.pojo.PageResult;
11 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 10 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -19,7 +18,6 @@ import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper; @@ -19,7 +18,6 @@ import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
19 import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; 18 import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
20 import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; 19 import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
21 import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; 20 import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
22 -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;  
23 import jakarta.annotation.Resource; 21 import jakarta.annotation.Resource;
24 import lombok.extern.slf4j.Slf4j; 22 import lombok.extern.slf4j.Slf4j;
25 import org.springframework.ai.chat.messages.*; 23 import org.springframework.ai.chat.messages.*;
@@ -28,9 +26,6 @@ import org.springframework.ai.chat.model.ChatResponse; @@ -28,9 +26,6 @@ import org.springframework.ai.chat.model.ChatResponse;
28 import org.springframework.ai.chat.model.StreamingChatModel; 26 import org.springframework.ai.chat.model.StreamingChatModel;
29 import org.springframework.ai.chat.prompt.ChatOptions; 27 import org.springframework.ai.chat.prompt.ChatOptions;
30 import org.springframework.ai.chat.prompt.Prompt; 28 import org.springframework.ai.chat.prompt.Prompt;
31 -import org.springframework.ai.ollama.api.OllamaOptions;  
32 -import org.springframework.ai.openai.OpenAiChatOptions;  
33 -import org.springframework.ai.qianfan.QianFanChatOptions;  
34 import org.springframework.stereotype.Service; 29 import org.springframework.stereotype.Service;
35 import org.springframework.transaction.annotation.Transactional; 30 import org.springframework.transaction.annotation.Transactional;
36 import reactor.core.publisher.Flux; 31 import reactor.core.publisher.Flux;
@@ -148,46 +143,17 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { @@ -148,46 +143,17 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
148 } 143 }
149 // 1.2 history message 历史消息 144 // 1.2 history message 历史消息
150 List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO); 145 List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO);
151 - contextMessages.forEach(message -> {  
152 - // TODO @芋艿:看看有没优化空间  
153 - if (MessageType.USER.getValue().equals(message.getType())) {  
154 - chatMessages.add(new UserMessage(message.getContent()));  
155 - } else {  
156 - chatMessages.add(new AssistantMessage(message.getContent()));  
157 - }  
158 - }); 146 + contextMessages.forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent())));
159 // 1.3 user message 新发送消息 147 // 1.3 user message 新发送消息
160 chatMessages.add(new UserMessage(sendReqVO.getContent())); 148 chatMessages.add(new UserMessage(sendReqVO.getContent()));
161 149
162 // 2. 构建 ChatOptions 对象 150 // 2. 构建 ChatOptions 对象
163 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); 151 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
164 - ChatOptions chatOptions = buildChatOptions(platform, model.getModel(), 152 + ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(),
165 conversation.getTemperature(), conversation.getMaxTokens()); 153 conversation.getTemperature(), conversation.getMaxTokens());
166 return new Prompt(chatMessages, chatOptions); 154 return new Prompt(chatMessages, chatOptions);
167 } 155 }
168 156
169 - private static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {  
170 - Float temperatureF = temperature != null ? temperature.floatValue() : null;  
171 - //noinspection EnhancedSwitchMigration  
172 - switch (platform) {  
173 - case OPENAI:  
174 - return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();  
175 - case OLLAMA:  
176 - return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);  
177 - case YI_YAN:  
178 - // TODO 芋艿:貌似 model 只要一设置,就报错  
179 -// return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();  
180 - return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build();  
181 - case XING_HUO:  
182 - return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)  
183 - .setMaxTokens(maxTokens);  
184 - case QIAN_WEN:  
185 - return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build();  
186 - default:  
187 - throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));  
188 - }  
189 - }  
190 -  
191 /** 157 /**
192 * 从历史消息中,获得倒序的 n 组消息作为消息上下文 158 * 从历史消息中,获得倒序的 n 组消息作为消息上下文
193 * 159 *
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java
@@ -2,8 +2,7 @@ package cn.iocoder.yudao.module.ai.service.write; @@ -2,8 +2,7 @@ package cn.iocoder.yudao.module.ai.service.write;
2 2
3 import cn.hutool.core.util.StrUtil; 3 import cn.hutool.core.util.StrUtil;
4 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; 4 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
5 -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;  
6 -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions; 5 +import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
7 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 6 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
8 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 7 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
9 import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO; 8 import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO;
@@ -16,16 +15,12 @@ import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum; @@ -16,16 +15,12 @@ import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum;
16 import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; 15 import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
17 import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; 16 import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
18 import cn.iocoder.yudao.module.system.api.dict.DictDataApi; 17 import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
19 -import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;  
20 import jakarta.annotation.Resource; 18 import jakarta.annotation.Resource;
21 import lombok.extern.slf4j.Slf4j; 19 import lombok.extern.slf4j.Slf4j;
22 import org.springframework.ai.chat.model.ChatResponse; 20 import org.springframework.ai.chat.model.ChatResponse;
23 import org.springframework.ai.chat.model.StreamingChatModel; 21 import org.springframework.ai.chat.model.StreamingChatModel;
24 import org.springframework.ai.chat.prompt.ChatOptions; 22 import org.springframework.ai.chat.prompt.ChatOptions;
25 import org.springframework.ai.chat.prompt.Prompt; 23 import org.springframework.ai.chat.prompt.Prompt;
26 -import org.springframework.ai.ollama.api.OllamaOptions;  
27 -import org.springframework.ai.openai.OpenAiChatOptions;  
28 -import org.springframework.ai.qianfan.QianFanChatOptions;  
29 import org.springframework.stereotype.Service; 24 import org.springframework.stereotype.Service;
30 import reactor.core.publisher.Flux; 25 import reactor.core.publisher.Flux;
31 26
@@ -56,19 +51,21 @@ public class AiWriteServiceImpl implements AiWriteService { @@ -56,19 +51,21 @@ public class AiWriteServiceImpl implements AiWriteService {
56 51
57 @Override 52 @Override
58 public Flux<CommonResult<String>> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId) { 53 public Flux<CommonResult<String>> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId) {
59 - // 1.1 校验模型 TODO 芋艿 是不是取默认的模型也ok? 54 + // 1.1 校验模型 TODO 芋艿 是不是取默认的模型也ok?;那可以,有限拿 chatRole 的角色;如果没有,则获取默认的;
60 AiChatModelDO model = chatModalService.getRequiredDefaultChatModel(); 55 AiChatModelDO model = chatModalService.getRequiredDefaultChatModel();
61 StreamingChatModel chatClient = apiKeyService.getChatClient(model.getKeyId()); 56 StreamingChatModel chatClient = apiKeyService.getChatClient(model.getKeyId());
62 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); 57 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
63 - ChatOptions chatOptions = buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens());  
64 58
65 // 1.2 插入写作信息 59 // 1.2 插入写作信息
  60 + // TODO @xin:建议把 writeDO.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform()),写在 toBean 的 consumer 里;原因是,让这个 set 保持完整性
66 AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class); 61 AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class);
67 writeMapper.insert(writeDO.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform())); 62 writeMapper.insert(writeDO.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform()));
68 63
69 // 2.1 构建提示词 64 // 2.1 构建提示词
  65 + ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens());
70 Prompt prompt = new Prompt(buildWritingPrompt(generateReqVO), chatOptions); 66 Prompt prompt = new Prompt(buildWritingPrompt(generateReqVO), chatOptions);
71 Flux<ChatResponse> streamResponse = chatClient.stream(prompt); 67 Flux<ChatResponse> streamResponse = chatClient.stream(prompt);
  68 +
72 // 2.2 流式返回 69 // 2.2 流式返回
73 StringBuffer contentBuffer = new StringBuffer(); 70 StringBuffer contentBuffer = new StringBuffer();
74 return streamResponse.map(chunk -> { 71 return streamResponse.map(chunk -> {
@@ -92,7 +89,9 @@ public class AiWriteServiceImpl implements AiWriteService { @@ -92,7 +89,9 @@ public class AiWriteServiceImpl implements AiWriteService {
92 String tone = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_TONE, generateReqVO.getFormat()); 89 String tone = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_TONE, generateReqVO.getFormat());
93 String language = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_LANGUAGE, generateReqVO.getFormat()); 90 String language = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_LANGUAGE, generateReqVO.getFormat());
94 String length = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_LENGTH, generateReqVO.getFormat()); 91 String length = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_LENGTH, generateReqVO.getFormat());
  92 + // TODO @xin:建议改成 if return 哈;更简洁;
95 if (Objects.equals(writeType, AiWriteTypeEnum.WRITING.getType())) { 93 if (Objects.equals(writeType, AiWriteTypeEnum.WRITING.getType())) {
  94 + // TODO @xin:写成静态枚举哈
96 template = "请撰写一篇关于 [{}] 的文章。文章的内容格式为:[{}],语气为:[{}],语言为:[{}],长度为:[{}]。请确保涵盖主要内容,不需要除了正文内容外的其他回复,如标题、额外的解释或道歉。"; 95 template = "请撰写一篇关于 [{}] 的文章。文章的内容格式为:[{}],语气为:[{}],语言为:[{}],长度为:[{}]。请确保涵盖主要内容,不需要除了正文内容外的其他回复,如标题、额外的解释或道歉。";
97 return StrUtil.format(template, generateReqVO.getPrompt(), format, tone, language, length); 96 return StrUtil.format(template, generateReqVO.getPrompt(), format, tone, language, length);
98 } else if (Objects.equals(writeType, AiWriteTypeEnum.REPLY.getType())) { 97 } else if (Objects.equals(writeType, AiWriteTypeEnum.REPLY.getType())) {
@@ -103,27 +102,4 @@ public class AiWriteServiceImpl implements AiWriteService { @@ -103,27 +102,4 @@ public class AiWriteServiceImpl implements AiWriteService {
103 } 102 }
104 } 103 }
105 104
106 - // TODO 芋艿:复用  
107 - private static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {  
108 - Float temperatureF = temperature != null ? temperature.floatValue() : null;  
109 - //noinspection EnhancedSwitchMigration  
110 - switch (platform) {  
111 - case OPENAI:  
112 - return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();  
113 - case OLLAMA:  
114 - return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);  
115 - case YI_YAN:  
116 - // TODO 芋艿:貌似 model 只要一设置,就报错  
117 -// return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();  
118 - return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build();  
119 - case XING_HUO:  
120 - return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)  
121 - .setMaxTokens(maxTokens);  
122 - case QIAN_WEN:  
123 - return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build();  
124 - default:  
125 - throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));  
126 - }  
127 - }  
128 -  
129 } 105 }
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.core.util;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  5 +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
  6 +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
  7 +import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
  8 +import org.springframework.ai.chat.messages.*;
  9 +import org.springframework.ai.chat.prompt.ChatOptions;
  10 +import org.springframework.ai.ollama.api.OllamaOptions;
  11 +import org.springframework.ai.openai.OpenAiChatOptions;
  12 +import org.springframework.ai.qianfan.QianFanChatOptions;
  13 +
  14 +/**
  15 + * Spring AI 工具类
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +public class AiUtils {
  20 +
  21 + public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
  22 + Float temperatureF = temperature != null ? temperature.floatValue() : null;
  23 + //noinspection EnhancedSwitchMigration
  24 + switch (platform) {
  25 + case OPENAI:
  26 + return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  27 + case OLLAMA:
  28 + return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
  29 + case YI_YAN:
  30 + // TODO @xin:貌似 model 只要一设置,就报错;可以排查下
  31 +// return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  32 + return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  33 + case XING_HUO:
  34 + return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)
  35 + .setMaxTokens(maxTokens);
  36 + case QIAN_WEN:
  37 + return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build();
  38 + default:
  39 + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
  40 + }
  41 + }
  42 +
  43 + public static Message buildMessage(String type, String content) {
  44 + if (MessageType.USER.getValue().equals(type)) {
  45 + return new UserMessage(content);
  46 + }
  47 + if (MessageType.ASSISTANT.getValue().equals(type)) {
  48 + return new AssistantMessage(content);
  49 + }
  50 + if (MessageType.SYSTEM.getValue().equals(type)) {
  51 + return new SystemMessage(content);
  52 + }
  53 + if (MessageType.FUNCTION.getValue().equals(type)) {
  54 + return new FunctionMessage(content);
  55 + }
  56 + throw new IllegalArgumentException(StrUtil.format("未知消息类型({})", type));
  57 + }
  58 +
  59 +}
0 \ No newline at end of file 60 \ No newline at end of file