Commit 77ead4859c61cb216f9d26b651390ac96c7baa92

Authored by xiaoxin
1 parent aa4c1cb2

【增加】AI 写作:支持撰写

yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
@@ -42,4 +42,9 @@ public interface ErrorCodeConstants { @@ -42,4 +42,9 @@ public interface ErrorCodeConstants {
42 // ========== API 音乐 1-040-006-000 ========== 42 // ========== API 音乐 1-040-006-000 ==========
43 ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!"); 43 ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!");
44 44
  45 +
  46 + // ========== API 写作 1-022-007-000 ==========
  47 + ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_022_007_000, "作文不存在!");
  48 + ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "Stream 对话异常!");
  49 +
45 } 50 }
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java
@@ -7,7 +7,7 @@ import lombok.Getter; @@ -7,7 +7,7 @@ import lombok.Getter;
7 import java.util.Arrays; 7 import java.util.Arrays;
8 8
9 /** 9 /**
10 - * AI 音乐状态的枚举 10 + * AI 音乐生成模式的枚举
11 * 11 *
12 * @author xiaoxin 12 * @author xiaoxin
13 */ 13 */
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.enums.write;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
  4 +import lombok.AllArgsConstructor;
  5 +import lombok.Getter;
  6 +
  7 +import java.util.Arrays;
  8 +
  9 +/**
  10 + * AI 写作类型的枚举
  11 + *
  12 + * @author xiaoxin
  13 + */
  14 +@AllArgsConstructor
  15 +@Getter
  16 +public enum AiWriteTypeEnum implements IntArrayValuable {
  17 +
  18 + DESCRIPTION(1, "撰写"),
  19 + LYRIC(2, "回复");
  20 +
  21 + /**
  22 + * 类型
  23 + */
  24 + private final Integer type;
  25 + /**
  26 + * 类型名
  27 + */
  28 + private final String name;
  29 +
  30 + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiWriteTypeEnum::getType).toArray();
  31 +
  32 + @Override
  33 + public int[] array() {
  34 + return ARRAYS;
  35 + }
  36 +
  37 +}
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java
@@ -15,6 +15,8 @@ import org.springframework.web.bind.annotation.RequestMapping; @@ -15,6 +15,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
15 import org.springframework.web.bind.annotation.RestController; 15 import org.springframework.web.bind.annotation.RestController;
16 import reactor.core.publisher.Flux; 16 import reactor.core.publisher.Flux;
17 17
  18 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  19 +
18 @Tag(name = "管理后台 - AI 写作") 20 @Tag(name = "管理后台 - AI 写作")
19 @RestController 21 @RestController
20 @RequestMapping("/ai/write") 22 @RequestMapping("/ai/write")
@@ -27,6 +29,6 @@ public class AiWriteController { @@ -27,6 +29,6 @@ public class AiWriteController {
27 @PermitAll 29 @PermitAll
28 @Operation(summary = "写作生成(流式)", description = "流式返回,响应较快") 30 @Operation(summary = "写作生成(流式)", description = "流式返回,响应较快")
29 public Flux<CommonResult<String>> generateComposition(@RequestBody @Valid AiWriteGenerateReqVO generateReqVO) { 31 public Flux<CommonResult<String>> generateComposition(@RequestBody @Valid AiWriteGenerateReqVO generateReqVO) {
30 - return writeService.generateComposition(generateReqVO); 32 + return writeService.generateWriteContent(generateReqVO, getLoginUserId());
31 } 33 }
32 } 34 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java
@@ -8,14 +8,14 @@ import lombok.Data; @@ -8,14 +8,14 @@ import lombok.Data;
8 @Data 8 @Data
9 public class AiWriteGenerateReqVO { 9 public class AiWriteGenerateReqVO {
10 10
11 - @Schema(description = "写作内容", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "田忌赛马")  
12 - private String content; 11 + @Schema(description = "写作内容提示", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "田忌赛马")
  12 + private String contentPrompt;
13 13
14 @Schema(description = "原文", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "领导我要辞职") 14 @Schema(description = "原文", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "领导我要辞职")
15 private String originalContent; 15 private String originalContent;
16 16
17 @Schema(description = "回复内容", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "准了") 17 @Schema(description = "回复内容", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "准了")
18 - private String replyContent; 18 + private String replyContentPrompt;
19 19
20 @Schema(description = "长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "中等") 20 @Schema(description = "长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "中等")
21 @NotBlank(message = "长度不能为空") 21 @NotBlank(message = "长度不能为空")
@@ -35,5 +35,5 @@ public class AiWriteGenerateReqVO { @@ -35,5 +35,5 @@ public class AiWriteGenerateReqVO {
35 35
36 36
37 @Schema(description = "写作类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") 37 @Schema(description = "写作类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
38 - private Integer writeType; 38 + private Integer writeType; //参见 AiWriteTypeEnum 枚举
39 } 39 }
40 \ No newline at end of file 40 \ No newline at end of file
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.dal.dataobject.write;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import com.baomidou.mybatisplus.annotation.IdType;
  5 +import com.baomidou.mybatisplus.annotation.TableId;
  6 +import com.baomidou.mybatisplus.annotation.TableName;
  7 +import lombok.Data;
  8 +import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum;
  9 +
  10 +/**
  11 + * AI 写作 DO
  12 + *
  13 + * @author xiaoxin
  14 + */
  15 +@TableName(value = "ai_write", autoResultMap = true)
  16 +@Data
  17 +public class AiWriteDO extends BaseDO {
  18 +
  19 + /**
  20 + * 编号
  21 + */
  22 + @TableId(type = IdType.AUTO)
  23 + private Long id;
  24 +
  25 + /**
  26 + * 用户编号
  27 + */
  28 + private Long userId;
  29 +
  30 + /**
  31 + * 写作类型
  32 + * <p>
  33 + * 枚举 {@link AiWriteTypeEnum}
  34 + */
  35 + private Integer writeType;
  36 +
  37 + /**
  38 + * 撰写内容提示
  39 + */
  40 + private String contentPrompt;
  41 +
  42 + /**
  43 + * 生成的撰写内容
  44 + */
  45 + private String generatedContent;
  46 +
  47 + /**
  48 + * 原文
  49 + */
  50 + private String originalContent;
  51 +
  52 + /**
  53 + * 回复内容提示
  54 + */
  55 + private String replyContentPrompt;
  56 +
  57 + /**
  58 + * 生成的回复内容
  59 + */
  60 + private String generatedReplyContent;
  61 +
  62 + /**
  63 + * 长度提示词
  64 + */
  65 + private String length;
  66 +
  67 + /**
  68 + * 格式提示词
  69 + */
  70 + private String format;
  71 +
  72 + /**
  73 + * 语气提示词
  74 + */
  75 + private String tone;
  76 +
  77 + /**
  78 + * 语言提示词
  79 + */
  80 + private String language;
  81 +
  82 + /**
  83 + * 模型
  84 + */
  85 + private String model;
  86 +
  87 + /**
  88 + * 平台
  89 + */
  90 + private String platform;
  91 +
  92 + /**
  93 + * 错误信息
  94 + */
  95 + private String errorMessage;
  96 +
  97 +}
0 \ No newline at end of file 98 \ No newline at end of file
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/write/AiWriteMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.dal.mysql.write;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.module.ai.dal.dataobject.write.AiWriteDO;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +/**
  8 + * AI 音乐 Mapper
  9 + *
  10 + * @author xiaoxin
  11 + */
  12 +@Mapper
  13 +public interface AiWriteMapper extends BaseMapperX<AiWriteDO> {
  14 +}
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java
@@ -12,7 +12,14 @@ import reactor.core.publisher.Flux; @@ -12,7 +12,14 @@ import reactor.core.publisher.Flux;
12 public interface AiWriteService { 12 public interface AiWriteService {
13 13
14 14
15 - Flux<CommonResult<String>> generateComposition(AiWriteGenerateReqVO generateReqVO); 15 + /**
  16 + * 生成写作内容
  17 + *
  18 + * @param generateReqVO 作文生成请求参数
  19 + * @param userId 用户编号
  20 + * @return 生成结果
  21 + */
  22 + Flux<CommonResult<String>> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId);
16 23
17 24
18 } 25 }
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java
@@ -2,22 +2,27 @@ package cn.iocoder.yudao.module.ai.service.write; @@ -2,22 +2,27 @@ 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.tongyi.QianWenOptions;  
6 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; 5 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
7 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions; 6 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
8 -import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatOptions;  
9 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  8 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
10 import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO; 9 import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO;
  10 +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
  11 +import cn.iocoder.yudao.module.ai.dal.dataobject.write.AiWriteDO;
  12 +import cn.iocoder.yudao.module.ai.dal.mysql.write.AiWriteMapper;
11 import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; 13 import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
12 import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; 14 import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
  15 +import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
  16 +import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
13 import jakarta.annotation.Resource; 17 import jakarta.annotation.Resource;
14 import lombok.extern.slf4j.Slf4j; 18 import lombok.extern.slf4j.Slf4j;
15 -import org.springframework.ai.chat.ChatResponse;  
16 -import org.springframework.ai.chat.StreamingChatClient; 19 +import org.springframework.ai.chat.model.ChatResponse;
  20 +import org.springframework.ai.chat.model.StreamingChatModel;
17 import org.springframework.ai.chat.prompt.ChatOptions; 21 import org.springframework.ai.chat.prompt.ChatOptions;
18 import org.springframework.ai.chat.prompt.Prompt; 22 import org.springframework.ai.chat.prompt.Prompt;
19 import org.springframework.ai.ollama.api.OllamaOptions; 23 import org.springframework.ai.ollama.api.OllamaOptions;
20 import org.springframework.ai.openai.OpenAiChatOptions; 24 import org.springframework.ai.openai.OpenAiChatOptions;
  25 +import org.springframework.ai.qianfan.QianFanChatOptions;
21 import org.springframework.stereotype.Service; 26 import org.springframework.stereotype.Service;
22 import reactor.core.publisher.Flux; 27 import reactor.core.publisher.Flux;
23 28
@@ -35,16 +40,29 @@ public class AiWriteServiceImpl implements AiWriteService { @@ -35,16 +40,29 @@ public class AiWriteServiceImpl implements AiWriteService {
35 40
36 @Resource 41 @Resource
37 private AiApiKeyService apiKeyService; 42 private AiApiKeyService apiKeyService;
  43 + @Resource
  44 + private AiChatModelService chatModalService;
  45 + @Resource
  46 + private AiWriteMapper writeMapper;
38 47
39 48
40 @Override 49 @Override
41 - public Flux<CommonResult<String>> generateComposition(AiWriteGenerateReqVO generateReqVO) {  
42 - StreamingChatClient chatClient = apiKeyService.getStreamingChatClient(6L);  
43 - AiPlatformEnum platform = AiPlatformEnum.validatePlatform("QianWen");  
44 - ChatOptions chatOptions = buildChatOptions(platform, "qwen-72b-chat", 1.0, 1000); 50 + public Flux<CommonResult<String>> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId) {
  51 + //TODO 芋艿 写作的模型配置放哪好 先用千问测试
  52 + // 1.1 校验模型
  53 + AiChatModelDO model = chatModalService.validateChatModel(14L);
  54 + StreamingChatModel chatClient = apiKeyService.getStreamingChatClient(model.getKeyId());
  55 + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
  56 + ChatOptions chatOptions = buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens());
  57 +
  58 + //1.2 插入写作信息
  59 + AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class);
  60 + writeMapper.insert(writeDO.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform()));
  61 +
  62 + //2.1 构建提示词
45 Prompt prompt = new Prompt(buildWritingPrompt(generateReqVO), chatOptions); 63 Prompt prompt = new Prompt(buildWritingPrompt(generateReqVO), chatOptions);
46 Flux<ChatResponse> streamResponse = chatClient.stream(prompt); 64 Flux<ChatResponse> streamResponse = chatClient.stream(prompt);
47 - // 3.3 流式返回 65 + // 2.2 流式返回
48 StringBuffer contentBuffer = new StringBuffer(); 66 StringBuffer contentBuffer = new StringBuffer();
49 return streamResponse.map(chunk -> { 67 return streamResponse.map(chunk -> {
50 String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null; 68 String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null;
@@ -53,17 +71,17 @@ public class AiWriteServiceImpl implements AiWriteService { @@ -53,17 +71,17 @@ public class AiWriteServiceImpl implements AiWriteService {
53 // 响应结果 71 // 响应结果
54 return success(newContent); 72 return success(newContent);
55 }).doOnComplete(() -> { 73 }).doOnComplete(() -> {
56 - log.info("generateComposition complete, content: {}", contentBuffer);  
57 - }).onErrorResume(error -> {  
58 - log.error("[AI 写作] 发生异常", error);  
59 - return Flux.just(error(ErrorCodeConstants.AI_CHAT_STREAM_ERROR));  
60 - }); 74 + writeMapper.updateById(new AiWriteDO().setId(writeDO.getId()).setGeneratedContent(contentBuffer.toString()));
  75 + }).doOnError(throwable -> {
  76 + log.error("[AI Write][generateReqVO({}) 发生异常]", generateReqVO, throwable);
  77 + writeMapper.updateById(new AiWriteDO().setId(writeDO.getId()).setErrorMessage(throwable.getMessage()));
  78 + }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.WRITE_STREAM_ERROR)));
61 } 79 }
62 80
63 81
64 private String buildWritingPrompt(AiWriteGenerateReqVO generateReqVO) { 82 private String buildWritingPrompt(AiWriteGenerateReqVO generateReqVO) {
65 - String template = "请直接写一篇关于 [{}] 的文章,格式为:{},语气为:{},语言为:{},长度为:{}。请确保涵盖主要内容,不需要任何额外的解释或道歉。";  
66 - String content = generateReqVO.getContent(); 83 + String template = "请直接写一篇关于 [{}] 的文章,格式为:{},语气为:{},语言为:{},长度为:{}。请确保涵盖主要内容,不需要除了正文内容外的其他回复,如标题、额外的解释或道歉。";
  84 + String content = generateReqVO.getContentPrompt();
67 String format = generateReqVO.getFormat(); 85 String format = generateReqVO.getFormat();
68 String tone = generateReqVO.getTone(); 86 String tone = generateReqVO.getTone();
69 String language = generateReqVO.getLanguage(); 87 String language = generateReqVO.getLanguage();
@@ -81,14 +99,14 @@ public class AiWriteServiceImpl implements AiWriteService { @@ -81,14 +99,14 @@ public class AiWriteServiceImpl implements AiWriteService {
81 case OLLAMA: 99 case OLLAMA:
82 return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens); 100 return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
83 case YI_YAN: 101 case YI_YAN:
84 - // TODO @fan:增加一个 model  
85 - return new YiYanChatOptions().setTemperature(temperatureF).setMaxOutputTokens(maxTokens); 102 + // TODO 芋艿:貌似 model 只要一设置,就报错
  103 +// return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  104 + return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build();
86 case XING_HUO: 105 case XING_HUO:
87 return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF) 106 return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)
88 .setMaxTokens(maxTokens); 107 .setMaxTokens(maxTokens);
89 case QIAN_WEN: 108 case QIAN_WEN:
90 - // TODO @fan:增加 model、temperature 参数  
91 - return new QianWenOptions().setMaxTokens(maxTokens); 109 + return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build();
92 default: 110 default:
93 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); 111 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
94 } 112 }