Commit f20c27a7eff09108dc33dbed9145b3ca6f7927ee
1 parent
9ddd2edd
【代码评审】AI:写作实现
Showing
7 changed files
with
43 additions
and
30 deletions
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
| ... | ... | @@ -33,7 +33,7 @@ public interface ErrorCodeConstants { |
| 33 | 33 | // ========== API 聊天消息 1-040-004-000 ========== |
| 34 | 34 | |
| 35 | 35 | ErrorCode CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_040_004_000, "消息不存在!"); |
| 36 | - ErrorCode CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "Stream 对话异常!"); | |
| 36 | + ErrorCode CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "对话生成异常!"); | |
| 37 | 37 | |
| 38 | 38 | // ========== API 绘画 1-040-005-000 ========== |
| 39 | 39 | |
| ... | ... | @@ -48,6 +48,6 @@ public interface ErrorCodeConstants { |
| 48 | 48 | |
| 49 | 49 | // ========== API 写作 1-022-007-000 ========== |
| 50 | 50 | ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_022_007_000, "作文不存在!"); |
| 51 | - ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "Stream 对话异常!"); | |
| 51 | + ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "写作生成异常!"); | |
| 52 | 52 | |
| 53 | 53 | } | ... | ... |
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiLanguageEnum.java
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java
| 1 | 1 | package cn.iocoder.yudao.module.ai.controller.admin.write.vo; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.framework.common.validation.InEnum; | |
| 4 | +import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum; | |
| 3 | 5 | import io.swagger.v3.oas.annotations.media.Schema; |
| 4 | 6 | import jakarta.validation.constraints.NotNull; |
| 5 | 7 | import lombok.Data; |
| ... | ... | @@ -8,6 +10,11 @@ import lombok.Data; |
| 8 | 10 | @Data |
| 9 | 11 | public class AiWriteGenerateReqVO { |
| 10 | 12 | |
| 13 | + @Schema(description = "写作类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | |
| 14 | + @InEnum(AiWriteTypeEnum.class) | |
| 15 | + private Integer type; | |
| 16 | + | |
| 17 | + // TODO @xin:如果非必填,可以不用写 requiredMode | |
| 11 | 18 | @Schema(description = "写作内容提示", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1.撰写:田忌赛马;2.回复:不批") |
| 12 | 19 | private String prompt; |
| 13 | 20 | |
| ... | ... | @@ -30,7 +37,4 @@ public class AiWriteGenerateReqVO { |
| 30 | 37 | @NotNull(message = "语言不能为空") |
| 31 | 38 | private Integer language; |
| 32 | 39 | |
| 33 | - | |
| 34 | - @Schema(description = "写作类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | |
| 35 | - private Integer type; //参见 AiWriteTypeEnum 枚举 | |
| 36 | 40 | } |
| 37 | 41 | \ 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
| 1 | 1 | package cn.iocoder.yudao.module.ai.dal.dataobject.write; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; | |
| 3 | 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 4 | 5 | import com.baomidou.mybatisplus.annotation.IdType; |
| 5 | 6 | import com.baomidou.mybatisplus.annotation.TableId; |
| ... | ... | @@ -35,6 +36,18 @@ public class AiWriteDO extends BaseDO { |
| 35 | 36 | private Integer type; |
| 36 | 37 | |
| 37 | 38 | /** |
| 39 | + * 模型 | |
| 40 | + */ | |
| 41 | + private String model; | |
| 42 | + | |
| 43 | + /** | |
| 44 | + * 平台 | |
| 45 | + * | |
| 46 | + * 枚举 {@link AiPlatformEnum} | |
| 47 | + */ | |
| 48 | + private String platform; | |
| 49 | + | |
| 50 | + /** | |
| 38 | 51 | * 生成内容提示 |
| 39 | 52 | */ |
| 40 | 53 | private String prompt; |
| ... | ... | @@ -70,16 +83,6 @@ public class AiWriteDO extends BaseDO { |
| 70 | 83 | private Integer language; |
| 71 | 84 | |
| 72 | 85 | /** |
| 73 | - * 模型 | |
| 74 | - */ | |
| 75 | - private String model; | |
| 76 | - | |
| 77 | - /** | |
| 78 | - * 平台 | |
| 79 | - */ | |
| 80 | - private String platform; | |
| 81 | - | |
| 82 | - /** | |
| 83 | 86 | * 错误信息 |
| 84 | 87 | */ |
| 85 | 88 | private String errorMessage; | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java
| ... | ... | @@ -11,7 +11,6 @@ import reactor.core.publisher.Flux; |
| 11 | 11 | */ |
| 12 | 12 | public interface AiWriteService { |
| 13 | 13 | |
| 14 | - | |
| 15 | 14 | /** |
| 16 | 15 | * 生成写作内容 |
| 17 | 16 | * |
| ... | ... | @@ -21,5 +20,4 @@ public interface AiWriteService { |
| 21 | 20 | */ |
| 22 | 21 | Flux<CommonResult<String>> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId); |
| 23 | 22 | |
| 24 | - | |
| 25 | 23 | } | ... | ... |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java
| ... | ... | @@ -46,23 +46,22 @@ public class AiWriteServiceImpl implements AiWriteService { |
| 46 | 46 | @Resource |
| 47 | 47 | private AiChatModelService chatModalService; |
| 48 | 48 | @Resource |
| 49 | - private AiWriteMapper writeMapper; | |
| 50 | - | |
| 49 | + private AiWriteMapper writeMapper; // TODO @xin:上面空一行;因为同类之间不要空行,非同类空行; | |
| 51 | 50 | |
| 52 | 51 | @Override |
| 53 | 52 | public Flux<CommonResult<String>> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId) { |
| 54 | - //TODO 芋艿 写作的模型配置放哪好 先用千问测试 | |
| 55 | 53 | // 1.1 校验模型 |
| 54 | + // TODO @xin:可以约定大于配置先,查询某个名字。例如说,写作助手!然后写作助手,上面是有个 model 的,可以使用它。 | |
| 56 | 55 | AiChatModelDO model = chatModalService.validateChatModel(14L); |
| 57 | 56 | StreamingChatModel chatClient = apiKeyService.getStreamingChatClient(model.getKeyId()); |
| 58 | 57 | AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); |
| 59 | 58 | ChatOptions chatOptions = buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens()); |
| 60 | 59 | |
| 61 | - //1.2 插入写作信息 | |
| 60 | + // 1.2 插入写作信息 | |
| 62 | 61 | AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class); |
| 63 | 62 | writeMapper.insert(writeDO.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform())); |
| 64 | 63 | |
| 65 | - //2.1 构建提示词 | |
| 64 | + // 2.1 构建提示词 | |
| 66 | 65 | Prompt prompt = new Prompt(buildWritingPrompt(generateReqVO), chatOptions); |
| 67 | 66 | Flux<ChatResponse> streamResponse = chatClient.stream(prompt); |
| 68 | 67 | // 2.2 流式返回 |
| ... | ... | @@ -81,7 +80,6 @@ public class AiWriteServiceImpl implements AiWriteService { |
| 81 | 80 | }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.WRITE_STREAM_ERROR))); |
| 82 | 81 | } |
| 83 | 82 | |
| 84 | - | |
| 85 | 83 | private String buildWritingPrompt(AiWriteGenerateReqVO generateReqVO) { |
| 86 | 84 | String template; |
| 87 | 85 | Integer writeType = generateReqVO.getType(); | ... | ... |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java
| ... | ... | @@ -9,12 +9,17 @@ import lombok.Data; |
| 9 | 9 | import lombok.Getter; |
| 10 | 10 | import lombok.extern.slf4j.Slf4j; |
| 11 | 11 | import org.springframework.ai.openai.api.ApiUtils; |
| 12 | +import org.springframework.http.HttpRequest; | |
| 13 | +import org.springframework.http.HttpStatusCode; | |
| 14 | +import org.springframework.web.reactive.function.client.ClientResponse; | |
| 12 | 15 | import org.springframework.web.reactive.function.client.WebClient; |
| 13 | 16 | import reactor.core.publisher.Mono; |
| 14 | 17 | |
| 15 | 18 | import java.util.Collection; |
| 16 | 19 | import java.util.List; |
| 17 | 20 | import java.util.Map; |
| 21 | +import java.util.function.Function; | |
| 22 | +import java.util.function.Predicate; | |
| 18 | 23 | |
| 19 | 24 | /** |
| 20 | 25 | * Midjourney API |
| ... | ... | @@ -25,6 +30,16 @@ import java.util.Map; |
| 25 | 30 | @Slf4j |
| 26 | 31 | public class MidjourneyApi { |
| 27 | 32 | |
| 33 | + private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful(); | |
| 34 | + | |
| 35 | + private final Function<Object, Function<ClientResponse, Mono<? extends Throwable>>> EXCEPTION_FUNCTION = | |
| 36 | + reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> { | |
| 37 | + HttpRequest request = response.request(); | |
| 38 | + log.error("[midjourney-api] 调用失败!请求方式:[{}],请求地址:[{}],请求参数:[{}],响应数据: [{}]", | |
| 39 | + request.getMethod(), request.getURI(), reqParam, responseBody); | |
| 40 | + sink.error(new IllegalStateException("[midjourney-api] 调用失败!")); | |
| 41 | + }); | |
| 42 | + | |
| 28 | 43 | private final WebClient webClient; |
| 29 | 44 | |
| 30 | 45 | /** |
| ... | ... | @@ -80,17 +95,11 @@ public class MidjourneyApi { |
| 80 | 95 | } |
| 81 | 96 | |
| 82 | 97 | private String post(String uri, Object body) { |
| 83 | - // 1、发送 post 请求 | |
| 84 | 98 | return webClient.post() |
| 85 | 99 | .uri(uri) |
| 86 | 100 | .body(Mono.just(JsonUtils.toJsonString(body)), String.class) |
| 87 | 101 | .retrieve() |
| 88 | - .onStatus(status -> !status.is2xxSuccessful(), | |
| 89 | - response -> response.bodyToMono(String.class) | |
| 90 | - .handle((respBody, sink) -> { | |
| 91 | - log.error("【Midjourney api】调用失败!resp: 【{}】", respBody); | |
| 92 | - sink.error(new IllegalStateException("【Midjourney api】调用失败!")); | |
| 93 | - })) | |
| 102 | + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(body)) | |
| 94 | 103 | .bodyToMono(String.class) |
| 95 | 104 | .block(); |
| 96 | 105 | } | ... | ... |