Commit 77ead4859c61cb216f9d26b651390ac96c7baa92
1 parent
aa4c1cb2
【增加】AI 写作:支持撰写
Showing
9 changed files
with
207 additions
and
27 deletions
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
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 | } | ... | ... |