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 42 // ========== API 音乐 1-040-006-000 ==========
43 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 import java.util.Arrays;
8 8  
9 9 /**
10   - * AI 音乐状态的枚举
  10 + * AI 音乐生成模式的枚举
11 11 *
12 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 15 import org.springframework.web.bind.annotation.RestController;
16 16 import reactor.core.publisher.Flux;
17 17  
  18 +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
  19 +
18 20 @Tag(name = "管理后台 - AI 写作")
19 21 @RestController
20 22 @RequestMapping("/ai/write")
... ... @@ -27,6 +29,6 @@ public class AiWriteController {
27 29 @PermitAll
28 30 @Operation(summary = "写作生成(流式)", description = "流式返回,响应较快")
29 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 8 @Data
9 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 14 @Schema(description = "原文", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "领导我要辞职")
15 15 private String originalContent;
16 16  
17 17 @Schema(description = "回复内容", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "准了")
18   - private String replyContent;
  18 + private String replyContentPrompt;
19 19  
20 20 @Schema(description = "长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "中等")
21 21 @NotBlank(message = "长度不能为空")
... ... @@ -35,5 +35,5 @@ public class AiWriteGenerateReqVO {
35 35  
36 36  
37 37 @Schema(description = "写作类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
38   - private Integer writeType;
  38 + private Integer writeType; //参见 AiWriteTypeEnum 枚举
39 39 }
40 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 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 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 2  
3 3 import cn.hutool.core.util.StrUtil;
4 4 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
5   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenOptions;
6 5 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
7 6 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
8   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatOptions;
9 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  8 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
10 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 13 import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
12 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 17 import jakarta.annotation.Resource;
14 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 21 import org.springframework.ai.chat.prompt.ChatOptions;
18 22 import org.springframework.ai.chat.prompt.Prompt;
19 23 import org.springframework.ai.ollama.api.OllamaOptions;
20 24 import org.springframework.ai.openai.OpenAiChatOptions;
  25 +import org.springframework.ai.qianfan.QianFanChatOptions;
21 26 import org.springframework.stereotype.Service;
22 27 import reactor.core.publisher.Flux;
23 28  
... ... @@ -35,16 +40,29 @@ public class AiWriteServiceImpl implements AiWriteService {
35 40  
36 41 @Resource
37 42 private AiApiKeyService apiKeyService;
  43 + @Resource
  44 + private AiChatModelService chatModalService;
  45 + @Resource
  46 + private AiWriteMapper writeMapper;
38 47  
39 48  
40 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 63 Prompt prompt = new Prompt(buildWritingPrompt(generateReqVO), chatOptions);
46 64 Flux<ChatResponse> streamResponse = chatClient.stream(prompt);
47   - // 3.3 流式返回
  65 + // 2.2 流式返回
48 66 StringBuffer contentBuffer = new StringBuffer();
49 67 return streamResponse.map(chunk -> {
50 68 String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null;
... ... @@ -53,17 +71,17 @@ public class AiWriteServiceImpl implements AiWriteService {
53 71 // 响应结果
54 72 return success(newContent);
55 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 82 private String buildWritingPrompt(AiWriteGenerateReqVO generateReqVO) {
65   - String template = "请直接写一篇关于 [{}] 的文章,格式为:{},语气为:{},语言为:{},长度为:{}。请确保涵盖主要内容,不需要任何额外的解释或道歉。";
66   - String content = generateReqVO.getContent();
  83 + String template = "请直接写一篇关于 [{}] 的文章,格式为:{},语气为:{},语言为:{},长度为:{}。请确保涵盖主要内容,不需要除了正文内容外的其他回复,如标题、额外的解释或道歉。";
  84 + String content = generateReqVO.getContentPrompt();
67 85 String format = generateReqVO.getFormat();
68 86 String tone = generateReqVO.getTone();
69 87 String language = generateReqVO.getLanguage();
... ... @@ -81,14 +99,14 @@ public class AiWriteServiceImpl implements AiWriteService {
81 99 case OLLAMA:
82 100 return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
83 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 105 case XING_HUO:
87 106 return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)
88 107 .setMaxTokens(maxTokens);
89 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 110 default:
93 111 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
94 112 }
... ...