Commit aa4c1cb2680b3b77aa885ad2f531d200ac6bc0ae

Authored by xiaoxin
2 parents 7a66a435 3e164121

Merge branch 'master-jdk21-ai' into master-jdk21-ai-write

Showing 94 changed files with 5697 additions and 1644 deletions
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
... ... @@ -29,13 +29,17 @@ public interface ErrorCodeConstants {
29 29  
30 30 // ========== API 聊天消息 1-040-004-000 ==========
31 31  
32   - ErrorCode AI_CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_040_004_000, "消息不存在!");
33   - ErrorCode AI_CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "Stream 对话异常!");
  32 + ErrorCode CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_040_004_000, "消息不存在!");
  33 + ErrorCode CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "Stream 对话异常!");
34 34  
35 35 // ========== API 绘画 1-040-005-000 ==========
36 36  
37   - ErrorCode AI_IMAGE_NOT_EXISTS = new ErrorCode(1_022_005_000, "图片不存在!");
38   - ErrorCode AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_022_005_001, "Midjourney 提交失败!原因:{}");
39   - ErrorCode AI_IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_022_005_002, "Midjourney 按钮 customId 不存在! {}");
  37 + ErrorCode IMAGE_NOT_EXISTS = new ErrorCode(1_022_005_000, "图片不存在!");
  38 + ErrorCode IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_022_005_001, "Midjourney 提交失败!原因:{}");
  39 + ErrorCode IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_022_005_002, "Midjourney 按钮 customId 不存在! {}");
  40 + ErrorCode IMAGE_FAIL = new ErrorCode(1_022_005_002, "图片绘画失败! {}");
  41 +
  42 + // ========== API 音乐 1-040-006-000 ==========
  43 + ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!");
40 44  
41 45 }
... ...
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiModelEnum.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.enums.model;
3 3 import lombok.AllArgsConstructor;
4 4 import lombok.Getter;
5 5  
  6 +// TODO @芋艿:可以考虑清理掉
6 7 /**
7 8 * ai 模型
8 9 *
... ...
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java
1 1 package cn.iocoder.yudao.module.ai.enums.music;
2 2  
  3 +import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
3 4 import lombok.AllArgsConstructor;
4 5 import lombok.Getter;
5 6  
  7 +import java.util.Arrays;
  8 +
6 9 /**
7 10 * AI 音乐状态的枚举
8 11 *
... ... @@ -10,10 +13,10 @@ import lombok.Getter;
10 13 */
11 14 @AllArgsConstructor
12 15 @Getter
13   -public enum AiMusicGenerateModeEnum {
  16 +public enum AiMusicGenerateModeEnum implements IntArrayValuable {
14 17  
15   - LYRIC(1, "歌词模式"),
16   - DESCRIPTION(2, "描述模式");
  18 + DESCRIPTION(1, "描述模式"),
  19 + LYRIC(2, "歌词模式");
17 20  
18 21 /**
19 22 * 模式
... ... @@ -24,4 +27,11 @@ public enum AiMusicGenerateModeEnum {
24 27 */
25 28 private final String name;
26 29  
  30 + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiMusicGenerateModeEnum::getMode).toArray();
  31 +
  32 + @Override
  33 + public int[] array() {
  34 + return ARRAYS;
  35 + }
  36 +
27 37 }
... ...
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java
1 1 package cn.iocoder.yudao.module.ai.enums.music;
2 2  
  3 +import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
3 4 import lombok.AllArgsConstructor;
4 5 import lombok.Getter;
5 6  
  7 +import java.util.Arrays;
  8 +
6 9 /**
7 10 * AI 音乐状态的枚举
8 11 *
... ... @@ -10,10 +13,11 @@ import lombok.Getter;
10 13 */
11 14 @AllArgsConstructor
12 15 @Getter
13   -public enum AiMusicStatusEnum {
  16 +public enum AiMusicStatusEnum implements IntArrayValuable {
14 17  
15 18 IN_PROGRESS(10, "进行中"),
16   - SUCCESS(20, "已完成");
  19 + SUCCESS(20, "已完成"),
  20 + FAIL(30, "已失败");
17 21  
18 22 /**
19 23 * 状态
... ... @@ -25,4 +29,11 @@ public enum AiMusicStatusEnum {
25 29 */
26 30 private final String name;
27 31  
  32 + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiMusicStatusEnum::getStatus).toArray();
  33 +
  34 + @Override
  35 + public int[] array() {
  36 + return ARRAYS;
  37 + }
  38 +
28 39 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
... ... @@ -9,7 +9,7 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
9 9 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
10 10 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
11 11 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageRespVO;
12   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdatePublicStatusReqVO;
  12 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
13 13 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
14 14 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
15 15 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
... ... @@ -25,6 +25,8 @@ import org.springframework.security.access.prepost.PreAuthorize;
25 25 import org.springframework.validation.annotation.Validated;
26 26 import org.springframework.web.bind.annotation.*;
27 27  
  28 +import java.util.List;
  29 +
28 30 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
29 31 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
30 32  
... ... @@ -37,15 +39,16 @@ public class AiImageController {
37 39 @Resource
38 40 private AiImageService imageService;
39 41  
40   - @Operation(summary = "获取【我的】绘图分页")
41 42 @GetMapping("/my-page")
  43 + @Operation(summary = "获取【我的】绘图分页")
42 44 public CommonResult<PageResult<AiImageRespVO>> getImagePageMy(@Validated PageParam pageReqVO) {
43 45 PageResult<AiImageDO> pageResult = imageService.getImagePageMy(getLoginUserId(), pageReqVO);
44 46 return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
45 47 }
46 48  
47   - @Operation(summary = "获取【我的】绘图记录")
48 49 @GetMapping("/get-my")
  50 + @Operation(summary = "获取【我的】绘图记录")
  51 + @Parameter(name = "id", required = true, description = "绘画编号", example = "1024")
49 52 public CommonResult<AiImageRespVO> getImageMy(@RequestParam("id") Long id) {
50 53 AiImageDO image = imageService.getImage(id);
51 54 if (image == null || ObjUtil.notEqual(getLoginUserId(), image.getUserId())) {
... ... @@ -54,6 +57,15 @@ public class AiImageController {
54 57 return success(BeanUtils.toBean(image, AiImageRespVO.class));
55 58 }
56 59  
  60 + @GetMapping("/my-list-by-ids")
  61 + @Operation(summary = "获取【我的】绘图记录列表")
  62 + @Parameter(name = "ids", required = true, description = "绘画编号数组", example = "1024,2048")
  63 + public CommonResult<List<AiImageRespVO>> getImageListMyByIds(@RequestParam("ids") List<Long> ids) {
  64 + List<AiImageDO> imageList = imageService.getImageList(ids);
  65 + imageList.removeIf(item -> !ObjUtil.equal(getLoginUserId(), item.getUserId()));
  66 + return success(BeanUtils.toBean(imageList, AiImageRespVO.class));
  67 + }
  68 +
57 69 @Operation(summary = "生成图片")
58 70 @PostMapping("/draw")
59 71 public CommonResult<Long> drawImage(@Validated @RequestBody AiImageDrawReqVO drawReqVO) {
... ... @@ -102,11 +114,11 @@ public class AiImageController {
102 114 return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
103 115 }
104 116  
105   - @PutMapping("/update-public-status")
106   - @Operation(summary = "更新绘画发布状态")
  117 + @PutMapping("/update")
  118 + @Operation(summary = "更新绘画")
107 119 @PreAuthorize("@ss.hasPermission('ai:image:update')")
108   - public CommonResult<Boolean> updateImagePublicStatus(@Valid @RequestBody AiImageUpdatePublicStatusReqVO updateReqVO) {
109   - imageService.updateImagePublicStatus(updateReqVO);
  120 + public CommonResult<Boolean> updateImage(@Valid @RequestBody AiImageUpdateReqVO updateReqVO) {
  121 + imageService.updateImage(updateReqVO);
110 122 return success(true);
111 123 }
112 124  
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java
... ... @@ -20,7 +20,7 @@ public class AiImagePageReqVO extends PageParam {
20 20 @Schema(description = "用户编号", example = "28987")
21 21 private Long userId;
22 22  
23   - @Schema(description = "平台")
  23 + @Schema(description = "平台", example = "OpenAI")
24 24 private String platform;
25 25  
26 26 @Schema(description = "绘画状态", example = "1")
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdatePublicStatusReqVO.java renamed to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java
... ... @@ -4,15 +4,15 @@ import io.swagger.v3.oas.annotations.media.Schema;
4 4 import jakarta.validation.constraints.NotNull;
5 5 import lombok.Data;
6 6  
7   -@Schema(description = "管理后台 - AI 绘画修改发布状态 Request VO")
  7 +@Schema(description = "管理后台 - AI 绘画修改 Request VO")
8 8 @Data
9   -public class AiImageUpdatePublicStatusReqVO {
  9 +public class AiImageUpdateReqVO {
10 10  
11 11 @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
  12 + @NotNull(message = "编号不能为空")
12 13 private Long id;
13 14  
14   - @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
15   - @NotNull(message = "是否发布不能为空")
  15 + @Schema(description = "是否发布", example = "true")
16 16 private Boolean publicStatus;
17 17  
18 18 }
19 19 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java
... ... @@ -31,8 +31,7 @@ public class AiMidjourneyImagineReqVO {
31 31 @NotEmpty(message = "版本号不能为空")
32 32 private String version;
33 33  
34   - // TODO @fan:参考图,建议用 referImageUrl。
35   - @Schema(description = "垫图(参考图)base64数组")
36   - private List<String> base64Array;
  34 + @Schema(description = "参考图")
  35 + private String referImageUrl;
37 36  
38 37 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiImageController.http renamed to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http
1   -### 生成音乐:Suno +
  1 +### 生成音乐:Suno + 歌词模式
2 2 POST {{baseUrl}}/ai/music/generate
3 3 Content-Type: application/json
4 4 Authorization: {{token}}
5 5  
6 6 {
7 7 "platform": "Suno",
8   - "generateMode": 1,
9   - "prompt": "来一首快乐的歌曲",
10   - "modelVersion": "chirp-v3.5",
  8 + "generateMode": 2,
  9 + "prompt": "周末啦!",
  10 + "model": "chirp-v3.5",
11 11 "tags": ["Happy"],
12 12 "title": "Happy Song"
  13 +}
  14 +
  15 +### 生成音乐:Suno + 描述模式
  16 +POST {{baseUrl}}/ai/music/generate
  17 +Content-Type: application/json
  18 +Authorization: {{token}}
  19 +
  20 +{
  21 + "platform": "Suno",
  22 + "generateMode": 1,
  23 + "model": "chirp-v3.5",
  24 + "gptDescriptionPrompt": "今天是星球六,结果是个下雨天,希望心情很美丽",
  25 + "makeInstrumental": false
13 26 }
14 27 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java
1 1 package cn.iocoder.yudao.module.ai.controller.admin.music;
2 2  
  3 +import cn.hutool.core.util.ObjUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4   -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  7 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.*;
  8 +import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
5 9 import cn.iocoder.yudao.module.ai.service.music.AiMusicService;
6 10 import io.swagger.v3.oas.annotations.Operation;
  11 +import io.swagger.v3.oas.annotations.Parameter;
7 12 import io.swagger.v3.oas.annotations.tags.Tag;
8 13 import jakarta.annotation.Resource;
9 14 import jakarta.validation.Valid;
10   -import org.springframework.web.bind.annotation.PostMapping;
11   -import org.springframework.web.bind.annotation.RequestBody;
12   -import org.springframework.web.bind.annotation.RequestMapping;
13   -import org.springframework.web.bind.annotation.RestController;
  15 +import org.springframework.security.access.prepost.PreAuthorize;
  16 +import org.springframework.web.bind.annotation.*;
14 17  
15 18 import java.util.List;
16 19  
... ... @@ -25,10 +28,71 @@ public class AiMusicController {
25 28 @Resource
26 29 private AiMusicService musicService;
27 30  
  31 + @GetMapping("/my-page")
  32 + @Operation(summary = "获得【我的】音乐分页")
  33 + public CommonResult<PageResult<AiMusicRespVO>> getMusicMyPage(@Valid AiMusicPageReqVO pageReqVO) {
  34 + PageResult<AiMusicDO> pageResult = musicService.getMusicMyPage(pageReqVO, getLoginUserId());
  35 + return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class));
  36 + }
  37 +
28 38 @PostMapping("/generate")
29 39 @Operation(summary = "音乐生成")
30 40 public CommonResult<List<Long>> generateMusic(@RequestBody @Valid AiSunoGenerateReqVO reqVO) {
31 41 return success(musicService.generateMusic(getLoginUserId(), reqVO));
32 42 }
33 43  
  44 + @Operation(summary = "删除【我的】音乐记录")
  45 + @DeleteMapping("/delete-my")
  46 + @Parameter(name = "id", required = true, description = "音乐编号", example = "1024")
  47 + public CommonResult<Boolean> deleteMusicMy(@RequestParam("id") Long id) {
  48 + musicService.deleteMusicMy(id, getLoginUserId());
  49 + return success(true);
  50 + }
  51 +
  52 + @GetMapping("/get-my")
  53 + @Operation(summary = "获取【我的】音乐")
  54 + @Parameter(name = "id", required = true, description = "音乐编号", example = "1024")
  55 + public CommonResult<AiMusicRespVO> getMusicMy(@RequestParam("id") Long id) {
  56 + AiMusicDO music = musicService.getMusic(id);
  57 + if (music == null || ObjUtil.notEqual(getLoginUserId(), music.getUserId())) {
  58 + return success(null);
  59 + }
  60 + return success(BeanUtils.toBean(music, AiMusicRespVO.class));
  61 + }
  62 +
  63 + @PostMapping("/update-my")
  64 + @Operation(summary = "修改【我的】音乐 目前只支持修改标题")
  65 + @Parameter(name = "title", required = true, description = "音乐名称", example = "夜空中最亮的星")
  66 + public CommonResult<Boolean> updateMy(AiMusicUpdateReqVO updateReqVO) {
  67 + musicService.updateMyMusic(updateReqVO, getLoginUserId());
  68 + return success(true);
  69 + }
  70 +
  71 + // ================ 音乐管理 ================
  72 +
  73 + @GetMapping("/page")
  74 + @Operation(summary = "获得音乐分页")
  75 + @PreAuthorize("@ss.hasPermission('ai:music:query')")
  76 + public CommonResult<PageResult<AiMusicRespVO>> getMusicPage(@Valid AiMusicPageReqVO pageReqVO) {
  77 + PageResult<AiMusicDO> pageResult = musicService.getMusicPage(pageReqVO);
  78 + return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class));
  79 + }
  80 +
  81 + @DeleteMapping("/delete")
  82 + @Operation(summary = "删除音乐")
  83 + @Parameter(name = "id", description = "编号", required = true)
  84 + @PreAuthorize("@ss.hasPermission('ai:music:delete')")
  85 + public CommonResult<Boolean> deleteMusic(@RequestParam("id") Long id) {
  86 + musicService.deleteMusic(id);
  87 + return success(true);
  88 + }
  89 +
  90 + @PutMapping("/update")
  91 + @Operation(summary = "更新音乐")
  92 + @PreAuthorize("@ss.hasPermission('ai:music:update')")
  93 + public CommonResult<Boolean> updateMusic(@Valid @RequestBody AiMusicUpdateReqVO updateReqVO) {
  94 + musicService.updateMusic(updateReqVO);
  95 + return success(true);
  96 + }
  97 +
34 98 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import cn.iocoder.yudao.framework.common.validation.InEnum;
  5 +import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum;
  6 +import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum;
  7 +import io.swagger.v3.oas.annotations.media.Schema;
  8 +import lombok.Data;
  9 +import lombok.EqualsAndHashCode;
  10 +import lombok.ToString;
  11 +import org.springframework.format.annotation.DateTimeFormat;
  12 +
  13 +import java.time.LocalDateTime;
  14 +
  15 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  16 +
  17 +@Schema(description = "管理后台 - AI 音乐分页 Request VO")
  18 +@Data
  19 +@EqualsAndHashCode(callSuper = true)
  20 +@ToString(callSuper = true)
  21 +public class AiMusicPageReqVO extends PageParam {
  22 +
  23 + @Schema(description = "用户编号", example = "12212")
  24 + private Long userId;
  25 +
  26 + @Schema(description = "音乐名称", example = "夜空中最亮的星")
  27 + private String title;
  28 +
  29 + @Schema(description = "音乐状态", example = "20")
  30 + @InEnum(AiMusicStatusEnum.class)
  31 + private Integer status;
  32 +
  33 + @Schema(description = "生成模式", example = "1")
  34 + @InEnum(AiMusicGenerateModeEnum.class)
  35 + private Integer generateMode;
  36 +
  37 + @Schema(description = "是否发布", example = "true")
  38 + private Boolean publicStatus;
  39 +
  40 + @Schema(description = "创建时间")
  41 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  42 + private LocalDateTime[] createTime;
  43 +
  44 +}
0 45 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +import java.util.List;
  8 +
  9 +@Schema(description = "管理后台 - AI 音乐 Response VO")
  10 +@Data
  11 +public class AiMusicRespVO {
  12 +
  13 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
  14 + private Long id;
  15 +
  16 + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12212")
  17 + private Long userId;
  18 +
  19 + @Schema(description = "音乐名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "夜空中最亮的星")
  20 + private String title;
  21 +
  22 + @Schema(description = "歌词", example = "oh~卖糕的")
  23 + private String lyric;
  24 +
  25 + @Schema(description = "图片地址", example = "https://www.iocoder.cn")
  26 + private String imageUrl;
  27 +
  28 + @Schema(description = "音频地址", example = "https://www.iocoder.cn")
  29 + private String audioUrl;
  30 +
  31 + @Schema(description = "视频地址", example = "https://www.iocoder.cn")
  32 + private String videoUrl;
  33 +
  34 + @Schema(description = "音乐状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
  35 + private Integer status;
  36 +
  37 + @Schema(description = "描述词", example = "一首轻快的歌曲")
  38 + private String gptDescriptionPrompt;
  39 +
  40 + @Schema(description = "提示词", example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")
  41 + private String prompt;
  42 +
  43 + @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "Suno")
  44 + private String platform;
  45 +
  46 + @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5")
  47 + private String model;
  48 +
  49 + @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  50 + private Integer generateMode;
  51 +
  52 + @Schema(description = "音乐风格标签")
  53 + private List<String> tags;
  54 +
  55 + @Schema(description = "音乐时长", example = "[\"pop\",\"jazz\",\"punk\"]")
  56 + private Double duration;
  57 +
  58 + @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
  59 + private Boolean publicStatus;
  60 +
  61 + @Schema(description = "任务编号", example = "11369")
  62 + private String taskId;
  63 +
  64 + @Schema(description = "错误信息")
  65 + private String errorMessage;
  66 +
  67 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  68 + private LocalDateTime createTime;
  69 +
  70 +}
0 71 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - AI 音乐修改 Request VO")
  8 +@Data
  9 +public class AiMusicUpdateReqVO {
  10 +
  11 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583")
  12 + @NotNull(message = "编号不能为空")
  13 + private Long id;
  14 +
  15 + @Schema(description = "是否发布", example = "true")
  16 + private Boolean publicStatus;
  17 +
  18 + // TODO @xin:得单独一个 vo。因为万一。。。模拟请求,就可以改 publicStatus 拉
  19 + @Schema(description = "音乐名称", example = "夜空中最亮的星")
  20 + private String title;
  21 +
  22 +}
0 23 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.controller.admin.music.vo;
2 2  
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import jakarta.validation.constraints.NotBlank;
  5 +import jakarta.validation.constraints.NotEmpty;
5 6 import jakarta.validation.constraints.NotNull;
6 7 import lombok.Data;
7 8  
... ... @@ -15,24 +16,42 @@ public class AiSunoGenerateReqVO {
15 16 @NotBlank(message = "平台不能为空")
16 17 private String platform; // 参见 AiPlatformEnum 枚举
17 18  
  19 + /**
  20 + * 1. 描述模式:描述词 + 是否纯音乐 + 模型
  21 + * 2. 歌词模式:歌词 + 音乐风格 + 标题 + 模型
  22 + */
18 23 @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
19 24 @NotNull(message = "生成模式不能为空")
20 25 private Integer generateMode; // 参见 AiMusicGenerateModeEnum 枚举
21 26  
22   - @Schema(description = "用于生成音乐音频的提示", requiredMode = Schema.RequiredMode.REQUIRED,
23   - example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。")
  27 + @Schema(description = "用于生成音乐音频的歌词提示",
  28 + example = """
  29 + 1.描述模式:创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。
  30 + 2.歌词模式:
  31 + [Verse]
  32 + 阳光下奔跑 多么欢快
  33 + 假期就要来 心都飞起来
  34 + 朋友在一旁 笑声又灿烂
  35 + 无忧无虑的 每一天甜蜜
  36 + [Chorus]
  37 + 马上放假了 快来庆祝
  38 + 一起去旅行 快去冒险
  39 + 日子太短暂 别再等待
  40 + 马上放假了 梦想起飞
  41 + """)
24 42 private String prompt;
25 43  
26   - @Schema(description = "是否纯音乐", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "true")
  44 + @Schema(description = "是否纯音乐", example = "true")
27 45 private Boolean makeInstrumental;
28 46  
29   - @Schema(description = "模型版本", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "chirp-v3.5")
30   - private String modelVersion; // 参见 AiModelEnum 枚举
  47 + @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5")
  48 + @NotEmpty(message = "模型不能为空")
  49 + private String model; // 参见 AiModelEnum 枚举
31 50  
32   - @Schema(description = "音乐风格", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "[\"pop\",\"jazz\",\"punk\"]")
  51 + @Schema(description = "音乐风格", example = "[\"pop\",\"jazz\",\"punk\"]")
33 52 private List<String> tags;
34 53  
35   - @Schema(description = "音乐/歌曲名称", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "夜空中最亮的星")
  54 + @Schema(description = "音乐/歌曲名称", example = "夜空中最亮的星")
36 55 private String title;
37 56  
38 57 }
39 58 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java
... ... @@ -18,7 +18,7 @@ import java.util.List;
18 18 *
19 19 * @author xiaoxin
20 20 */
21   -@TableName("ai_music")
  21 +@TableName(value = "ai_music", autoResultMap = true)
22 22 @Data
23 23 public class AiMusicDO extends BaseDO {
24 24  
... ... @@ -30,7 +30,7 @@ public class AiMusicDO extends BaseDO {
30 30  
31 31 /**
32 32 * 用户编号
33   - *
  33 + * <p>
34 34 * 关联 AdminUserDO 的 userId 字段
35 35 */
36 36 private Long userId;
... ... @@ -67,7 +67,7 @@ public class AiMusicDO extends BaseDO {
67 67  
68 68 /**
69 69 * 生成模式
70   - *
  70 + * <p>
71 71 * 枚举 {@link AiMusicGenerateModeEnum}
72 72 */
73 73 private Integer generateMode;
... ... @@ -75,11 +75,7 @@ public class AiMusicDO extends BaseDO {
75 75 /**
76 76 * 描述词
77 77 */
78   - private String gptDescriptionPrompt;
79   - /**
80   - * 提示词
81   - */
82   - private String prompt;
  78 + private String description;
83 79  
84 80 /**
85 81 * 平台
... ... @@ -99,6 +95,16 @@ public class AiMusicDO extends BaseDO {
99 95 private List<String> tags;
100 96  
101 97 /**
  98 + * 音乐时长
  99 + */
  100 + private Double duration;
  101 +
  102 + /**
  103 + * 是否公开
  104 + */
  105 + private Boolean publicStatus;
  106 +
  107 + /**
102 108 * 任务编号
103 109 */
104 110 private String taskId;
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java
1 1 package cn.iocoder.yudao.module.ai.dal.mysql.music;
2 2  
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
3 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO;
4 7 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
5 8 import org.apache.ibatis.annotations.Mapper;
6 9  
... ... @@ -18,4 +21,24 @@ public interface AiMusicMapper extends BaseMapperX&lt;AiMusicDO&gt; {
18 21 return selectList(AiMusicDO::getStatus, status);
19 22 }
20 23  
  24 + default PageResult<AiMusicDO> selectPage(AiMusicPageReqVO reqVO) {
  25 + return selectPage(reqVO, new LambdaQueryWrapperX<AiMusicDO>()
  26 + .eqIfPresent(AiMusicDO::getUserId, reqVO.getUserId())
  27 + .eqIfPresent(AiMusicDO::getTitle, reqVO.getTitle())
  28 + .eqIfPresent(AiMusicDO::getStatus, reqVO.getStatus())
  29 + .eqIfPresent(AiMusicDO::getGenerateMode, reqVO.getGenerateMode())
  30 + .betweenIfPresent(AiMusicDO::getCreateTime, reqVO.getCreateTime())
  31 + .eqIfPresent(AiMusicDO::getPublicStatus, reqVO.getPublicStatus())
  32 + .orderByDesc(AiMusicDO::getId));
  33 + }
  34 +
  35 + default PageResult<AiMusicDO> selectPageByMy(AiMusicPageReqVO reqVO, Long userId) {
  36 + return selectPage(reqVO, new LambdaQueryWrapperX<AiMusicDO>()
  37 + // 情况一:公开
  38 + .eq(Boolean.TRUE.equals(reqVO.getPublicStatus()), AiMusicDO::getPublicStatus, reqVO.getPublicStatus())
  39 + // 情况二:私有
  40 + .eq(Boolean.FALSE.equals(reqVO.getPublicStatus()), AiMusicDO::getUserId, userId)
  41 + .orderByAsc(AiMusicDO::getId));
  42 + }
  43 +
21 44 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
... ... @@ -4,10 +4,8 @@ import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.util.ObjUtil;
5 5 import cn.hutool.core.util.StrUtil;
6 6 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
7   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenOptions;
8 7 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
9 8 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
10   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatOptions;
11 9 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
12 10 import cn.iocoder.yudao.framework.common.pojo.PageResult;
13 11 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
... ... @@ -24,15 +22,17 @@ import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
24 22 import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
25 23 import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
26 24 import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
  25 +import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
27 26 import jakarta.annotation.Resource;
28 27 import lombok.extern.slf4j.Slf4j;
29   -import org.springframework.ai.chat.ChatResponse;
30   -import org.springframework.ai.chat.StreamingChatClient;
31 28 import org.springframework.ai.chat.messages.*;
  29 +import org.springframework.ai.chat.model.ChatResponse;
  30 +import org.springframework.ai.chat.model.StreamingChatModel;
32 31 import org.springframework.ai.chat.prompt.ChatOptions;
33 32 import org.springframework.ai.chat.prompt.Prompt;
34 33 import org.springframework.ai.ollama.api.OllamaOptions;
35 34 import org.springframework.ai.openai.OpenAiChatOptions;
  35 +import org.springframework.ai.qianfan.QianFanChatOptions;
36 36 import org.springframework.stereotype.Service;
37 37 import org.springframework.transaction.annotation.Transactional;
38 38 import reactor.core.publisher.Flux;
... ... @@ -44,8 +44,8 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
44 44 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
45 45 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
46 46 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
47   -import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.AI_CHAT_MESSAGE_NOT_EXIST;
48 47 import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS;
  48 +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_NOT_EXIST;
49 49  
50 50 /**
51 51 * AI 聊天消息 Service 实现类
... ... @@ -117,7 +117,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
117 117 List<AiChatMessageDO> historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId());
118 118 // 1.2 校验模型
119 119 AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId());
120   - StreamingChatClient chatClient = apiKeyService.getStreamingChatClient(model.getKeyId());
  120 + StreamingChatModel chatClient = apiKeyService.getStreamingChatClient(model.getKeyId());
121 121 // 1.3 获取用户头像、角色头像
122 122 AiChatRoleDO role = conversation.getRoleId() != null ? chatRoleService.getChatRole(conversation.getRoleId()) : null;
123 123  
... ... @@ -150,7 +150,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
150 150 log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
151 151 chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage()));
152 152 }).onErrorResume(error -> {
153   - return Flux.just(error(ErrorCodeConstants.AI_CHAT_STREAM_ERROR));
  153 + return Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR));
154 154 });
155 155 }
156 156  
... ... @@ -164,7 +164,14 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
164 164 }
165 165 // 1.2 history message 历史消息
166 166 List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO);
167   - contextMessages.forEach(message -> chatMessages.add(new ChatMessage(message.getType().toUpperCase(), message.getContent())));
  167 + contextMessages.forEach(message -> {
  168 + // TODO @芋艿:看看有没优化空间
  169 + if (MessageType.USER.getValue().equals(message.getType())) {
  170 + chatMessages.add(new UserMessage(message.getContent()));
  171 + } else {
  172 + chatMessages.add(new AssistantMessage(message.getContent()));
  173 + }
  174 + });
168 175 // 1.3 user message 新发送消息
169 176 chatMessages.add(new UserMessage(sendReqVO.getContent()));
170 177  
... ... @@ -184,14 +191,14 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
184 191 case OLLAMA:
185 192 return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
186 193 case YI_YAN:
187   - // TODO @fan:增加一个 model
188   - return new YiYanChatOptions().setTemperature(temperatureF).setMaxOutputTokens(maxTokens);
  194 + // TODO 芋艿:貌似 model 只要一设置,就报错
  195 +// return QianFanChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  196 + return QianFanChatOptions.builder().withTemperature(temperatureF).withMaxTokens(maxTokens).build();
189 197 case XING_HUO:
190 198 return new XingHuoOptions().setChatModel(XingHuoChatModel.valueOfModel(model)).setTemperature(temperatureF)
191 199 .setMaxTokens(maxTokens);
192 200 case QIAN_WEN:
193   - // TODO @fan:增加 model、temperature 参数
194   - return new QianWenOptions().setMaxTokens(maxTokens);
  201 + return TongYiChatOptions.builder().withModel(model).withTemperature(temperature).withMaxTokens(maxTokens).build();
195 202 default:
196 203 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
197 204 }
... ... @@ -257,7 +264,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
257 264 // 1. 校验消息存在
258 265 AiChatMessageDO message = chatMessageMapper.selectById(id);
259 266 if (message == null || ObjUtil.notEqual(message.getUserId(), userId)) {
260   - throw exception(AI_CHAT_MESSAGE_NOT_EXIST);
  267 + throw exception(CHAT_MESSAGE_NOT_EXIST);
261 268 }
262 269 // 2. 执行删除
263 270 chatMessageMapper.deleteById(id);
... ... @@ -268,7 +275,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
268 275 // 1. 校验消息存在
269 276 List<AiChatMessageDO> messages = chatMessageMapper.selectListByConversationId(conversationId);
270 277 if (CollUtil.isEmpty(messages) || ObjUtil.notEqual(messages.get(0).getUserId(), userId)) {
271   - throw exception(AI_CHAT_MESSAGE_NOT_EXIST);
  278 + throw exception(CHAT_MESSAGE_NOT_EXIST);
272 279 }
273 280 // 2. 执行删除
274 281 chatMessageMapper.deleteBatchIds(convertList(messages, AiChatMessageDO::getId));
... ... @@ -279,7 +286,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
279 286 // 1. 校验消息存在
280 287 AiChatMessageDO message = chatMessageMapper.selectById(id);
281 288 if (message == null) {
282   - throw exception(AI_CHAT_MESSAGE_NOT_EXIST);
  289 + throw exception(CHAT_MESSAGE_NOT_EXIST);
283 290 }
284 291 // 2. 执行删除
285 292 chatMessageMapper.deleteById(id);
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java
... ... @@ -5,12 +5,14 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
5 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6 6 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
7 7 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
8   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdatePublicStatusReqVO;
  8 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
9 9 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
10 10 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
11 11 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
12 12 import jakarta.validation.Valid;
13 13  
  14 +import java.util.List;
  15 +
14 16 /**
15 17 * AI 绘图 Service 接口
16 18 *
... ... @@ -36,6 +38,14 @@ public interface AiImageService {
36 38 AiImageDO getImage(Long id);
37 39  
38 40 /**
  41 + * 获得绘图列表
  42 + *
  43 + * @param ids 绘图编号数组
  44 + * @return 绘图记录列表
  45 + */
  46 + List<AiImageDO> getImageList(List<Long> ids);
  47 +
  48 + /**
39 49 * 绘制图片
40 50 *
41 51 * @param userId 用户编号
... ... @@ -61,11 +71,11 @@ public interface AiImageService {
61 71 PageResult<AiImageDO> getImagePage(AiImagePageReqVO pageReqVO);
62 72  
63 73 /**
64   - * 更新绘画发布状态
  74 + * 更新绘画
65 75 *
66 76 * @param updateReqVO 更新信息
67 77 */
68   - void updateImagePublicStatus(@Valid AiImageUpdatePublicStatusReqVO updateReqVO);
  78 + void updateImage(@Valid AiImageUpdateReqVO updateReqVO);
69 79  
70 80 /**
71 81 * 删除绘画
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
... ... @@ -15,7 +15,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
15 15 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
16 16 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
17 17 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
18   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdatePublicStatusReqVO;
  18 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
19 19 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
20 20 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
21 21 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
... ... @@ -25,7 +25,7 @@ import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
25 25 import cn.iocoder.yudao.module.infra.api.file.FileApi;
26 26 import jakarta.annotation.Resource;
27 27 import lombok.extern.slf4j.Slf4j;
28   -import org.springframework.ai.image.ImageClient;
  28 +import org.springframework.ai.image.ImageModel;
29 29 import org.springframework.ai.image.ImageOptions;
30 30 import org.springframework.ai.image.ImagePrompt;
31 31 import org.springframework.ai.image.ImageResponse;
... ... @@ -35,6 +35,8 @@ import org.springframework.scheduling.annotation.Async;
35 35 import org.springframework.stereotype.Service;
36 36 import org.springframework.transaction.annotation.Transactional;
37 37  
  38 +import java.util.ArrayList;
  39 +import java.util.Collections;
38 40 import java.util.List;
39 41 import java.util.Map;
40 42  
... ... @@ -61,9 +63,6 @@ public class AiImageServiceImpl implements AiImageService {
61 63 @Resource
62 64 private AiApiKeyService apiKeyService;
63 65  
64   - @Resource
65   - private MidjourneyApi midjourneyApi;
66   -
67 66 @Override
68 67 public PageResult<AiImageDO> getImagePageMy(Long userId, PageParam pageReqVO) {
69 68 return imageMapper.selectPage(userId, pageReqVO);
... ... @@ -75,6 +74,14 @@ public class AiImageServiceImpl implements AiImageService {
75 74 }
76 75  
77 76 @Override
  77 + public List<AiImageDO> getImageList(List<Long> ids) {
  78 + if (CollUtil.isEmpty(ids)) {
  79 + return Collections.emptyList();
  80 + }
  81 + return imageMapper.selectBatchIds(ids);
  82 + }
  83 +
  84 + @Override
78 85 public Long drawImage(Long userId, AiImageDrawReqVO drawReqVO) {
79 86 // 1. 保存数据库
80 87 AiImageDO image = BeanUtils.toBean(drawReqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false)
... ... @@ -91,7 +98,7 @@ public class AiImageServiceImpl implements AiImageService {
91 98 // 1.1 构建请求
92 99 ImageOptions request = buildImageOptions(req);
93 100 // 1.2 执行请求
94   - ImageClient imageClient = apiKeyService.getImageClient(AiPlatformEnum.validatePlatform(req.getPlatform()));
  101 + ImageModel imageClient = apiKeyService.getImageClient(AiPlatformEnum.validatePlatform(req.getPlatform()));
95 102 ImageResponse response = imageClient.call(new ImagePrompt(req.getPrompt(), request));
96 103  
97 104 // 2. 上传到文件服务
... ... @@ -117,9 +124,16 @@ public class AiImageServiceImpl implements AiImageService {
117 124 .withResponseFormat("b64_json")
118 125 .build();
119 126 } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.STABLE_DIFFUSION.getPlatform())) {
  127 + // https://platform.stability.ai/docs/api-reference#tag/SDXL-and-SD1.6/operation/textToImage
120 128 // https://platform.stability.ai/docs/api-reference#tag/Text-to-Image/operation/textToImage
121 129 return StabilityAiImageOptions.builder().withModel(draw.getModel())
122   - .withHeight(draw.getHeight()).withWidth(draw.getWidth()) // TODO @芋艿:各种参数
  130 + .withHeight(draw.getHeight()).withWidth(draw.getWidth())
  131 + .withSeed(Long.valueOf(draw.getOptions().get("seed")))
  132 + .withCfgScale(Float.valueOf(draw.getOptions().get("scale")))
  133 + .withSteps(Integer.valueOf(draw.getOptions().get("steps")))
  134 + .withSampler(String.valueOf(draw.getOptions().get("sampler")))
  135 + .withStylePreset(String.valueOf(draw.getOptions().get("stylePreset")))
  136 + .withClipGuidancePreset(String.valueOf(draw.getOptions().get("clipGuidancePreset")))
123 137 .build();
124 138 }
125 139 throw new IllegalArgumentException("不支持的 AI 平台:" + draw.getPlatform());
... ... @@ -130,7 +144,7 @@ public class AiImageServiceImpl implements AiImageService {
130 144 // 1. 校验是否存在
131 145 AiImageDO image = validateImageExists(id);
132 146 if (ObjUtil.notEqual(image.getUserId(), userId)) {
133   - throw exception(AI_IMAGE_NOT_EXISTS);
  147 + throw exception(IMAGE_NOT_EXISTS);
134 148 }
135 149 // 2. 删除记录
136 150 imageMapper.deleteById(id);
... ... @@ -142,7 +156,7 @@ public class AiImageServiceImpl implements AiImageService {
142 156 }
143 157  
144 158 @Override
145   - public void updateImagePublicStatus(AiImageUpdatePublicStatusReqVO updateReqVO) {
  159 + public void updateImage(AiImageUpdateReqVO updateReqVO) {
146 160 // 1. 校验存在
147 161 validateImageExists(updateReqVO.getId());
148 162 // 2. 更新发布状态
... ... @@ -160,7 +174,7 @@ public class AiImageServiceImpl implements AiImageService {
160 174 private AiImageDO validateImageExists(Long id) {
161 175 AiImageDO image = imageMapper.selectById(id);
162 176 if (image == null) {
163   - throw exception(AI_IMAGE_NOT_EXISTS);
  177 + throw exception(IMAGE_NOT_EXISTS);
164 178 }
165 179 return image;
166 180 }
... ... @@ -170,6 +184,7 @@ public class AiImageServiceImpl implements AiImageService {
170 184 @Override
171 185 @Transactional(rollbackFor = Exception.class)
172 186 public Long midjourneyImagine(Long userId, AiMidjourneyImagineReqVO reqVO) {
  187 + MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi();
173 188 // 1. 保存数据库
174 189 AiImageDO image = BeanUtils.toBean(reqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false)
175 190 .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus())
... ... @@ -177,16 +192,21 @@ public class AiImageServiceImpl implements AiImageService {
177 192 imageMapper.insert(image);
178 193  
179 194 // 2. 调用 Midjourney Proxy 提交任务
  195 + List<String> base64Array = new ArrayList<>(8);
  196 + if (StrUtil.isNotBlank(reqVO.getReferImageUrl())) {
  197 + base64Array.add("data:image/jpeg;base64,".concat(Base64.encode(HttpUtil.downloadBytes(reqVO.getReferImageUrl()))));
  198 + }
180 199 MidjourneyApi.ImagineRequest imagineRequest = new MidjourneyApi.ImagineRequest(
181   - null, reqVO.getPrompt(),null,
182   - MidjourneyApi.ImagineRequest.buildState(reqVO.getWidth(), reqVO.getHeight(), reqVO.getVersion(), reqVO.getModel()));
  200 + base64Array, reqVO.getPrompt(),null,
  201 + MidjourneyApi.ImagineRequest.buildState(reqVO.getWidth(),
  202 + reqVO.getHeight(), reqVO.getVersion(), reqVO.getModel()));
183 203 MidjourneyApi.SubmitResponse imagineResponse = midjourneyApi.imagine(imagineRequest);
184 204  
185 205 // 3. 情况一【失败】:抛出业务异常
186 206 if (!MidjourneyApi.SubmitCodeEnum.SUCCESS_CODES.contains(imagineResponse.code())) {
187 207 String description = imagineResponse.description().contains("quota_not_enough") ?
188 208 "账户余额不足" : imagineResponse.description();
189   - throw exception(AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL, description);
  209 + throw exception(IMAGE_MIDJOURNEY_SUBMIT_FAIL, description);
190 210 }
191 211  
192 212 // 4. 情况二【成功】:更新 taskId 和参数
... ... @@ -197,6 +217,7 @@ public class AiImageServiceImpl implements AiImageService {
197 217  
198 218 @Override
199 219 public Integer midjourneySync() {
  220 + MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi();
200 221 // 1.1 获取 Midjourney 平台,状态在 “进行中” 的 image
201 222 List<AiImageDO> imageList = imageMapper.selectListByStatusAndPlatform(
202 223 AiImageStatusEnum.IN_PROGRESS.getStatus(), AiPlatformEnum.MIDJOURNEY.getPlatform());
... ... @@ -263,16 +284,17 @@ public class AiImageServiceImpl implements AiImageService {
263 284  
264 285 @Override
265 286 public Long midjourneyAction(Long userId, AiMidjourneyActionReqVO reqVO) {
  287 + MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi();
266 288 // 1.1 检查 image
267 289 AiImageDO image = validateImageExists(reqVO.getId());
268 290 if (ObjUtil.notEqual(userId, image.getUserId())) {
269   - throw exception(AI_IMAGE_NOT_EXISTS);
  291 + throw exception(IMAGE_NOT_EXISTS);
270 292 }
271 293 // 1.2 检查 customId
272 294 MidjourneyApi.Button button = CollUtil.findOne(image.getButtons(),
273 295 buttonX -> buttonX.customId().equals(reqVO.getCustomId()));
274 296 if (button == null) {
275   - throw exception(AI_IMAGE_CUSTOM_ID_NOT_EXISTS);
  297 + throw exception(IMAGE_CUSTOM_ID_NOT_EXISTS);
276 298 }
277 299  
278 300 // 2. 调用 Midjourney Proxy 提交任务
... ... @@ -281,7 +303,7 @@ public class AiImageServiceImpl implements AiImageService {
281 303 if (!MidjourneyApi.SubmitCodeEnum.SUCCESS_CODES.contains(actionResponse.code())) {
282 304 String description = actionResponse.description().contains("quota_not_enough") ?
283 305 "账户余额不足" : actionResponse.description();
284   - throw exception(AI_IMAGE_MIDJOURNEY_SUBMIT_FAIL, description);
  306 + throw exception(IMAGE_MIDJOURNEY_SUBMIT_FAIL, description);
285 307 }
286 308  
287 309 // 3. 新增 image 记录
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/MidjourneyImageOptions.java deleted
1   -package cn.iocoder.yudao.module.ai.service.image;
2   -
3   -import lombok.Data;
4   -import org.springframework.ai.image.ImageOptions;
5   -
6   -/**
7   - * @author fansili
8   - * @time 2024/6/5 10:34
9   - * @since 1.0
10   - */
11   -@Data
12   -public class MidjourneyImageOptions implements ImageOptions {
13   - /**
14   - * 模型
15   - */
16   - private String model;
17   - /**
18   - * 宽度
19   - */
20   - private Integer width;
21   - /**
22   - * 高度
23   - */
24   - private Integer height;
25   - /**
26   - * 版本
27   - */
28   - private String version;
29   - /**
30   - * 参数
31   - */
32   - private String state;
33   -
34   - @Override
35   - public Integer getN() {
36   - return 0;
37   - }
38   -
39   - @Override
40   - public String getModel() {
41   - return model;
42   - }
43   -
44   - @Override
45   - public Integer getWidth() {
46   - return width;
47   - }
48   -
49   - @Override
50   - public Integer getHeight() {
51   - return height;
52   - }
53   -
54   - @Override
55   - public String getResponseFormat() {
56   - return "";
57   - }
58   -}
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java
1 1 package cn.iocoder.yudao.module.ai.service.model;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
  4 +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
  5 +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
4 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 7 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO;
6 8 import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO;
7 9 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
8 10 import jakarta.validation.Valid;
9   -import org.springframework.ai.chat.StreamingChatClient;
10   -import org.springframework.ai.image.ImageClient;
  11 +import org.springframework.ai.chat.model.StreamingChatModel;
  12 +import org.springframework.ai.image.ImageModel;
11 13  
12 14 import java.util.List;
13 15  
... ... @@ -79,7 +81,7 @@ public interface AiApiKeyService {
79 81 * @param id 编号
80 82 * @return StreamingChatClient 对象
81 83 */
82   - StreamingChatClient getStreamingChatClient(Long id);
  84 + StreamingChatModel getStreamingChatClient(Long id);
83 85  
84 86 /**
85 87 * 获得 ImageClient 对象
... ... @@ -89,6 +91,24 @@ public interface AiApiKeyService {
89 91 * @param platform 平台
90 92 * @return ImageClient 对象
91 93 */
92   - ImageClient getImageClient(AiPlatformEnum platform);
  94 + ImageModel getImageClient(AiPlatformEnum platform);
  95 +
  96 + /**
  97 + * 获得 MidjourneyApi 对象
  98 + *
  99 + * TODO 可优化点:目前默认获取 Midjourney 对应的第一个开启的配置用于绘画;后续可以支持配置选择
  100 + *
  101 + * @return MidjourneyApi 对象
  102 + */
  103 + MidjourneyApi getMidjourneyApi();
  104 +
  105 + /**
  106 + * 获得 SunoApi 对象
  107 + *
  108 + * TODO 可优化点:目前默认获取 Suno 对应的第一个开启的配置用于音乐;后续可以支持配置选择
  109 + *
  110 + * @return SunoApi 对象
  111 + */
  112 + SunoApi getSunoApi();
93 113  
94 114 }
95 115 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java
... ... @@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.ai.service.model;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
4 4 import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory;
  5 +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
  6 +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
5 7 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
6 8 import cn.iocoder.yudao.framework.common.pojo.PageResult;
7 9 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
... ... @@ -10,8 +12,8 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveR
10 12 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO;
11 13 import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper;
12 14 import jakarta.annotation.Resource;
13   -import org.springframework.ai.chat.StreamingChatClient;
14   -import org.springframework.ai.image.ImageClient;
  15 +import org.springframework.ai.chat.model.StreamingChatModel;
  16 +import org.springframework.ai.image.ImageModel;
15 17 import org.springframework.stereotype.Service;
16 18 import org.springframework.validation.annotation.Validated;
17 19  
... ... @@ -96,14 +98,14 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
96 98 // ========== 与 spring-ai 集成 ==========
97 99  
98 100 @Override
99   - public StreamingChatClient getStreamingChatClient(Long id) {
  101 + public StreamingChatModel getStreamingChatClient(Long id) {
100 102 AiApiKeyDO apiKey = validateApiKey(id);
101 103 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
102 104 return clientFactory.getOrCreateStreamingChatClient(platform, apiKey.getApiKey(), apiKey.getUrl());
103 105 }
104 106  
105 107 @Override
106   - public ImageClient getImageClient(AiPlatformEnum platform) {
  108 + public ImageModel getImageClient(AiPlatformEnum platform) {
107 109 AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform.getName(), CommonStatusEnum.ENABLE.getStatus());
108 110 if (apiKey == null) {
109 111 return null;
... ... @@ -111,4 +113,25 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
111 113 return clientFactory.getOrCreateImageClient(platform, apiKey.getApiKey(), apiKey.getUrl());
112 114 }
113 115  
  116 + @Override
  117 + public MidjourneyApi getMidjourneyApi() {
  118 + AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(
  119 + AiPlatformEnum.MIDJOURNEY.getPlatform(), CommonStatusEnum.ENABLE.getStatus());
  120 + // todo @芋艿 这些地方直接抛异常会好点,不然调用到的地方都需要做判断
  121 + if (apiKey == null) {
  122 + return null;
  123 + }
  124 + return clientFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl());
  125 + }
  126 +
  127 + @Override
  128 + public SunoApi getSunoApi() {
  129 + AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(
  130 + AiPlatformEnum.SUNO.getPlatform(), CommonStatusEnum.ENABLE.getStatus());
  131 + if (apiKey == null) {
  132 + return null;
  133 + }
  134 + return clientFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl());
  135 + }
  136 +
114 137 }
115 138 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java
1 1 package cn.iocoder.yudao.module.ai.service.music;
2 2  
3   -import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.*;
  5 +import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
  6 +import jakarta.validation.Valid;
4 7  
5 8 import java.util.List;
6 9  
... ... @@ -15,7 +18,7 @@ public interface AiMusicService {
15 18 * 音乐生成
16 19 *
17 20 * @param userId 用户编号
18   - * @param reqVO 请求参数
  21 + * @param reqVO 请求参数
19 22 * @return 生成的音乐ID
20 23 */
21 24 List<Long> generateMusic(Long userId, AiSunoGenerateReqVO reqVO);
... ... @@ -27,4 +30,58 @@ public interface AiMusicService {
27 30 */
28 31 Integer syncMusic();
29 32  
  33 + /**
  34 + * 更新音乐发布状态
  35 + *
  36 + * @param updateReqVO 更新信息
  37 + */
  38 + void updateMusic(@Valid AiMusicUpdateReqVO updateReqVO);
  39 +
  40 + /**
  41 + * 更新我的音乐
  42 + *
  43 + * @param updateReqVO 更新信息
  44 + */
  45 + void updateMyMusic(@Valid AiMusicUpdateReqVO updateReqVO, Long userId);
  46 +
  47 + /**
  48 + * 删除AI 音乐
  49 + *
  50 + * @param id 编号
  51 + */
  52 + void deleteMusic(Long id);
  53 +
  54 + /**
  55 + * 删除【我的】音乐记录
  56 + *
  57 + * @param id 音乐编号
  58 + * @param userId 用户编号
  59 + */
  60 + void deleteMusicMy(Long id, Long userId);
  61 +
  62 + /**
  63 + * 获得AI 音乐
  64 + *
  65 + * @param id 音乐编号
  66 + * @return 音乐内容
  67 + */
  68 + AiMusicDO getMusic(Long id);
  69 +
  70 + /**
  71 + * 获得音乐分页
  72 + *
  73 + * @param pageReqVO 分页查询
  74 + * @return 音乐分页
  75 + */
  76 + PageResult<AiMusicDO> getMusicPage(AiMusicPageReqVO pageReqVO);
  77 +
  78 + /**
  79 + * 获得【我的】音乐分页
  80 + *
  81 + * @param pageReqVO 分页查询
  82 + * @param userId 用户编号
  83 + * @return 音乐分页
  84 + */
  85 + PageResult<AiMusicDO> getMusicMyPage(AiMusicPageReqVO pageReqVO, Long userId);
  86 +
30 87 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java
... ... @@ -2,21 +2,32 @@ package cn.iocoder.yudao.module.ai.service.music;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.text.StrPool;
  5 +import cn.hutool.core.util.ObjUtil;
  6 +import cn.hutool.core.util.ObjectUtil;
5 7 import cn.hutool.core.util.StrUtil;
  8 +import cn.hutool.http.HttpUtil;
6 9 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
  10 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  11 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO;
  12 +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdateReqVO;
7 13 import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO;
8 14 import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO;
9 15 import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper;
10 16 import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum;
11 17 import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum;
  18 +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
  19 +import cn.iocoder.yudao.module.infra.api.file.FileApi;
12 20 import jakarta.annotation.Resource;
13 21 import lombok.extern.slf4j.Slf4j;
14 22 import org.springframework.stereotype.Service;
15 23  
16 24 import java.util.*;
17 25  
  26 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
18 27 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
19 28 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
  29 +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.IMAGE_NOT_EXISTS;
  30 +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MUSIC_NOT_EXISTS;
20 31  
21 32 /**
22 33 * AI 音乐 Service 实现类
... ... @@ -28,25 +39,30 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
28 39 public class AiMusicServiceImpl implements AiMusicService {
29 40  
30 41 @Resource
31   - private SunoApi sunoApi;
  42 + private AiApiKeyService apiKeyService;
32 43  
33 44 @Resource
34 45 private AiMusicMapper musicMapper;
35 46  
  47 + @Resource
  48 + private FileApi fileApi;
  49 +
36 50 @Override
37 51 public List<Long> generateMusic(Long userId, AiSunoGenerateReqVO reqVO) {
38 52 // 1. 调用 Suno 生成音乐
  53 + SunoApi sunoApi = apiKeyService.getSunoApi();
  54 + // TODO 芋艿:这两个貌似一直没跑成功,你那可以么?用的请求是 AiMusicController.http 的 --xin:大部分ok的,补充了error_message
39 55 List<SunoApi.MusicData> musicDataList;
40   - if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) {
41   - // 1.1 歌词模式
42   - SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest(
43   - reqVO.getPrompt(), reqVO.getModelVersion(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle());
44   - musicDataList = sunoApi.customGenerate(generateRequest);
45   - } else if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) {
46   - // 1.2 描述模式
  56 + if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) {
  57 + // 1.1 描述模式
47 58 SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest(
48   - reqVO.getPrompt(), reqVO.getModelVersion(), reqVO.getMakeInstrumental());
  59 + reqVO.getPrompt(), reqVO.getModel(), reqVO.getMakeInstrumental());
49 60 musicDataList = sunoApi.generate(generateRequest);
  61 + } else if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) {
  62 + // 1.2 歌词模式
  63 + SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest(
  64 + reqVO.getPrompt(), reqVO.getModel(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle());
  65 + musicDataList = sunoApi.customGenerate(generateRequest);
50 66 } else {
51 67 throw new IllegalArgumentException(StrUtil.format("未知生成模式({})", reqVO));
52 68 }
... ... @@ -56,7 +72,7 @@ public class AiMusicServiceImpl implements AiMusicService {
56 72 return Collections.emptyList();
57 73 }
58 74 List<AiMusicDO> musicList = buildMusicDOList(musicDataList);
59   - musicList.forEach(music -> music.setUserId(userId).setPlatform(music.getPlatform()).setGenerateMode(reqVO.getGenerateMode()));
  75 + musicList.forEach(music -> music.setUserId(userId).setPlatform(reqVO.getPlatform()).setGenerateMode(reqVO.getGenerateMode()));
60 76 musicMapper.insertBatch(musicList);
61 77 return convertList(musicList, AiMusicDO::getId);
62 78 }
... ... @@ -70,6 +86,7 @@ public class AiMusicServiceImpl implements AiMusicService {
70 86 log.info("[syncMusic][Suno 开始同步, 共 ({}) 个任务]", streamingTask.size());
71 87  
72 88 // GET 请求,为避免参数过长,分批次处理
  89 + SunoApi sunoApi = apiKeyService.getSunoApi();
73 90 CollUtil.split(streamingTask, 36).forEach(chunkList -> {
74 91 Map<String, Long> taskIdMap = convertMap(chunkList, AiMusicDO::getTaskId, AiMusicDO::getId);
75 92 List<SunoApi.MusicData> musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet()));
... ... @@ -85,19 +102,120 @@ public class AiMusicServiceImpl implements AiMusicService {
85 102 return streamingTask.size();
86 103 }
87 104  
  105 + @Override
  106 + public void updateMusic(AiMusicUpdateReqVO updateReqVO) {
  107 + // 校验存在
  108 + validateMusicExists(updateReqVO.getId());
  109 + // 更新
  110 + musicMapper.updateById(new AiMusicDO().setId(updateReqVO.getId()).setPublicStatus(updateReqVO.getPublicStatus()));
  111 + }
  112 +
  113 + @Override
  114 + public void updateMyMusic(AiMusicUpdateReqVO updateReqVO, Long userId) {
  115 + // 校验音乐是否存在
  116 + AiMusicDO musicDO = validateMusicExists(updateReqVO.getId());
  117 + if (ObjUtil.notEqual(musicDO.getUserId(), userId)) {
  118 + throw exception(MUSIC_NOT_EXISTS);
  119 + }
  120 + // 更新
  121 + musicMapper.updateById(new AiMusicDO().setId(updateReqVO.getId()).setTitle(updateReqVO.getTitle()));
  122 + }
  123 +
  124 + @Override
  125 + public void deleteMusic(Long id) {
  126 + // 校验存在
  127 + validateMusicExists(id);
  128 + // 删除
  129 + musicMapper.deleteById(id);
  130 + }
  131 +
  132 + @Override
  133 + public void deleteMusicMy(Long id, Long userId) {
  134 + // 1. 校验是否存在
  135 + AiMusicDO music = validateMusicExists(id);
  136 + if (ObjUtil.notEqual(music.getUserId(), userId)) {
  137 + throw exception(IMAGE_NOT_EXISTS);
  138 + }
  139 + // 2. 删除记录
  140 + musicMapper.deleteById(id);
  141 + }
  142 +
  143 + @Override
  144 + public AiMusicDO getMusic(Long id) {
  145 + return musicMapper.selectById(id);
  146 + }
  147 +
  148 + @Override
  149 + public PageResult<AiMusicDO> getMusicPage(AiMusicPageReqVO pageReqVO) {
  150 + return musicMapper.selectPage(pageReqVO);
  151 + }
  152 +
  153 + @Override
  154 + public PageResult<AiMusicDO> getMusicMyPage(AiMusicPageReqVO pageReqVO, Long userId) {
  155 + return musicMapper.selectPageByMy(pageReqVO, userId);
  156 + }
  157 +
88 158 /**
89 159 * 构建 AiMusicDO 集合
90 160 *
91 161 * @param musicList suno 音乐任务列表
92 162 * @return AiMusicDO 集合
93 163 */
94   - private static List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) {
95   - return convertList(musicList, musicData -> new AiMusicDO()
96   - .setTaskId(musicData.id()).setModel(musicData.modelName())
97   - .setPrompt(musicData.prompt()).setGptDescriptionPrompt(musicData.gptDescriptionPrompt())
98   - .setAudioUrl(musicData.audioUrl()).setVideoUrl(musicData.videoUrl()).setImageUrl(musicData.imageUrl())
99   - .setTitle(musicData.title()).setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA))
100   - .setStatus(Objects.equals("complete", musicData.status()) ? AiMusicStatusEnum.SUCCESS.getStatus() : AiMusicStatusEnum.IN_PROGRESS.getStatus()));
  164 + private List<AiMusicDO> buildMusicDOList(List<SunoApi.MusicData> musicList) {
  165 + return convertList(musicList, musicData -> {
  166 + Integer status;
  167 + if (Objects.equals("complete", musicData.status())) {
  168 + status = AiMusicStatusEnum.SUCCESS.getStatus();
  169 + } else if (Objects.equals("error", musicData.status())) {
  170 + status = AiMusicStatusEnum.FAIL.getStatus();
  171 + } else {
  172 + status = AiMusicStatusEnum.IN_PROGRESS.getStatus();
  173 + }
  174 + return new AiMusicDO()
  175 + .setTaskId(musicData.id()).setModel(musicData.modelName())
  176 + .setDescription(musicData.gptDescriptionPrompt())
  177 + .setAudioUrl(downloadFile(status, musicData.audioUrl()))
  178 + .setVideoUrl(downloadFile(status, musicData.videoUrl()))
  179 + .setImageUrl(downloadFile(status, musicData.imageUrl()))
  180 + .setTitle(musicData.title()).setDuration(musicData.duration())
  181 + .setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA))
  182 + .setErrorMessage(musicData.errorMessage())
  183 + .setStatus(status);
  184 + });
  185 + }
  186 +
  187 + /**
  188 + * 音乐生成好后,将音频文件上传到文件服务器
  189 + *
  190 + * @param status 音乐状态
  191 + * @param url 音频文件地址
  192 + * @return 内部文件地址
  193 + */
  194 + private String downloadFile(Integer status, String url) {
  195 + if (StrUtil.isBlank(url) || ObjectUtil.notEqual(status, AiMusicStatusEnum.SUCCESS.getStatus())) {
  196 + return url;
  197 + }
  198 + try {
  199 + byte[] bytes = HttpUtil.downloadBytes(url);
  200 + return fileApi.createFile(bytes);
  201 + } catch (Exception e) {
  202 + log.error("[downloadFile][url({}) 下载失败]", url, e);
  203 + return url;
  204 + }
  205 + }
101 206  
  207 + /**
  208 + * 校验音乐是否存在
  209 + *
  210 + * @param id 音乐编号
  211 + * @return 音乐信息
  212 + */
  213 + private AiMusicDO validateMusicExists(Long id) {
  214 + AiMusicDO music = musicMapper.selectById(id);
  215 + if (music == null) {
  216 + throw exception(MUSIC_NOT_EXISTS);
  217 + }
  218 + return music;
102 219 }
  220 +
103 221 }
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
... ... @@ -2,33 +2,38 @@
2 2 <project xmlns="http://maven.apache.org/POM/4.0.0"
3 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5   - <modelVersion>4.0.0</modelVersion>
6 5 <parent>
7 6 <groupId>cn.iocoder.boot</groupId>
8 7 <artifactId>yudao-module-ai</artifactId>
9 8 <version>${revision}</version>
10 9 </parent>
  10 + <modelVersion>4.0.0</modelVersion>
11 11  
12 12 <artifactId>yudao-spring-boot-starter-ai</artifactId>
  13 + <name>${project.artifactId}</name>
  14 + <description>AI 大模型拓展,接入国内外大模型</description>
  15 + <properties>
  16 + <spring-ai.version>1.0.0-M1</spring-ai.version>
  17 + </properties>
13 18  
14 19 <dependencies>
15 20 <dependency>
16   - <groupId>io.springboot.ai</groupId>
  21 + <groupId>org.springframework.ai</groupId>
17 22 <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
18   - <version>1.0.3</version>
  23 + <version>${spring-ai.version}</version>
19 24 </dependency>
20 25 <dependency>
21   - <groupId>io.springboot.ai</groupId>
  26 + <groupId>org.springframework.ai</groupId>
22 27 <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
23   - <version>1.0.3</version>
  28 + <version>${spring-ai.version}</version>
24 29 </dependency>
25 30 <dependency>
26   - <groupId>io.springboot.ai</groupId>
27   - <artifactId>spring-ai-stability-ai</artifactId>
28   - <version>1.0.3</version>
  31 + <groupId>org.springframework.ai</groupId>
  32 + <artifactId>spring-ai-stability-ai-spring-boot-starter</artifactId>
  33 + <version>${spring-ai.version}</version>
29 34 </dependency>
30 35 <!-- <dependency>-->
31   -<!-- <groupId>io.springboot.ai</groupId>-->
  36 +<!-- <groupId>org.springframework.ai</groupId>-->
32 37 <!-- <artifactId>spring-ai-vertex-ai-gemini</artifactId>-->
33 38 <!-- <version>1.0.3</version>-->
34 39 <!-- </dependency>-->
... ... @@ -38,11 +43,19 @@
38 43 <artifactId>yudao-common</artifactId>
39 44 </dependency>
40 45  
  46 + <!-- TODO 芋艿:等 spring-ai 官方发布后,需要把 groupId 改下 -->
  47 + <dependency>
  48 + <groupId>group.springframework.ai</groupId>
  49 + <artifactId>spring-ai-qianfan-spring-boot-starter</artifactId>
  50 + <version>1.1.0</version>
  51 + </dependency>
  52 +
41 53 <!-- 阿里云 通义千问 -->
  54 + <!-- TODO 芋艿:等 spring cloud alibaba ai 发布最新的时候,可以替换掉这个依赖,并且删除我们直接 cv 的代码 -->
42 55 <dependency>
43 56 <groupId>com.alibaba</groupId>
44 57 <artifactId>dashscope-sdk-java</artifactId>
45   - <version>2.11.0</version>
  58 + <version>2.14.0</version>
46 59 </dependency>
47 60  
48 61 <dependency>
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
... ... @@ -4,21 +4,16 @@ import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactory;
4 4 import cn.iocoder.yudao.framework.ai.core.factory.AiClientFactoryImpl;
5 5 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
6 6 import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
7   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient;
8   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal;
9   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenOptions;
10   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.api.QianWenApi;
11 7 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient;
12 8 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
13 9 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
14   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatClient;
15   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatOptions;
16   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanApi;
  10 +import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration;
17 11 import lombok.extern.slf4j.Slf4j;
18 12 import org.springframework.boot.autoconfigure.AutoConfiguration;
19 13 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
20 14 import org.springframework.boot.context.properties.EnableConfigurationProperties;
21 15 import org.springframework.context.annotation.Bean;
  16 +import org.springframework.context.annotation.Import;
22 17  
23 18 /**
24 19 * 芋道 AI 自动配置
... ... @@ -28,6 +23,7 @@ import org.springframework.context.annotation.Bean;
28 23 @AutoConfiguration
29 24 @EnableConfigurationProperties(YudaoAiProperties.class)
30 25 @Slf4j
  26 +@Import(TongYiAutoConfiguration.class)
31 27 public class YudaoAiAutoConfiguration {
32 28  
33 29 @Bean
... ... @@ -58,48 +54,6 @@ public class YudaoAiAutoConfiguration {
58 54 }
59 55  
60 56 @Bean
61   - @ConditionalOnProperty(value = "yudao.ai.qianwen.enable", havingValue = "true")
62   - public QianWenChatClient qianWenChatClient(YudaoAiProperties yudaoAiProperties) {
63   - YudaoAiProperties.QianWenProperties qianWenProperties = yudaoAiProperties.getQianwen();
64   - // 转换配置
65   - QianWenOptions qianWenOptions = new QianWenOptions();
66   -// qianWenOptions.setModel(qianWenProperties.getModel().getModel()); TODO @fan:这里报错了
67   - qianWenOptions.setTemperature(qianWenProperties.getTemperature());
68   -// qianWenOptions.setTopK(qianWenProperties.getTopK()); TODO 芋艿:后续弄
69   - qianWenOptions.setTopP(qianWenProperties.getTopP());
70   - qianWenOptions.setMaxTokens(qianWenProperties.getMaxTokens());
71   -// qianWenOptions.setTemperature(qianWenProperties.getTemperature()); TODO 芋艿:后续弄
72   - return new QianWenChatClient(
73   - new QianWenApi(
74   - qianWenProperties.getApiKey(),
75   - QianWenChatModal.QWEN_72B_CHAT
76   - ),
77   - qianWenOptions
78   - );
79   - }
80   -
81   - @Bean
82   - @ConditionalOnProperty(value = "yudao.ai.yiyan.enable", havingValue = "true")
83   - public YiYanChatClient yiYanChatClient(YudaoAiProperties yudaoAiProperties) {
84   - YudaoAiProperties.YiYanProperties yiYanProperties = yudaoAiProperties.getYiyan();
85   - // 转换配置
86   - YiYanChatOptions yiYanOptions = new YiYanChatOptions();
87   -// yiYanOptions.setTopK(yiYanProperties.getTopK()); TODO 芋艿:后续弄
88   - yiYanOptions.setTopP(yiYanProperties.getTopP());
89   - yiYanOptions.setTemperature(yiYanProperties.getTemperature());
90   - yiYanOptions.setMaxOutputTokens(yiYanProperties.getMaxTokens());
91   - return new YiYanChatClient(
92   - new YiYanApi(
93   - yiYanProperties.getAppKey(),
94   - yiYanProperties.getSecretKey(),
95   - yiYanProperties.getModel(),
96   - yiYanProperties.getRefreshTokenSecondTime()
97   - ),
98   - yiYanOptions
99   - );
100   - }
101   -
102   - @Bean
103 57 @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true")
104 58 public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) {
105 59 YudaoAiProperties.MidjourneyProperties config = yudaoAiProperties.getMidjourney();
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java
... ... @@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.ai.config;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
4 4 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
5   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanChatModel;
6 5 import lombok.Data;
7 6 import lombok.experimental.Accessors;
8 7 import org.springframework.ai.autoconfigure.openai.OpenAiImageProperties;
... ... @@ -20,9 +19,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
20 19 @ConfigurationProperties(prefix = "yudao.ai")
21 20 public class YudaoAiProperties {
22 21  
23   - private QianWenProperties qianwen;
24 22 private XingHuoProperties xinghuo;
25   - private YiYanProperties yiyan;
26 23 private OpenAiImageProperties openAiImage;
27 24 private MidjourneyProperties midjourney;
28 25 private SunoProperties suno;
... ... @@ -47,22 +44,6 @@ public class YudaoAiProperties {
47 44 }
48 45  
49 46 @Data
50   - @Accessors(chain = true)
51   - public static class QianWenProperties extends ChatProperties {
52   -
53   - /**
54   - * api key
55   - */
56   - private String apiKey;
57   - /**
58   - * model
59   - */
60   - private YiYanChatModel model;
61   -
62   - }
63   -
64   - @Data
65   - @Accessors(chain = true)
66 47 public static class XingHuoProperties extends ChatProperties {
67 48  
68 49 private String appId;
... ... @@ -73,28 +54,6 @@ public class YudaoAiProperties {
73 54 }
74 55  
75 56 @Data
76   - @Accessors(chain = true)
77   - public static class YiYanProperties extends ChatProperties {
78   -
79   - /**
80   - * appKey
81   - */
82   - private String appKey;
83   - /**
84   - * secretKey
85   - */
86   - private String secretKey;
87   - /**
88   - * 模型
89   - */
90   - private YiYanChatModel model = YiYanChatModel.ERNIE4_3_5_8K;
91   - /**
92   - * token 刷新时间(默认 86400 = 24小时)
93   - */
94   - private int refreshTokenSecondTime = 86400;
95   - }
96   -
97   - @Data
98 57 public static class MidjourneyProperties {
99 58  
100 59 private String enable;
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactory.java
1 1 package cn.iocoder.yudao.framework.ai.core.factory;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
4   -import org.springframework.ai.chat.StreamingChatClient;
5   -import org.springframework.ai.image.ImageClient;
  4 +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
  5 +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
  6 +import org.springframework.ai.chat.model.StreamingChatModel;
  7 +import org.springframework.ai.image.ImageModel;
6 8  
7 9 /**
8 10 * AI 客户端工厂的接口类
... ... @@ -21,7 +23,7 @@ public interface AiClientFactory {
21 23 * @param url API URL
22 24 * @return StreamingChatClient 对象
23 25 */
24   - StreamingChatClient getOrCreateStreamingChatClient(AiPlatformEnum platform, String apiKey, String url);
  26 + StreamingChatModel getOrCreateStreamingChatClient(AiPlatformEnum platform, String apiKey, String url);
25 27  
26 28 /**
27 29 * 基于默认配置,获得 StreamingChatClient 对象
... ... @@ -31,7 +33,7 @@ public interface AiClientFactory {
31 33 * @param platform 平台
32 34 * @return StreamingChatClient 对象
33 35 */
34   - StreamingChatClient getDefaultStreamingChatClient(AiPlatformEnum platform);
  36 + StreamingChatModel getDefaultStreamingChatClient(AiPlatformEnum platform);
35 37  
36 38 /**
37 39 * 基于默认配置,获得 ImageClient 对象
... ... @@ -41,7 +43,7 @@ public interface AiClientFactory {
41 43 * @param platform 平台
42 44 * @return ImageClient 对象
43 45 */
44   - ImageClient getDefaultImageClient(AiPlatformEnum platform);
  46 + ImageModel getDefaultImageClient(AiPlatformEnum platform);
45 47  
46 48 /**
47 49 * 基于指定配置,获得 ImageClient 对象
... ... @@ -53,6 +55,28 @@ public interface AiClientFactory {
53 55 * @param url API URL
54 56 * @return ImageClient 对象
55 57 */
56   - ImageClient getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url);
  58 + ImageModel getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url);
  59 +
  60 + /**
  61 + * 基于指定配置,获得 MidjourneyApi 对象
  62 + *
  63 + * 如果不存在,则进行创建
  64 + *
  65 + * @param apiKey API KEY
  66 + * @param url API URL
  67 + * @return MidjourneyApi 对象
  68 + */
  69 + MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url);
  70 +
  71 + /**
  72 + * 基于指定配置,获得 SunoApi 对象
  73 + *
  74 + * 如果不存在,则进行创建
  75 + *
  76 + * @param apiKey API KEY
  77 + * @param url API URL
  78 + * @return SunoApi 对象
  79 + */
  80 + SunoApi getOrCreateSunoApi(String apiKey, String url);
57 81  
58 82 }
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiClientFactoryImpl.java
... ... @@ -9,26 +9,35 @@ import cn.hutool.extra.spring.SpringUtil;
9 9 import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration;
10 10 import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties;
11 11 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
12   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient;
13   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal;
14   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.api.QianWenApi;
  12 +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
  13 +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
15 14 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient;
16 15 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
17   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatClient;
18   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanApi;
  16 +import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration;
  17 +import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties;
  18 +import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel;
  19 +import com.alibaba.cloud.ai.tongyi.chat.TongYiChatProperties;
  20 +import com.alibaba.dashscope.aigc.generation.Generation;
19 21 import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
20 22 import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
21   -import org.springframework.ai.chat.StreamingChatClient;
22   -import org.springframework.ai.image.ImageClient;
23   -import org.springframework.ai.ollama.OllamaChatClient;
  23 +import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration;
  24 +import org.springframework.ai.autoconfigure.qianfan.QianFanChatProperties;
  25 +import org.springframework.ai.autoconfigure.qianfan.QianFanConnectionProperties;
  26 +import org.springframework.ai.chat.model.StreamingChatModel;
  27 +import org.springframework.ai.image.ImageModel;
  28 +import org.springframework.ai.ollama.OllamaChatModel;
24 29 import org.springframework.ai.ollama.api.OllamaApi;
25   -import org.springframework.ai.openai.OpenAiChatClient;
26   -import org.springframework.ai.openai.OpenAiImageClient;
  30 +import org.springframework.ai.openai.OpenAiChatModel;
  31 +import org.springframework.ai.openai.OpenAiImageModel;
27 32 import org.springframework.ai.openai.api.ApiUtils;
28 33 import org.springframework.ai.openai.api.OpenAiApi;
29 34 import org.springframework.ai.openai.api.OpenAiImageApi;
30   -import org.springframework.ai.stabilityai.StabilityAiImageClient;
  35 +import org.springframework.ai.qianfan.QianFanChatModel;
  36 +import org.springframework.ai.qianfan.api.QianFanApi;
  37 +import org.springframework.ai.stabilityai.StabilityAiImageModel;
31 38 import org.springframework.ai.stabilityai.api.StabilityAiApi;
  39 +import org.springframework.retry.support.RetryTemplate;
  40 +import org.springframework.web.client.ResponseErrorHandler;
32 41 import org.springframework.web.client.RestClient;
33 42  
34 43 import java.util.List;
... ... @@ -41,9 +50,9 @@ import java.util.List;
41 50 public class AiClientFactoryImpl implements AiClientFactory {
42 51  
43 52 @Override
44   - public StreamingChatClient getOrCreateStreamingChatClient(AiPlatformEnum platform, String apiKey, String url) {
45   - String cacheKey = buildClientCacheKey(StreamingChatClient.class, platform, apiKey, url);
46   - return Singleton.get(cacheKey, (Func0<StreamingChatClient>) () -> {
  53 + public StreamingChatModel getOrCreateStreamingChatClient(AiPlatformEnum platform, String apiKey, String url) {
  54 + String cacheKey = buildClientCacheKey(StreamingChatModel.class, platform, apiKey, url);
  55 + return Singleton.get(cacheKey, (Func0<StreamingChatModel>) () -> {
47 56 //noinspection EnhancedSwitchMigration
48 57 switch (platform) {
49 58 case OPENAI:
... ... @@ -65,39 +74,39 @@ public class AiClientFactoryImpl implements AiClientFactory {
65 74 }
66 75  
67 76 @Override
68   - public StreamingChatClient getDefaultStreamingChatClient(AiPlatformEnum platform) {
  77 + public StreamingChatModel getDefaultStreamingChatClient(AiPlatformEnum platform) {
69 78 //noinspection EnhancedSwitchMigration
70 79 switch (platform) {
71 80 case OPENAI:
72   - return SpringUtil.getBean(OpenAiChatClient.class);
  81 + return SpringUtil.getBean(OpenAiChatModel.class);
73 82 case OLLAMA:
74   - return SpringUtil.getBean(OllamaChatClient.class);
  83 + return SpringUtil.getBean(OllamaChatModel.class);
75 84 case YI_YAN:
76   - return SpringUtil.getBean(YiYanChatClient.class);
  85 + return SpringUtil.getBean(QianFanChatModel.class);
77 86 case XING_HUO:
78 87 return SpringUtil.getBean(XingHuoChatClient.class);
79 88 case QIAN_WEN:
80   - return SpringUtil.getBean(QianWenChatClient.class);
  89 + return SpringUtil.getBean(TongYiChatModel.class);
81 90 default:
82 91 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
83 92 }
84 93 }
85 94  
86 95 @Override
87   - public ImageClient getDefaultImageClient(AiPlatformEnum platform) {
  96 + public ImageModel getDefaultImageClient(AiPlatformEnum platform) {
88 97 //noinspection EnhancedSwitchMigration
89 98 switch (platform) {
90 99 case OPENAI:
91   - return SpringUtil.getBean(OpenAiImageClient.class);
  100 + return SpringUtil.getBean(OpenAiImageModel.class);
92 101 case STABLE_DIFFUSION:
93   - return SpringUtil.getBean(StabilityAiImageClient.class);
  102 + return SpringUtil.getBean(StabilityAiImageModel.class);
94 103 default:
95 104 throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
96 105 }
97 106 }
98 107  
99 108 @Override
100   - public ImageClient getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url) {
  109 + public ImageModel getOrCreateImageClient(AiPlatformEnum platform, String apiKey, String url) {
101 110 //noinspection EnhancedSwitchMigration
102 111 switch (platform) {
103 112 case OPENAI:
... ... @@ -109,6 +118,21 @@ public class AiClientFactoryImpl implements AiClientFactory {
109 118 }
110 119 }
111 120  
  121 + @Override
  122 + public MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url) {
  123 + String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, url);
  124 + return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> {
  125 + YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class).getMidjourney();
  126 + return new MidjourneyApi(url, apiKey, properties.getNotifyUrl());
  127 + });
  128 + }
  129 +
  130 + @Override
  131 + public SunoApi getOrCreateSunoApi(String apiKey, String url) {
  132 + String cacheKey = buildClientCacheKey(SunoApi.class, AiPlatformEnum.SUNO.getPlatform(), apiKey, url);
  133 + return Singleton.get(cacheKey, (Func0<SunoApi>) () -> new SunoApi(url));
  134 + }
  135 +
112 136 private static String buildClientCacheKey(Class<?> clazz, Object... params) {
113 137 if (ArrayUtil.isEmpty(params)) {
114 138 return clazz.getName();
... ... @@ -121,30 +145,31 @@ public class AiClientFactoryImpl implements AiClientFactory {
121 145 /**
122 146 * 可参考 {@link OpenAiAutoConfiguration}
123 147 */
124   - private static OpenAiChatClient buildOpenAiChatClient(String openAiToken, String url) {
  148 + private static OpenAiChatModel buildOpenAiChatClient(String openAiToken, String url) {
125 149 url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL);
126 150 OpenAiApi openAiApi = new OpenAiApi(url, openAiToken);
127   - return new OpenAiChatClient(openAiApi);
  151 + return new OpenAiChatModel(openAiApi);
128 152 }
129 153  
130 154 /**
131 155 * 可参考 {@link OllamaAutoConfiguration}
132 156 */
133   - private static OllamaChatClient buildOllamaChatClient(String url) {
  157 + private static OllamaChatModel buildOllamaChatClient(String url) {
134 158 OllamaApi ollamaApi = new OllamaApi(url);
135   - return new OllamaChatClient(ollamaApi);
  159 + return new OllamaChatModel(ollamaApi);
136 160 }
137 161  
138 162 /**
139   - * 可参考 {@link YudaoAiAutoConfiguration#yiYanChatClient(YudaoAiProperties)}
  163 + * 可参考 {@link QianFanAutoConfiguration#qianFanChatModel(QianFanConnectionProperties, QianFanChatProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)}
140 164 */
141   - private static YiYanChatClient buildYiYanChatClient(String key) {
  165 + private static QianFanChatModel buildYiYanChatClient(String key) {
  166 + // TODO 芋艿:貌似目前设置,request 势必会报错
142 167 List<String> keys = StrUtil.split(key, '|');
143 168 Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
144 169 String appKey = keys.get(0);
145 170 String secretKey = keys.get(1);
146   - YiYanApi yiYanApi = new YiYanApi(appKey, secretKey, YiYanApi.DEFAULT_CHAT_MODEL, 0);
147   - return new YiYanChatClient(yiYanApi);
  171 + QianFanApi qianFanApi = new QianFanApi(appKey, secretKey);
  172 + return new QianFanChatModel(qianFanApi);
148 173 }
149 174  
150 175 /**
... ... @@ -161,11 +186,16 @@ public class AiClientFactoryImpl implements AiClientFactory {
161 186 }
162 187  
163 188 /**
164   - * 可参考 {@link YudaoAiAutoConfiguration#qianWenChatClient(YudaoAiProperties)}
  189 + * 可参考 {@link TongYiAutoConfiguration#tongYiChatClient(Generation, TongYiChatProperties, TongYiConnectionProperties)}
165 190 */
166   - private static QianWenChatClient buildQianWenChatClient(String key) {
167   - QianWenApi qianWenApi = new QianWenApi(key, QianWenChatModal.QWEN_72B_CHAT);
168   - return new QianWenChatClient(qianWenApi);
  191 + private static TongYiChatModel buildQianWenChatClient(String key) {
  192 + com.alibaba.dashscope.aigc.generation.Generation generation = SpringUtil.getBean(Generation.class);
  193 + TongYiChatProperties chatOptions = SpringUtil.getBean(TongYiChatProperties.class);
  194 + // TODO @芋艿:貌似 apiKey 是全局唯一的???得测试下
  195 + // TODO @芋艿:貌似阿里云不是增量返回的
  196 + TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties();
  197 + connectionProperties.setApiKey(key);
  198 + return new TongYiAutoConfiguration().tongYiChatClient(generation, chatOptions, connectionProperties);
169 199 }
170 200  
171 201 // private static VertexAiGeminiChatClient buildGoogleGemir(String key) {
... ... @@ -175,16 +205,16 @@ public class AiClientFactoryImpl implements AiClientFactory {
175 205 // return new VertexAiGeminiChatClient(vertexApi);
176 206 // }
177 207  
178   - private ImageClient buildOpenAiImageClient(String openAiToken, String url) {
  208 + private OpenAiImageModel buildOpenAiImageClient(String openAiToken, String url) {
179 209 url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL);
180 210 OpenAiImageApi openAiApi = new OpenAiImageApi(url, openAiToken, RestClient.builder());
181   - return new OpenAiImageClient(openAiApi);
  211 + return new OpenAiImageModel(openAiApi);
182 212 }
183 213  
184   - private ImageClient buildStabilityAiImageClient(String apiKey, String url) {
  214 + private StabilityAiImageModel buildStabilityAiImageClient(String apiKey, String url) {
185 215 url = StrUtil.blankToDefault(url, StabilityAiApi.DEFAULT_BASE_URL);
186 216 StabilityAiApi stabilityAiApi = new StabilityAiApi(apiKey, StabilityAiApi.DEFAULT_IMAGE_MODEL, url);
187   - return new StabilityAiImageClient(stabilityAiApi);
  217 + return new StabilityAiImageModel(stabilityAiApi);
188 218 }
189 219  
190 220 }
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java
... ... @@ -117,7 +117,7 @@ public class SunoApi {
117 117 * @param prompt 用于生成音乐音频的提示
118 118 * @param tags 音乐风格
119 119 * @param title 音乐名称
120   - * @param mv 模型
  120 + * @param model 模型
121 121 * @param waitAudio false 表示后台模式,仅返回音频任务信息,需要调用 get API 获取详细的音频信息。
122 122 * true 表示同步模式,API 最多等待 100s,音频生成完毕后直接返回音频链接等信息,建议在 GPT 等 agent 中使用。
123 123 * @param makeInstrumental 指示音乐音频是否为定制,如果为 true,则从歌词生成,否则从提示生成
... ... @@ -127,7 +127,7 @@ public class SunoApi {
127 127 String prompt,
128 128 String tags,
129 129 String title,
130   - String mv,
  130 + String model,
131 131 @JsonProperty("wait_audio") boolean waitAudio,
132 132 @JsonProperty("make_instrumental") boolean makeInstrumental
133 133 ) {
... ... @@ -136,12 +136,12 @@ public class SunoApi {
136 136 this(prompt, null, null, null, false, false);
137 137 }
138 138  
139   - public MusicGenerateRequest(String prompt, String mv, boolean makeInstrumental) {
140   - this(prompt, null, null, mv, false, makeInstrumental);
  139 + public MusicGenerateRequest(String prompt, String model, boolean makeInstrumental) {
  140 + this(prompt, null, null, model, false, makeInstrumental);
141 141 }
142 142  
143   - public MusicGenerateRequest(String prompt, String mv, String tags, String title) {
144   - this(prompt, tags, title, mv, false, false);
  143 + public MusicGenerateRequest(String prompt, String model, String tags, String title) {
  144 + this(prompt, tags, title, model, false, false);
145 145 }
146 146  
147 147 }
... ... @@ -162,6 +162,7 @@ public class SunoApi {
162 162 * @param prompt 生成音乐音频的提示
163 163 * @param type 操作类型
164 164 * @param tags 音乐类型标签
  165 + * @param duration 音乐时长
165 166 */
166 167 public record MusicData(
167 168 String id,
... ... @@ -174,9 +175,11 @@ public class SunoApi {
174 175 @JsonProperty("model_name") String modelName,
175 176 String status,
176 177 @JsonProperty("gpt_description_prompt") String gptDescriptionPrompt,
  178 + @JsonProperty("error_message") String errorMessage,
177 179 String prompt,
178 180 String type,
179   - String tags
  181 + String tags,
  182 + Double duration
180 183 ) {
181 184 }
182 185  
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/tongyi/QianWenChatClient.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.tongyi;
2   -
3   -import cn.hutool.core.util.NumberUtil;
4   -import cn.iocoder.yudao.framework.ai.core.exception.ChatException;
5   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.api.QianWenApi;
6   -import org.springframework.ai.chat.*;
7   -import org.springframework.ai.chat.prompt.ChatOptions;
8   -import org.springframework.ai.chat.prompt.Prompt;
9   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.exception.YiYanApiException;
10   -import com.alibaba.dashscope.aigc.generation.GenerationResult;
11   -import com.alibaba.dashscope.aigc.generation.models.QwenParam;
12   -import com.alibaba.dashscope.common.Message;
13   -import com.google.common.collect.Lists;
14   -import io.reactivex.Flowable;
15   -import lombok.extern.slf4j.Slf4j;
16   -import org.jetbrains.annotations.NotNull;
17   -import org.springframework.http.ResponseEntity;
18   -import org.springframework.retry.RetryCallback;
19   -import org.springframework.retry.RetryContext;
20   -import org.springframework.retry.RetryListener;
21   -import org.springframework.retry.support.RetryTemplate;
22   -import reactor.core.publisher.Flux;
23   -
24   -import java.time.Duration;
25   -import java.util.List;
26   -import java.util.stream.Collectors;
27   -
28   -// TODO @芋艿:暂时不需要重构;等 spring cloud alibaba 的 ai 发布最新的
29   -/**
30   - * 阿里 通义千问 client
31   - * <p>
32   - * 文档地址:https://help.aliyun.com/document_detail/2587494.html?spm=a2c4g.2587492.0.0.53f33c566sXskp
33   - * <p>
34   - * author: fansili
35   - * time: 2024/3/13 21:06
36   - */
37   -@Slf4j
38   -public class QianWenChatClient implements ChatClient, StreamingChatClient {
39   -
40   - private QianWenApi qianWenApi;
41   -
42   - private QianWenOptions qianWenOptions;
43   -
44   - public QianWenChatClient() {
45   - }
46   -
47   - public QianWenChatClient(QianWenApi qianWenApi) {
48   - this.qianWenApi = qianWenApi;
49   - }
50   -
51   - public QianWenChatClient(QianWenApi qianWenApi, QianWenOptions qianWenOptions) {
52   - this.qianWenApi = qianWenApi;
53   - this.qianWenOptions = qianWenOptions;
54   - }
55   -
56   - // TODO @fansili:看看咋公用出来,允许传入类似异常之类的参数;
57   - public final RetryTemplate retryTemplate = RetryTemplate.builder()
58   - // 最大重试次数 10
59   - .maxAttempts(10)
60   - .retryOn(YiYanApiException.class)
61   - // 最大重试5次,第一次间隔3000ms,第二次3000ms * 2,第三次3000ms * 3,以此类推,最大间隔3 * 60000ms
62   - .exponentialBackoff(Duration.ofMillis(3000), 2, Duration.ofMillis(3 * 60000))
63   - .withListener(new RetryListener() {
64   - @Override
65   - public <T extends Object, E extends Throwable> void onError(RetryContext context,
66   - RetryCallback<T, E> callback, Throwable throwable) {
67   - log.warn("重试异常:" + context.getRetryCount(), throwable);
68   - }
69   - })
70   - .build();
71   -
72   - @Override
73   - public ChatResponse call(Prompt prompt) {
74   - return this.retryTemplate.execute(ctx -> {
75   - // ctx 会有重试的信息
76   - // 创建 request 请求,stream模式需要供应商支持
77   - QwenParam request = this.createRequest(prompt, false);
78   - // 调用 callWithFunctionSupport 发送请求
79   - ResponseEntity<GenerationResult> responseEntity = qianWenApi.chatCompletionEntity(request);
80   - // 获取结果封装 chatCompletion
81   - GenerationResult response = responseEntity.getBody();
82   -// if (!response.isSuccess()) {
83   -// return new ChatResponse(List.of(new Generation(String.format("failed to create completion, requestId: %s, code: %s, message: %s\n",
84   -// response.getRequestId(), response.getCode(), response.getMessage()))));
85   -// }
86   - // 转换为 Generation 返回
87   - return new ChatResponse(response.getOutput().getChoices().stream()
88   - .map(choices -> new Generation(choices.getMessage().getContent()))
89   - .collect(Collectors.toList()));
90   - });
91   - }
92   -
93   - private QwenParam createRequest(Prompt prompt, boolean stream) {
94   - // 获取 ChatOptions
95   - QianWenOptions chatOptions = getChatOptions(prompt);
96   - //
97   - List<Message> messageList = Lists.newArrayList();
98   - prompt.getInstructions().stream().forEach(instruction -> {
99   - Message message = new Message();
100   - message.setRole(instruction.getMessageType().getValue());
101   - message.setContent(instruction.getContent());
102   - messageList.add(message);
103   - });
104   - return QwenParam.builder()
105   - .model(qianWenApi.getQianWenChatModal().getModel())
106   - .prompt(prompt.getContents())
107   - .messages(messageList)
108   - .maxTokens(chatOptions.getMaxTokens())
109   - .resultFormat(QwenParam.ResultFormat.MESSAGE)
110   - .topP(chatOptions.getTopP() == null ? null : Double.valueOf(chatOptions.getTopP()))
111   - .topK(chatOptions.getTopK())
112   - .temperature(chatOptions.getTemperature())
113   - // 控制流式输出模式,即后面的内容会包含已经输出的内容;设置为True,将开启增量输出模式,后面的输出不会包含已经输出的内容,您需要自行拼接整体输出
114   - .incrementalOutput(true)
115   - /* set the random seed, optional, default to 1234 if not set */
116   - .seed(100)
117   - .apiKey(qianWenApi.getApiKey())
118   - .build();
119   - }
120   -
121   - private @NotNull QianWenOptions getChatOptions(Prompt prompt) {
122   - // 两个都为null 则没有配置文件
123   - if (qianWenOptions == null && prompt.getOptions() == null) {
124   - throw new ChatException("ChatOptions 未配置参数!");
125   - }
126   - // 优先使用 Prompt 里面的 ChatOptions
127   - ChatOptions options = qianWenOptions;
128   - if (prompt.getOptions() != null) {
129   - options = (ChatOptions) prompt.getOptions();
130   - }
131   - // Prompt 里面是一个 ChatOptions,用户可以随意传入,这里做一下判断
132   - if (!(options instanceof QianWenOptions)) {
133   - throw new ChatException("Prompt 传入的不是 QianWenOptions!");
134   - }
135   - return (QianWenOptions) options;
136   - }
137   -
138   - @Override
139   - public Flux<ChatResponse> stream(Prompt prompt) {
140   - // ctx 会有重试的信息
141   - // 创建 request 请求,stream模式需要供应商支持
142   - QwenParam request = this.createRequest(prompt, true);
143   - // 调用 callWithFunctionSupport 发送请求
144   - Flowable<GenerationResult> responseResult = this.qianWenApi.chatCompletionStream(request);
145   -
146   - return Flux.create(fluxSink ->
147   - responseResult.subscribe(
148   - value -> fluxSink.next(
149   - new ChatResponse(value.getOutput().getChoices().stream()
150   - .map(choices -> new Generation(choices.getMessage().getContent()))
151   - .collect(Collectors.toList()))
152   - ),
153   - error -> fluxSink.error(error),
154   - () -> fluxSink.complete()
155   - )
156   - );
157   - }
158   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/tongyi/QianWenChatModal.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.tongyi;
2   -
3   -import lombok.AllArgsConstructor;
4   -import lombok.Getter;
5   -
6   -/**
7   - * 千问 chat 模型
8   - *
9   - * 模型地址:https://help.aliyun.com/document_detail/2712576.html
10   - * 模型介绍:https://help.aliyun.com/document_detail/2666503.html?spm=a2c4g.2701795.0.0.26eb34dfKzcWN4
11   - *
12   - * @author fansili
13   - * @time 2024/4/26 10:15
14   - * @since 1.0
15   - */
16   -@AllArgsConstructor
17   -@Getter
18   -public enum QianWenChatModal {
19   -
20   - // 千问付费模型
21   - QWEN_TURBO("通义千问超大规模语言模型", "qwen-turbo"),
22   - QWEN_PLUS("通义千问超大规模语言模型增强版", "qwen-plus"),
23   - QWEN_MAX("通义千问千亿级别超大规模语言模型", "qwen-max"),
24   - QWEN_MAX_0403("通义千问千亿级别超大规模语言模型-0403", "qwen-max-0403"),
25   - QWEN_MAX_0107("通义千问千亿级别超大规模语言模型-0107", "qwen-max-0107"),
26   - QWEN_MAX_1201("通义千问千亿级别超大规模语言模型-1201", "qwen-max-1201"),
27   - QWEN_MAX_LONGCONTEXT("通义千问千亿级别超大规模语言模型-28k tokens", "qwen-max-longcontext"),
28   -
29   - // 开源模型
30   - // https://help.aliyun.com/document_detail/2666503.html?spm=a2c4g.2701795.0.0.26eb34dfKzcWN4
31   - QWEN_72B_CHAT("通义千问1.5对外开源的72B规模参数量的经过人类指令对齐的chat模型", "qwen-72b-chat"),
32   -
33   - ;
34   -
35   - private String name;
36   -
37   - private String model;
38   -
39   - public static QianWenChatModal valueOfModel(String model) {
40   - for (QianWenChatModal itemEnum : QianWenChatModal.values()) {
41   - if (itemEnum.getModel().equals(model)) {
42   - return itemEnum;
43   - }
44   - }
45   - throw new IllegalArgumentException("Invalid MessageType value: " + model);
46   - }
47   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/tongyi/QianWenOptions.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.tongyi;
2   -
3   -import org.springframework.ai.chat.prompt.ChatOptions;
4   -import lombok.Data;
5   -import lombok.experimental.Accessors;
6   -
7   -import java.util.List;
8   -
9   -/**
10   - * 阿里云 千问 属性
11   - *
12   - * 地址:https://help.aliyun.com/document_detail/2684682.html?spm=a2c4g.2621347.0.0.195117e7Ytpkyo
13   - *
14   - * author: fansili
15   - * time: 2024/3/15 19:57
16   - */
17   -@Data
18   -@Accessors(chain = true)
19   -public class QianWenOptions implements ChatOptions {
20   -
21   - /**
22   - * 用户与模型的对话历史
23   - */
24   - private List<Message> messages;
25   - /**
26   - * 生成时,核采样方法的概率阈值。例如,取值为0.8时,仅保留累计概率之和大于等于0.8的概率分布中的token,
27   - * 作为随机采样的候选集。取值范围为(0,1.0),取值越大,生成的随机性越高;取值越低,生成的随机性越低。
28   - * 默认值为0.8。注意,取值不要大于等于1
29   - */
30   - private Float topP;
31   - /**
32   - * 用于限制模型生成token的数量,max_tokens设置的是生成上限,并不表示一定会生成这么多的token数量。其中qwen1.5-14b-chat、qwen1.5-7b-chat、qwen-14b-chat和qwen-7b-chat最大值和默认值均为1500,qwen-1.8b-chat、qwen-1.8b-longcontext-chat和qwen-72b-chat最大值和默认值均为2000
33   - */
34   - private Integer maxTokens = 1500;
35   - /**
36   - * 模型
37   - */
38   - private String model;
39   - /**
40   - * temperature
41   - */
42   - private Float temperature;
43   -
44   - //
45   - // 适配 ChatOptions
46   -
47   - @Override
48   - public Float getTemperature() {
49   - return null;
50   - }
51   -
52   - @Override
53   - public Integer getTopK() {
54   - return null;
55   - }
56   -
57   - public Float getTopP() {
58   - return topP;
59   - }
60   -
61   - @Data
62   - @Accessors
63   - public static class Message {
64   - /**
65   - * 角色: system、user或assistant
66   - */
67   - private String role;
68   - /**
69   - * 提示词或模型内容
70   - */
71   - private String content;
72   - }
73   -
74   - @Data
75   - @Accessors
76   - public static class Parameters {
77   - /**
78   - * 输出格式, 默认为"text"
79   - * "text"表示旧版本的text
80   - * "message"表示兼容openai的message
81   - */
82   - private String resultFormat;
83   - /**
84   - * 生成时,采样候选集的大小。例如,取值为50时,仅将单次生成中得分最高的50个token组成随机采样的候选集。
85   - * 取值越大,生成的随机性越高;取值越小,生成的确定性越高。
86   - * 注意:如果top_k参数为空或者top_k的值大于100,表示不启用top_k策略,此时仅有top_p策略生效,默认是空。
87   - */
88   - private Integer topK;
89   - /**
90   - * 生成时使用的随机数种子,用户控制模型生成内容的随机性。
91   - * seed支持无符号64位整数,默认值为1234。在使用seed时,模型将尽可能生成相同或相似的结果,但目前不保证每次生成的结果完全相同。
92   - */
93   - private Integer seed;
94   - /**
95   - * 用于控制随机性和多样性的程度。具体来说,temperature值控制了生成文本时对每个候选词的概率分布进行平滑的程度。
96   - * 较高的temperature值会降低概率分布的峰值,使得更多的低概率词被选择,
97   - * 生成结果更加多样化;而较低的temperature值则会增强概率分布的峰值,使得高概率词更容易被选择,生成结果更加确定。
98   - * 取值范围: [0, 2),系统默认值1.0。不建议取值为0,无意义。
99   - */
100   - private Float temperature;
101   - /**
102   - * 用于限制模型生成token的数量,max_tokens设置的是生成上限,并不表示一定会生成这么多的token数量。
103   - * 其中qwen-turbo 最大值和默认值为1500, qwen-max、qwen-max-1201 、qwen-max-longcontext 和 qwen-plus最大值和默认值均为2000。
104   - */
105   - private Integer maxTokens;
106   - /**
107   - * stop参数用于实现内容生成过程的精确控制,在生成内容即将包含指定的字符串或token_ids时自动停止,生成内容不包含指定的内容。
108   - * 例如,如果指定stop为"你好",表示将要生成"你好"时停止;如果指定stop为[37763, 367],表示将要生成"Observation"时停止。
109   - */
110   - private List<String> stop;
111   - /**
112   - * 用于控制流式输出模式,默认False,即后面内容会包含已经输出的内容;设置为True,将开启增量输出模式,
113   - * 后面输出不会包含已经输出的内容,您需要自行拼接整体输出,参考流式输出示例代码。
114   - */
115   - private Boolean incrementalOutput;
116   - }
117   -}
118   -
119   -
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/tongyi/api/QianWenApi.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.tongyi.api;
2   -
3   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal;
4   -import cn.iocoder.yudao.framework.ai.core.exception.AiException;
5   -import com.alibaba.dashscope.aigc.generation.Generation;
6   -import com.alibaba.dashscope.aigc.generation.GenerationResult;
7   -import com.alibaba.dashscope.aigc.generation.models.QwenParam;
8   -import com.alibaba.dashscope.exception.InputRequiredException;
9   -import com.alibaba.dashscope.exception.NoApiKeyException;
10   -import io.reactivex.Flowable;
11   -import lombok.Getter;
12   -import org.springframework.http.HttpStatusCode;
13   -import org.springframework.http.ResponseEntity;
14   -
15   -// TODO done @fansili:是不是挪到 api 包里?按照 spring ai 的结构;根目录只放 client 和 options
16   -
17   -/**
18   - * 阿里 通义千问
19   - * <p>
20   - * author: fansili
21   - * time: 2024/3/13 21:09
22   - */
23   -@Getter
24   -public class QianWenApi {
25   -
26   - // api key 获取地址:https://bailian.console.aliyun.com/?spm=5176.28197581.0.0.38db29a4G3GcVb&apiKey=1#/api-key
27   - private String apiKey = "sk-Zsd81gZYg7";
28   - private Generation gen = new Generation();
29   - private QianWenChatModal qianWenChatModal;
30   -
31   - public QianWenApi(String apiKey, QianWenChatModal qianWenChatModal) {
32   - this.apiKey = apiKey;
33   - this.qianWenChatModal = qianWenChatModal;
34   - }
35   -
36   - public ResponseEntity<GenerationResult> chatCompletionEntity(QwenParam request) {
37   - GenerationResult call;
38   - try {
39   - call = gen.call(request);
40   - } catch (NoApiKeyException e) {
41   - throw new AiException("没有找到apiKey!" + e.getMessage());
42   - } catch (InputRequiredException e) {
43   - throw new AiException("chat缺少必填字段!" + e.getMessage());
44   - }
45   - // 阿里云的这个 http code 随便设置,外面判断是否成功用的 CompletionsResponse.isSuccess
46   - return new ResponseEntity<>(call, HttpStatusCode.valueOf(200));
47   - }
48   -
49   - public Flowable<GenerationResult> chatCompletionStream(QwenParam request) {
50   - Flowable<GenerationResult> resultFlowable;
51   - try {
52   - resultFlowable = gen.streamCall(request);
53   - } catch (NoApiKeyException e) {
54   - throw new AiException("没有找到apiKey!" + e.getMessage());
55   - } catch (InputRequiredException e) {
56   - throw new AiException("chat缺少必填字段!" + e.getMessage());
57   - }
58   - return resultFlowable;
59   - }
60   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/tongyi/api/QianWenChatCompletion.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.tongyi.api;
2   -
3   -
4   -/**
5   - * author: fansili
6   - * time: 2024/3/13 21:07
7   - */
8   -public class QianWenChatCompletion {
9   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/tongyi/api/QianWenChatCompletionMessage.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.tongyi.api;
2   -
3   -/**
4   - * author: fansili
5   - * time: 2024/3/13 21:07
6   - */
7   -public class QianWenChatCompletionMessage {
8   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/tongyi/api/QianWenChatCompletionRequest.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.tongyi.api;
2   -
3   -import com.alibaba.dashscope.aigc.generation.models.QwenParam;
4   -
5   -/**
6   - * 千问
7   - *
8   - * author: fansili
9   - * time: 2024/3/13 21:07
10   - */
11   -public class QianWenChatCompletionRequest extends QwenParam {
12   -
13   - protected QianWenChatCompletionRequest(QwenParamBuilder<?, ?> b) {
14   - super(b);
15   - }
16   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/tongyi/package-info.java deleted
1   -/**
2   - * 阿里的 通义千问
3   - *
4   - * 链接:https://www.aliyun.com/search?k=%E9%80%9A%E4%B9%89%E5%A4%A7%E6%A8%A1%E5%9E%8B&scene=all
5   - *
6   - * 千问所有模型:https://bailian.console.aliyun.com/?spm=5176.28515448.J_TC9GqcHi2edq9zUs9ZsDQ.1.417338b17zJTjy#/efm/my_model
7   - *
8   - * author: fansili
9   - * time: 2024/3/13 21:05
10   - */
11   -package cn.iocoder.yudao.framework.ai.core.model.tongyi;
12 0 \ No newline at end of file
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatClient.java
... ... @@ -6,10 +6,13 @@ import cn.iocoder.yudao.framework.ai.core.exception.ChatException;
6 6 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
7 7 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletion;
8 8 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoChatCompletionRequest;
9   -import org.springframework.ai.chat.*;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +import org.springframework.ai.chat.model.ChatModel;
  11 +import org.springframework.ai.chat.model.ChatResponse;
  12 +import org.springframework.ai.chat.model.Generation;
  13 +import org.springframework.ai.chat.model.StreamingChatModel;
10 14 import org.springframework.ai.chat.prompt.ChatOptions;
11 15 import org.springframework.ai.chat.prompt.Prompt;
12   -import lombok.extern.slf4j.Slf4j;
13 16 import org.springframework.http.ResponseEntity;
14 17 import org.springframework.retry.RetryCallback;
15 18 import org.springframework.retry.RetryContext;
... ... @@ -29,7 +32,7 @@ import java.util.stream.Collectors;
29 32 * time: 2024/3/11 10:19
30 33 */
31 34 @Slf4j
32   -public class XingHuoChatClient implements ChatClient, StreamingChatClient {
  35 +public class XingHuoChatClient implements ChatModel, StreamingChatModel {
33 36  
34 37 private XingHuoApi xingHuoApi;
35 38  
... ... @@ -64,7 +67,6 @@ public class XingHuoChatClient implements ChatClient, StreamingChatClient {
64 67  
65 68 @Override
66 69 public ChatResponse call(Prompt prompt) {
67   -
68 70 return this.retryTemplate.execute(ctx -> {
69 71 // ctx 会有重试的信息
70 72 // 获取 chatOptions 属性
... ... @@ -79,6 +81,12 @@ public class XingHuoChatClient implements ChatClient, StreamingChatClient {
79 81 }
80 82  
81 83 @Override
  84 + public ChatOptions getDefaultOptions() {
  85 + // TODO 芋艿:需要跟进下
  86 + throw new UnsupportedOperationException();
  87 + }
  88 +
  89 + @Override
82 90 public Flux<ChatResponse> stream(Prompt prompt) {
83 91 // 获取 chatOptions 属性
84 92 XingHuoOptions chatOptions = this.getChatOptions(prompt);
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/api/XingHuoApi.java
... ... @@ -134,7 +134,7 @@ public class XingHuoApi {
134 134 URI uri = URI.create(authUrl);
135 135 // 发起 wss 请求并处理响应
136 136 // 创建一个 Flux 来处理接收到的消息
137   - Flux<XingHuoChatCompletion> messageFlux = Flux.create(sink -> {
  137 + return Flux.create(sink -> {
138 138 socketClient.execute(uri, session ->
139 139 session.send(Mono.just(session.textMessage(JSONUtil.toJsonStr(request))))
140 140 .thenMany(session.receive()
... ... @@ -145,6 +145,5 @@ public class XingHuoApi {
145 145 .then())
146 146 .subscribe(); // 订阅以开始会话
147 147 });
148   - return messageFlux;
149 148 }
150 149 }
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/yiyan/YiYanChatClient.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.yiyan;
2   -
3   -import cn.hutool.core.bean.BeanUtil;
4   -import cn.iocoder.yudao.framework.ai.core.exception.ChatException;
5   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanApi;
6   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanChatCompletionResponse;
7   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanChatCompletionRequest;
8   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.exception.YiYanApiException;
9   -import lombok.extern.slf4j.Slf4j;
10   -import org.springframework.ai.chat.ChatClient;
11   -import org.springframework.ai.chat.ChatResponse;
12   -import org.springframework.ai.chat.Generation;
13   -import org.springframework.ai.chat.StreamingChatClient;
14   -import org.springframework.ai.chat.messages.Message;
15   -import org.springframework.ai.chat.messages.MessageType;
16   -import org.springframework.ai.chat.prompt.ChatOptions;
17   -import org.springframework.ai.chat.prompt.Prompt;
18   -import org.springframework.http.ResponseEntity;
19   -import org.springframework.retry.RetryCallback;
20   -import org.springframework.retry.RetryContext;
21   -import org.springframework.retry.RetryListener;
22   -import org.springframework.retry.support.RetryTemplate;
23   -import org.springframework.util.Assert;
24   -import reactor.core.publisher.Flux;
25   -
26   -import java.time.Duration;
27   -import java.util.List;
28   -import java.util.stream.Collectors;
29   -
30   -/**
31   - * 文心一言的 {@link ChatClient} 实现类
32   - *
33   - * @author fansili
34   - */
35   -@Slf4j
36   -public class YiYanChatClient implements ChatClient, StreamingChatClient {
37   -
38   - private final YiYanApi yiYanApi;
39   -
40   - private YiYanChatOptions defaultOptions;
41   -
42   - // TODO @fan:参考 OpenAiChatClient 调整下 retryTemplate;使用 RetryUtils.DEFAULT_RETRY_TEMPLATE;加允许传入?
43   -
44   - public YiYanChatClient(YiYanApi yiYanApi) {
45   - this.yiYanApi = yiYanApi;
46   - // TODO @fan:这个情况,是不是搞个 defaultOptions;OpenAiChatOptions.builder().withModel(OpenAiApi.DEFAULT_CHAT_MODEL).withTemperature(0.7f).build()
47   - }
48   -
49   - public YiYanChatClient(YiYanApi yiYanApi, YiYanChatOptions defaultOptions) {
50   - Assert.notNull(yiYanApi, "OllamaApi must not be null");
51   - Assert.notNull(defaultOptions, "DefaultOptions must not be null");
52   - this.yiYanApi = yiYanApi;
53   - this.defaultOptions = defaultOptions;
54   - }
55   -
56   - public final RetryTemplate retryTemplate = RetryTemplate.builder()
57   - .maxAttempts(10)
58   - .retryOn(YiYanApiException.class)
59   - .exponentialBackoff(Duration.ofMillis(3000), 2, Duration.ofMillis(3 * 60000))
60   - .withListener(new RetryListener() {
61   -
62   - @Override
63   - public <T, E extends Throwable> void onError(RetryContext context,
64   - RetryCallback<T, E> callback, Throwable throwable) {
65   - log.warn("重试异常:" + context.getRetryCount(), throwable);
66   - }
67   -
68   - })
69   - .build();
70   -
71   - @Override
72   - public ChatResponse call(Prompt prompt) {
73   - YiYanChatCompletionRequest request = createRequest(prompt, false);
74   - return this.retryTemplate.execute(ctx -> {
75   - // 发送请求
76   - ResponseEntity<YiYanChatCompletionResponse> response = yiYanApi.chatCompletionEntity(request);
77   - // 获取结果封装 ChatResponse
78   - YiYanChatCompletionResponse chatCompletion = response.getBody();
79   - // TODO @fan:为空时,参考 OpenAiChatClient 的封装;
80   - // TODO @fan:chatResponseMetadata,参考 OpenAiChatResponseMetadata.from(completionEntity.getBody())
81   - return new ChatResponse(List.of(new Generation(chatCompletion.getResult())));
82   - });
83   - }
84   -
85   - @Override
86   - public Flux<ChatResponse> stream(Prompt prompt) {
87   - YiYanChatCompletionRequest request = this.createRequest(prompt, true);
88   - // TODO done @fan:return this.retryTemplate.execute(ctx -> {
89   - return retryTemplate.execute(ctx -> {
90   - // 调用 callWithFunctionSupport 发送请求
91   - Flux<YiYanChatCompletionResponse> response = this.yiYanApi.chatCompletionStream(request);
92   - return response.map(chunk -> {
93   -// System.err.println("---".concat(chunk.getResult()));
94   - // TODO @fan:ChatResponseMetadata chatResponseMetadata
95   - return new ChatResponse(List.of(new Generation(chunk.getResult())));
96   - });
97   - });
98   - }
99   -
100   -
101   -
102   - private YiYanChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
103   - // 参考 https://cloud.baidu.com/doc/WENXINWORKSHOP/s/clntwmv7t 文档,system 是独立字段
104   - // 1.1 获取 user 和 assistant
105   - List<YiYanChatCompletionRequest.Message> messageList = prompt.getInstructions().stream()
106   - // 过滤 system
107   - .filter(msg -> MessageType.SYSTEM != msg.getMessageType())
108   - .map(message -> new YiYanChatCompletionRequest.Message()
109   - .setRole(message.getMessageType().getValue()).setContent(message.getContent())
110   - ).toList();
111   - // 1.2 获取 system
112   - String systemPrompt = prompt.getInstructions().stream()
113   - .filter(message -> MessageType.SYSTEM == message.getMessageType())
114   - .map(Message::getContent)
115   - .collect(Collectors.joining());
116   -
117   - // 3. 创建 request
118   - YiYanChatCompletionRequest request = new YiYanChatCompletionRequest(messageList);
119   - // 复制 YiYanOptions 属性,到 request 中(这里 options 属性和 request 基本保持一致)
120   - YiYanChatOptions useOptions = getYiYanOptions(prompt);
121   - BeanUtil.copyProperties(useOptions, request);
122   - request.setTopP(useOptions.getTopP())
123   - .setMaxOutputTokens(useOptions.getMaxOutputTokens())
124   - .setTemperature(useOptions.getTemperature())
125   - .setSystem(systemPrompt)
126   - .setStream(stream);
127   - return request;
128   - }
129   -
130   - // TODO @fan:Options 的处理,参考下 OpenAiChatClient 的 createRequest
131   - private YiYanChatOptions getYiYanOptions(Prompt prompt) {
132   - // 两个都为null 则没有配置文件
133   - if (defaultOptions == null && prompt.getOptions() == null) {
134   - // TODO @fan:IllegalArgumentException 参数更好哈
135   - throw new ChatException("ChatOptions 未配置参数!");
136   - }
137   - // 优先使用 Prompt 里面的 ChatOptions
138   - ChatOptions options = defaultOptions;
139   - if (prompt.getOptions() != null) {
140   - options = (ChatOptions) prompt.getOptions();
141   - }
142   - // Prompt 里面是一个 ChatOptions,用户可以随意传入,这里做一下判断
143   - if (!(options instanceof YiYanChatOptions)) {
144   - // TODO @fan:IllegalArgumentException 参数更好哈
145   - // TODO @fan:需要兼容 ChatOptionsBuilder 创建出来的
146   - throw new ChatException("Prompt 传入的不是 YiYanOptions!");
147   - }
148   - // 转换 YiYanOptions
149   - return (YiYanChatOptions) options;
150   - }
151   -
152   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/yiyan/YiYanChatOptions.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.yiyan;
2   -
3   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanChatCompletionRequest;
4   -import lombok.Data;
5   -import org.springframework.ai.chat.prompt.ChatOptions;
6   -
7   -import java.util.List;
8   -
9   -/**
10   - * 文心一言的 {@link ChatOptions} 实现类
11   - *
12   - * 字段说明:参考 <a href="https://cloud.baidu.com/doc/WENXINWORKSHOP/s/clntwmv7t">ERNIE-4.0-8K</a>
13   - *
14   - * @author fansili
15   - */
16   -@Data
17   -public class YiYanChatOptions implements ChatOptions {
18   -
19   - /**
20   - * functions 函数
21   - */
22   - private List<YiYanChatCompletionRequest.Function> functions;
23   - /**
24   - * temperature
25   - */
26   - private Float temperature;
27   - /**
28   - * topP
29   - */
30   - private Float topP;
31   - /**
32   - * 通过对已生成的token增加惩罚,减少重复生成的现象
33   - */
34   - private Float penaltyScore;
35   - /**
36   - * stream 模式请求
37   - */
38   - private Boolean stream;
39   - /**
40   - * system 提示
41   - */
42   - private String system;
43   - /**
44   - * 生成停止标识,当模型生成结果以stop中某个元素结尾时,停止文本生成
45   - */
46   - private List<String> stop;
47   - /**
48   - * 是否强制关闭实时搜索功能
49   - */
50   - private Boolean disableSearch;
51   - /**
52   - * 是否开启上角标返回
53   - */
54   - private Boolean enableCitation;
55   - /**
56   - * 输出最大 token
57   - */
58   - private Integer maxOutputTokens;
59   - /**
60   - * 响应格式 text、json_object
61   - */
62   - private String responseFormat;
63   - /**
64   - * 用户id
65   - */
66   - private String userId;
67   - /**
68   - * 在函数调用场景下,提示大模型选择指定的函数(非强制),说明:指定的函数名必须在functions中存在
69   - * tip: ERNIE-4.0-8K 模型没有这个字段
70   - */
71   - private String toolChoice;
72   -
73   - @Override
74   - public Float getTemperature() {
75   - return this.temperature;
76   - }
77   -
78   - @Override
79   - public Float getTopP() {
80   - return topP;
81   - }
82   -
83   - /**
84   - * 百度么有 topK
85   - */
86   - @Override
87   - public Integer getTopK() {
88   - return null;
89   - }
90   -
91   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/yiyan/api/YiYanApi.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.yiyan.api;
2   -
3   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.exception.YiYanApiException;
4   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
5   -import org.springframework.http.HttpStatusCode;
6   -import org.springframework.http.ResponseEntity;
7   -import org.springframework.web.reactive.function.client.WebClient;
8   -import reactor.core.publisher.Flux;
9   -import reactor.core.publisher.Mono;
10   -
11   -/**
12   - * 文心一言 API
13   - *
14   - * @author fansili
15   - */
16   -public class YiYanApi {
17   -
18   - private static final String DEFAULT_BASE_URL = "https://aip.baidubce.com";
19   -
20   - private static final String AUTH_2_TOKEN_URI = "/oauth/2.0/token";
21   -
22   - public static final YiYanChatModel DEFAULT_CHAT_MODEL = YiYanChatModel.ERNIE4_0;
23   -
24   - private final String appKey;
25   - private final String secretKey;
26   - /**
27   - * TODO fan:这个是不是要有个刷新机制哈;如果目前不需要,可以删除掉 refreshTokenSecondTime;整体更简洁;
28   - */
29   - private final String token;
30   - /**
31   - * token 刷新时间(秒)
32   - */
33   - private int refreshTokenSecondTime;
34   - /**
35   - * 发送请求 webClient
36   - */
37   - private final WebClient webClient;
38   - /**
39   - * 使用的模型
40   - */
41   - private final YiYanChatModel useChatModel;
42   -
43   - // TODO fan:看看是不是去掉 refreshTokenSecondTime 字段
44   - public YiYanApi(String appKey, String secretKey, YiYanChatModel useChatModel, int refreshTokenSecondTime) {
45   - this.appKey = appKey;
46   - this.secretKey = secretKey;
47   - this.useChatModel = useChatModel;
48   - this.refreshTokenSecondTime = refreshTokenSecondTime;
49   - this.webClient = WebClient.builder().baseUrl(DEFAULT_BASE_URL).build();
50   - // 获取访问令牌
51   - token = getToken();
52   - }
53   -
54   - /**
55   - * 获得访问令牌
56   - *
57   - * @see <a href="https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Ilkkrb0i5">文档地址</>
58   - * @return 访问令牌
59   - */
60   - private String getToken() {
61   - ResponseEntity<YiYanAuthResponse> response = this.webClient.post()
62   - .uri(uriBuilder -> uriBuilder.path(AUTH_2_TOKEN_URI)
63   - .queryParam("grant_type", "client_credentials")
64   - .queryParam("client_id", appKey)
65   - .queryParam("client_secret", secretKey)
66   - .build()
67   - )
68   - .retrieve()
69   - .toEntity(YiYanAuthResponse.class)
70   - .block();
71   - // 检查请求状态
72   - // TODO @fan:可以使用 response.getStatusCode().is2xxSuccessful()
73   - if (HttpStatusCode.valueOf(200) != response.getStatusCode()
74   - || response.getBody() == null) {
75   - // TODO @fan:可以使用 IllegalStateException 替代;另外,最好打印下返回;方便排错;
76   - throw new YiYanApiException("一言认证失败! api:https://aip.baidubce.com/oauth/2.0/token 请检查 client_id、client_secret 是否正确!");
77   - }
78   - return response.getBody().getAccess_token();
79   - }
80   -
81   - public ResponseEntity<YiYanChatCompletionResponse> chatCompletionEntity(YiYanChatCompletionRequest request) {
82   - // TODO: 2024/3/10 小范 这里错误信息返回的结构不一样
83   -// {"error_code":17,"error_msg":"Open api daily request limit reached"}
84   - return this.webClient.post()
85   - .uri(uriBuilder
86   - -> uriBuilder.path(useChatModel.getUri())
87   - .queryParam("access_token", token)
88   - .build())
89   - .body(Mono.just(JsonUtils.toJsonString(request)), String.class)
90   - .retrieve()
91   - .toEntity(YiYanChatCompletionResponse.class)
92   - .block();
93   - }
94   -
95   - public Flux<YiYanChatCompletionResponse> chatCompletionStream(YiYanChatCompletionRequest request) {
96   - return this.webClient.post()
97   - .uri(uriBuilder
98   - -> uriBuilder.path(useChatModel.getUri())
99   - .queryParam("access_token", token)
100   - .build())
101   - .body(Mono.just(request), YiYanChatCompletionRequest.class)
102   - .retrieve()
103   - .bodyToFlux(YiYanChatCompletionResponse.class);
104   - }
105   -
106   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/yiyan/api/YiYanAuthResponse.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.yiyan.api;
2   -
3   -import lombok.Data;
4   -
5   -// TODO @fan:字段驼峰;字段注释都可以删除,贴个链接就好;
6   -/**
7   - * 获取文心一言的 access_token 的 Response
8   - *
9   - * @author fansili
10   - */
11   -@Data
12   -public class YiYanAuthResponse {
13   -
14   - /**
15   - * 访问凭证
16   - */
17   - private String access_token;
18   - /**
19   - * 有效期,Access Token的有效期。
20   - * 说明:单位是秒,有效期30天
21   - */
22   - private int expires_in;
23   - /**
24   - * 错误码,说明:响应失败时返回该字段,成功时不返回
25   - */
26   - private String error;
27   - /**
28   - * 错误描述信息,帮助理解和解决发生的错误
29   - * 说明:响应失败时返回该字段,成功时不返回
30   - */
31   - private String error_description;
32   - /**
33   - * 暂时未使用,可忽略
34   - */
35   - private String session_key;
36   - /**
37   - * 暂时未使用,可忽略
38   - */
39   - private String refresh_token;
40   - /**
41   - * 暂时未使用,可忽略
42   - */
43   - private String scope;
44   - /**
45   - * 暂时未使用,可忽略
46   - */
47   - private String session_secret;
48   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/yiyan/api/YiYanChatCompletionRequest.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.yiyan.api;
2   -
3   -import com.fasterxml.jackson.annotation.JsonProperty;
4   -import lombok.Data;
5   -
6   -import java.util.List;
7   -
8   -/**
9   - * 文心一言 Completion Request
10   - *
11   - * 百度千帆文档:https://cloud.baidu.com/doc/WENXINWORKSHOP/s/jlil56u11
12   - *
13   - * @author fansili
14   - */
15   -@Data
16   -public class YiYanChatCompletionRequest {
17   -
18   - public YiYanChatCompletionRequest(List<Message> messages) {
19   - this.messages = messages;
20   - }
21   -
22   - /**
23   - * 聊天上下文信息
24   - */
25   - private List<Message> messages;
26   - /**
27   - * functions 函数
28   - */
29   - private List<Function> functions;
30   - /**
31   - * temperature
32   - */
33   - private Float temperature;
34   - /**
35   - * topP
36   - */
37   - @JsonProperty("top_p")
38   - private Float topP;
39   - /**
40   - * 通过对已生成的token增加惩罚,减少重复生成的现象
41   - */
42   - @JsonProperty("penalty_score")
43   - private Float penaltyScore;
44   - /**
45   - * stream 模式
46   - */
47   - private Boolean stream;
48   - /**
49   - * system 预设角色
50   - */
51   - private String system;
52   - /**
53   - * 生成停止标识,当模型生成结果以stop中某个元素结尾时,停止文本生成
54   - */
55   - private List<String> stop;
56   - /**
57   - * 是否强制关闭实时搜索功能
58   - */
59   - @JsonProperty("disable_search")
60   - private Boolean disableSearch;
61   - /**
62   - * 是否开启上角标返回
63   - */
64   - @JsonProperty("enable_citation")
65   - private Boolean enableCitation;
66   - /**
67   - * 最大输出 token 数
68   - */
69   - @JsonProperty("max_output_tokens")
70   - private Integer maxOutputTokens;
71   - /**
72   - * 返回格式 text、json_object
73   - */
74   - @JsonProperty("response_format")
75   - private String responseFormat;
76   - /**
77   - * 用户 id
78   - */
79   - @JsonProperty("user_id")
80   - private String userId;
81   - /**
82   - * 在函数调用场景下,提示大模型选择指定的函数(非强制),说明:指定的函数名必须在functions中存在
83   - * tip: ERNIE-4.0-8K 模型没有这个字段
84   - */
85   - @JsonProperty("tool_choice")
86   - private String toolChoice;
87   -
88   -
89   - @Data
90   - public static class Message {
91   -
92   - private String role;
93   -
94   - private String content;
95   -
96   - }
97   -
98   - @Data
99   - public static class ToolChoice {
100   - /**
101   - * 指定工具类型,function
102   - * 必填: 是
103   - */
104   - private String type;
105   - /**
106   - * 指定要使用的函数
107   - * 必填: 是
108   - */
109   - private Function function;
110   - /**
111   - * 指定要使用的函数名
112   - * 必填: 是
113   - */
114   - private String name;
115   - }
116   -
117   - @Data
118   - public static class Function {
119   - /**
120   - * 函数名
121   - * 必填: 是
122   - */
123   - private String name;
124   - /**
125   - * 函数描述
126   - * 必填: 是
127   - */
128   - private String description;
129   - /**
130   - * 函数请求参数,说明:
131   - * (1)JSON Schema 格式,参考JSON Schema描述
132   - * (2)如果函数没有请求参数,parameters值格式如下:
133   - * {"type": "object","properties": {}}
134   - * 必填: 是
135   - */
136   - private String parameters;
137   - /**
138   - * 函数响应参数,JSON Schema 格式,参考JSON Schema描述
139   - * 必填: 否
140   - */
141   - private String responses;
142   - /**
143   - * function调用的一些历史示例,说明:
144   - * (1)可以提供正例(正常触发)和反例(无需触发)的example
145   - * ·正例:从历史请求数据中获取
146   - * ·反例:
147   - * 当role = user,不会触发请求的query
148   - * 当role = assistant,有固定的格式。function_call的name为空,arguments是空对象:"{}",thought可以填固定的:"我不需要调用任何工具"
149   - * (2)兼容之前的 List(example) 格式
150   - */
151   - private String examples;
152   - }
153   -
154   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/yiyan/api/YiYanChatCompletionResponse.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.yiyan.api;
2   -
3   -import lombok.Data;
4   -
5   -/**
6   - * 文心一言 Completion Response
7   - *
8   - * 百度链接: https://cloud.baidu.com/doc/WENXINWORKSHOP/s/clntwmv7t
9   - *
10   - * @author fansili
11   - */
12   -@Data
13   -public class YiYanChatCompletionResponse {
14   -
15   - /**
16   - * 本轮对话的id
17   - */
18   - private String id;
19   - /**
20   - * 回包类型,chat.completion:多轮对话返回
21   - */
22   - private String object;
23   - /**
24   - * 时间戳
25   - */
26   - private int created;
27   - /**
28   - * 表示当前子句的序号。只有在流式接口模式下会返回该字段
29   - */
30   - private int sentence_id;
31   - /**
32   - * 表示当前子句是否是最后一句。只有在流式接口模式下会返回该字段
33   - */
34   - private boolean is_end;
35   - /**
36   - * 当前生成的结果是否被截断
37   - */
38   - private boolean is_truncated;
39   - /**
40   - * 输出内容标识,说明:
41   - * · normal:输出内容完全由大模型生成,未触发截断、替换
42   - * · stop:输出结果命中入参stop中指定的字段后被截断
43   - * · length:达到了最大的token数,根据EB返回结果is_truncated来截断
44   - * · content_filter:输出内容被截断、兜底、替换为**等
45   - */
46   - private String finish_reason;
47   - /**
48   - * 搜索数据,当请求参数enable_citation为true并且触发搜索时,会返回该字段
49   - */
50   - private String search_info;
51   - /**
52   - * 对话返回结果
53   - */
54   - private String result;
55   - /**
56   - * 表示用户输入是否存在安全,是否关闭当前会话,清理历史会话信息
57   - * true:是,表示用户输入存在安全风险,建议关闭当前会话,清理历史会话信息
58   - * false:否,表示用户输入无安全风险
59   - */
60   - private boolean need_clear_history;
61   - /**
62   - * 说明:
63   - * · 0:正常返回
64   - * · 其他:非正常
65   - */
66   - private int flag;
67   - /**
68   - * 当need_clear_history为true时,此字段会告知第几轮对话有敏感信息,如果是当前问题,ban_round=-1
69   - */
70   - private int ban_round;
71   - /**
72   - * token统计信息
73   - */
74   - private Usage usage;
75   -
76   - @Data
77   - public static class Usage {
78   - /**
79   - * 问题tokens数
80   - */
81   - private int prompt_tokens;
82   - /**
83   - * 回答tokens数
84   - */
85   - private int completion_tokens;
86   - /**
87   - * tokens总数
88   - */
89   - private int total_tokens;
90   - }
91   -
92   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/yiyan/api/YiYanChatModel.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.yiyan.api;
2   -
3   -import lombok.AllArgsConstructor;
4   -import lombok.Getter;
5   -
6   -/**
7   - * 文心一言的模型枚举
8   - *
9   - * 可参考 <a href="https://cloud.baidu.com/doc/WENXINWORKSHOP/s/clntwmv7t">百度文档</>
10   - *
11   - * @author fansili
12   - */
13   -@Getter
14   -@AllArgsConstructor
15   -public enum YiYanChatModel {
16   -
17   - ERNIE4_0("ERNIE 4.0", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro"),
18   - ERNIE4_3_5_8K("ERNIE-3.5-8K", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions"),
19   - ERNIE4_3_5_8K_0205("ERNIE-3.5-8K-0205", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-3.5-8k-0205"),
20   - ERNIE4_3_5_8K_1222("ERNIE-3.5-8K-1222", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-3.5-8k-1222"),
21   - ERNIE4_BOT_8K("ERNIE-Bot-8K", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie_bot_8k"),
22   - ERNIE4_3_5_4K_0205("ERNIE-3.5-4K-0205", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-3.5-4k-0205"),
23   - ;
24   -
25   - /**
26   - * 模型名
27   - */
28   - private final String model;
29   - /**
30   - * API URL
31   - */
32   - private final String uri;
33   -
34   - public static YiYanChatModel valueOfModel(String model) {
35   - for (YiYanChatModel modelEnum : YiYanChatModel.values()) {
36   - if (modelEnum.getModel().equals(model)) {
37   - return modelEnum;
38   - }
39   - }
40   - throw new IllegalArgumentException("Invalid MessageType value: " + model);
41   - }
42   -}
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/yiyan/exception/YiYanApiException.java deleted
1   -package cn.iocoder.yudao.framework.ai.core.model.yiyan.exception;
2   -
3   -/**
4   - * 一言 api 调用异常
5   - */
6   -public class YiYanApiException extends RuntimeException {
7   -
8   - public YiYanApiException(String message) {
9   - super(message);
10   - }
11   -
12   - public YiYanApiException(String message, Throwable cause) {
13   - super(message, cause);
14   - }
15   -
16   -}
17 0 \ No newline at end of file
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiAutoConfiguration.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.audio.speech.TongYiAudioSpeechModel;
  20 +import com.alibaba.cloud.ai.tongyi.audio.speech.TongYiAudioSpeechProperties;
  21 +import com.alibaba.cloud.ai.tongyi.audio.transcription.TongYiAudioTranscriptionModel;
  22 +import com.alibaba.cloud.ai.tongyi.audio.transcription.TongYiAudioTranscriptionProperties;
  23 +import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel;
  24 +import com.alibaba.cloud.ai.tongyi.chat.TongYiChatProperties;
  25 +import com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants;
  26 +import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException;
  27 +import com.alibaba.cloud.ai.tongyi.embedding.TongYiTextEmbeddingModel;
  28 +import com.alibaba.cloud.ai.tongyi.embedding.TongYiTextEmbeddingProperties;
  29 +import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel;
  30 +import com.alibaba.cloud.ai.tongyi.image.TongYiImagesProperties;
  31 +import com.alibaba.dashscope.aigc.generation.Generation;
  32 +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
  33 +import com.alibaba.dashscope.audio.asr.transcription.Transcription;
  34 +import com.alibaba.dashscope.audio.tts.SpeechSynthesizer;
  35 +import com.alibaba.dashscope.common.MessageManager;
  36 +import com.alibaba.dashscope.embeddings.TextEmbedding;
  37 +import com.alibaba.dashscope.exception.NoApiKeyException;
  38 +import com.alibaba.dashscope.utils.ApiKey;
  39 +import com.alibaba.dashscope.utils.Constants;
  40 +import org.springframework.ai.model.function.FunctionCallbackContext;
  41 +import org.springframework.boot.autoconfigure.AutoConfiguration;
  42 +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
  43 +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
  44 +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  45 +import org.springframework.boot.context.properties.EnableConfigurationProperties;
  46 +import org.springframework.context.ApplicationContext;
  47 +import org.springframework.context.annotation.Bean;
  48 +import org.springframework.context.annotation.Scope;
  49 +
  50 +import java.util.Objects;
  51 +
  52 +/**
  53 + * @author yuluo
  54 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  55 + * @since 2023.0.1.0
  56 + */
  57 +
  58 +@AutoConfiguration
  59 +@ConditionalOnClass({
  60 + MessageManager.class,
  61 + TongYiChatModel.class,
  62 + TongYiImagesModel.class,
  63 + TongYiAudioSpeechModel.class,
  64 + TongYiTextEmbeddingModel.class,
  65 + TongYiAudioTranscriptionModel.class
  66 +})
  67 +@EnableConfigurationProperties({
  68 + TongYiChatProperties.class,
  69 + TongYiImagesProperties.class,
  70 + TongYiAudioSpeechProperties.class,
  71 + TongYiConnectionProperties.class,
  72 + TongYiTextEmbeddingProperties.class,
  73 + TongYiAudioTranscriptionProperties.class
  74 +})
  75 +public class TongYiAutoConfiguration {
  76 +
  77 + @Bean
  78 + @Scope("prototype")
  79 + @ConditionalOnMissingBean
  80 + public Generation generation() {
  81 +
  82 + return new Generation();
  83 + }
  84 +
  85 + @Bean
  86 + @Scope("prototype")
  87 + @ConditionalOnMissingBean
  88 + public MessageManager msgManager() {
  89 +
  90 + return new MessageManager(10);
  91 + }
  92 +
  93 + @Bean
  94 + @Scope("prototype")
  95 + @ConditionalOnMissingBean
  96 + public ImageSynthesis imageSynthesis() {
  97 +
  98 + return new ImageSynthesis();
  99 + }
  100 +
  101 + @Bean
  102 + @Scope("prototype")
  103 + @ConditionalOnMissingBean
  104 + public SpeechSynthesizer speechSynthesizer() {
  105 +
  106 + return new SpeechSynthesizer();
  107 + }
  108 +
  109 + @Bean
  110 + @ConditionalOnMissingBean
  111 + public Transcription transcription() {
  112 +
  113 + return new Transcription();
  114 + }
  115 +
  116 + @Bean
  117 + @ConditionalOnMissingBean
  118 + public TextEmbedding textEmbedding() {
  119 +
  120 + return new TextEmbedding();
  121 + }
  122 +
  123 + @Bean
  124 + @ConditionalOnMissingBean
  125 + public FunctionCallbackContext springAiFunctionManager(ApplicationContext context) {
  126 +
  127 + FunctionCallbackContext manager = new FunctionCallbackContext();
  128 + manager.setApplicationContext(context);
  129 +
  130 + return manager;
  131 + }
  132 +
  133 + @Bean
  134 + @ConditionalOnProperty(
  135 + prefix = TongYiChatProperties.CONFIG_PREFIX,
  136 + name = "enabled",
  137 + havingValue = "true",
  138 + matchIfMissing = true
  139 + )
  140 + public TongYiChatModel tongYiChatClient(Generation generation,
  141 + TongYiChatProperties chatOptions,
  142 + TongYiConnectionProperties connectionProperties
  143 + ) {
  144 +
  145 + settingApiKey(connectionProperties);
  146 +
  147 + return new TongYiChatModel(generation, chatOptions.getOptions());
  148 + }
  149 +
  150 + @Bean
  151 + @ConditionalOnProperty(
  152 + prefix = TongYiImagesProperties.CONFIG_PREFIX,
  153 + name = "enabled",
  154 + havingValue = "true",
  155 + matchIfMissing = true
  156 + )
  157 + public TongYiImagesModel tongYiImagesClient(
  158 + ImageSynthesis imageSynthesis,
  159 + TongYiImagesProperties imagesOptions,
  160 + TongYiConnectionProperties connectionProperties
  161 + ) {
  162 +
  163 + settingApiKey(connectionProperties);
  164 +
  165 + return new TongYiImagesModel(imageSynthesis, imagesOptions.getOptions());
  166 + }
  167 +
  168 + @Bean
  169 + @ConditionalOnProperty(
  170 + prefix = TongYiAudioSpeechProperties.CONFIG_PREFIX,
  171 + name = "enabled",
  172 + havingValue = "true",
  173 + matchIfMissing = true
  174 + )
  175 + public TongYiAudioSpeechModel tongYiAudioSpeechClient(
  176 + SpeechSynthesizer speechSynthesizer,
  177 + TongYiAudioSpeechProperties speechProperties,
  178 + TongYiConnectionProperties connectionProperties
  179 + ) {
  180 +
  181 + settingApiKey(connectionProperties);
  182 +
  183 + return new TongYiAudioSpeechModel(speechSynthesizer, speechProperties.getOptions());
  184 + }
  185 +
  186 + @Bean
  187 + @ConditionalOnProperty(
  188 + prefix = TongYiAudioTranscriptionProperties.CONFIG_PREFIX,
  189 + name = "enabled",
  190 + havingValue = "true",
  191 + matchIfMissing = true
  192 + )
  193 + public TongYiAudioTranscriptionModel tongYiAudioTranscriptionClient(
  194 + Transcription transcription,
  195 + TongYiAudioTranscriptionProperties transcriptionProperties,
  196 + TongYiConnectionProperties connectionProperties) {
  197 +
  198 + settingApiKey(connectionProperties);
  199 +
  200 + return new TongYiAudioTranscriptionModel(
  201 + transcriptionProperties.getOptions(),
  202 + transcription
  203 + );
  204 + }
  205 +
  206 + @Bean
  207 + @ConditionalOnProperty(
  208 + prefix = TongYiTextEmbeddingProperties.CONFIG_PREFIX,
  209 + name = "enabled",
  210 + havingValue = "true",
  211 + matchIfMissing = true
  212 + )
  213 + public TongYiTextEmbeddingModel tongYiTextEmbeddingClient(
  214 + TextEmbedding textEmbedding,
  215 + TongYiConnectionProperties connectionProperties
  216 + ) {
  217 +
  218 + settingApiKey(connectionProperties);
  219 + return new TongYiTextEmbeddingModel(textEmbedding);
  220 + }
  221 +
  222 + /**
  223 + * Setting the API key.
  224 + * @param connectionProperties {@link TongYiConnectionProperties}
  225 + */
  226 + private void settingApiKey(TongYiConnectionProperties connectionProperties) {
  227 +
  228 + String apiKey;
  229 +
  230 + try {
  231 + // It is recommended to set the key by defining the api-key in an environment variable.
  232 + var envKey = System.getenv(TongYiConstants.SCA_AI_TONGYI_API_KEY);
  233 + if (Objects.nonNull(envKey)) {
  234 + Constants.apiKey = envKey;
  235 + return;
  236 + }
  237 + if (Objects.nonNull(connectionProperties.getApiKey())) {
  238 + apiKey = connectionProperties.getApiKey();
  239 + }
  240 + else {
  241 + apiKey = ApiKey.getApiKey(null);
  242 + }
  243 +
  244 + Constants.apiKey = apiKey;
  245 + }
  246 + catch (NoApiKeyException e) {
  247 +
  248 + throw new TongYiException(e.getMessage());
  249 + }
  250 +
  251 + }
  252 +
  253 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiConnectionProperties.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi;
  18 +
  19 +import org.springframework.boot.context.properties.ConfigurationProperties;
  20 +
  21 +import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION;
  22 +
  23 +/**
  24 + * Spring Cloud Alibaba AI TongYi LLM connection properties.
  25 + *
  26 + * @author yuluo
  27 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  28 + * @since 2023.0.1.0
  29 + */
  30 +
  31 +@ConfigurationProperties(TongYiConnectionProperties.CONFIG_PREFIX)
  32 +public class TongYiConnectionProperties {
  33 +
  34 + /**
  35 + * Spring Cloud Alibaba AI connection configuration Prefix.
  36 + */
  37 + public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "tongyi";
  38 +
  39 + /**
  40 + * TongYi LLM API key.
  41 + */
  42 + private String apiKey;
  43 +
  44 + public String getApiKey() {
  45 + return apiKey;
  46 + }
  47 +
  48 + public void setApiKey(String apiKey) {
  49 + this.apiKey = apiKey;
  50 + }
  51 +
  52 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioSpeechModels.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio;
  18 +
  19 +/**
  20 + * More models see: https://help.aliyun.com/zh/dashscope/model-list?spm=a2c4g.11186623.0.i5
  21 + * Support all models in list.
  22 + *
  23 + * @author yuluo
  24 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  25 + * @since 2023.0.1.0
  26 + */
  27 +
  28 +public final class AudioSpeechModels {
  29 +
  30 + private AudioSpeechModels() {
  31 + }
  32 +
  33 + /**
  34 + * Male Voice of the Tongue(舌尖男声).
  35 + * zh & en.
  36 + * Default sample rate: 48 Hz.
  37 + */
  38 + public static final String SAMBERT_ZHICHU_V1 = "sambert-zhichu-v1";
  39 +
  40 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioTranscriptionModels.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio;
  18 +
  19 +/**
  20 + * @author xYLiu
  21 + * @author yuluo
  22 + * @since 2023.0.1.0
  23 + */
  24 +
  25 +public final class AudioTranscriptionModels {
  26 +
  27 + private AudioTranscriptionModels() {
  28 + }
  29 +
  30 + /**
  31 + * Paraformer Chinese and English speech recognition model supports audio or video speech recognition with a sampling rate of 16kHz or above.
  32 + */
  33 + public static final String Paraformer_V1 = "paraformer-v1";
  34 + /**
  35 + * Paraformer Chinese speech recognition model, support 8kHz telephone speech recognition.
  36 + */
  37 + public static final String Paraformer_8K_V1 = "paraformer-8k-v1";
  38 + /**
  39 + * The Paraformer multilingual speech recognition model supports audio or video speech recognition with a sample rate of 16kHz or above.
  40 + */
  41 + public static final String Paraformer_MTL_V1 = "paraformer-mtl-v1";
  42 +
  43 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechModel.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.speech;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.audio.AudioSpeechModels;
  20 +import com.alibaba.cloud.ai.tongyi.audio.speech.api.*;
  21 +import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioSpeechResponseMetadata;
  22 +import com.alibaba.dashscope.audio.tts.SpeechSynthesisParam;
  23 +import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult;
  24 +import com.alibaba.dashscope.audio.tts.SpeechSynthesizer;
  25 +import com.alibaba.dashscope.common.ResultCallback;
  26 +import io.reactivex.Flowable;
  27 +import org.slf4j.Logger;
  28 +import org.slf4j.LoggerFactory;
  29 +import org.springframework.util.Assert;
  30 +import reactor.core.publisher.Flux;
  31 +import reactor.core.scheduler.Schedulers;
  32 +
  33 +import java.nio.ByteBuffer;
  34 +
  35 +/**
  36 + * TongYiAudioSpeechClient is a client for TongYi audio speech service for Spring Cloud Alibaba AI.
  37 + *
  38 + * @author yuluo
  39 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  40 + * @since 2023.0.1.0
  41 + */
  42 +
  43 +public class TongYiAudioSpeechModel implements SpeechModel, SpeechStreamModel {
  44 +
  45 + private final Logger logger = LoggerFactory.getLogger(getClass());
  46 +
  47 + /**
  48 + * Default speed rate.
  49 + */
  50 + private static final float SPEED_RATE = 1.0f;
  51 +
  52 + /**
  53 + * TongYi models api.
  54 + */
  55 + private final SpeechSynthesizer speechSynthesizer;
  56 +
  57 + /**
  58 + * TongYi models options.
  59 + */
  60 + private final TongYiAudioSpeechOptions defaultOptions;
  61 +
  62 + /**
  63 + * TongYiAudioSpeechClient constructor.
  64 + * @param speechSynthesizer the speech synthesizer
  65 + */
  66 + public TongYiAudioSpeechModel(SpeechSynthesizer speechSynthesizer) {
  67 +
  68 + this(speechSynthesizer, null);
  69 + }
  70 +
  71 + /**
  72 + * TongYiAudioSpeechClient constructor.
  73 + * @param speechSynthesizer the speech synthesizer
  74 + * @param tongYiAudioOptions the tongYi audio options
  75 + */
  76 + public TongYiAudioSpeechModel(SpeechSynthesizer speechSynthesizer, TongYiAudioSpeechOptions tongYiAudioOptions) {
  77 +
  78 + Assert.notNull(speechSynthesizer, "speechSynthesizer must not be null");
  79 + Assert.notNull(tongYiAudioOptions, "tongYiAudioOptions must not be null");
  80 +
  81 + this.speechSynthesizer = speechSynthesizer;
  82 + this.defaultOptions = tongYiAudioOptions;
  83 + }
  84 +
  85 + /**
  86 + * Call the TongYi audio speech service.
  87 + * @param text the text message to be converted to audio.
  88 + * @return the audio byte buffer.
  89 + */
  90 + @Override
  91 + public ByteBuffer call(String text) {
  92 +
  93 + var speechRequest = new SpeechPrompt(text);
  94 +
  95 + return call(speechRequest).getResult().getOutput();
  96 + }
  97 +
  98 +
  99 + /**
  100 + * Call the TongYi audio speech service.
  101 + * @param prompt the speech prompt.
  102 + * @return the speech response.
  103 + */
  104 + @Override
  105 + public SpeechResponse call(SpeechPrompt prompt) {
  106 +
  107 + var SCASpeechParam = merge(prompt.getOptions());
  108 + var speechSynthesisParams = toSpeechSynthesisParams(SCASpeechParam);
  109 + speechSynthesisParams.setText(prompt.getInstructions().getText());
  110 + logger.info(speechSynthesisParams.toString());
  111 +
  112 + var res = speechSynthesizer.call(speechSynthesisParams);
  113 +
  114 + return convert(res, null);
  115 + }
  116 +
  117 + /**
  118 + * Call the TongYi audio speech service.
  119 + * @param prompt the speech prompt.
  120 + * @param callback the result callback.
  121 + * {@link SpeechSynthesizer#call(SpeechSynthesisParam, ResultCallback)}
  122 + */
  123 + public void call(SpeechPrompt prompt, ResultCallback<SpeechSynthesisResult> callback) {
  124 +
  125 + var SCASpeechParam = merge(prompt.getOptions());
  126 + var speechSynthesisParams = toSpeechSynthesisParams(SCASpeechParam);
  127 + speechSynthesisParams.setText(prompt.getInstructions().getText());
  128 +
  129 + speechSynthesizer.call(speechSynthesisParams, callback);
  130 + }
  131 +
  132 + /**
  133 + * Stream the TongYi audio speech service.
  134 + * @param prompt the speech prompt.
  135 + * @return the speech response.
  136 + * {@link SpeechSynthesizer#streamCall(SpeechSynthesisParam)}
  137 + */
  138 + @Override
  139 + public Flux<SpeechResponse> stream(SpeechPrompt prompt) {
  140 +
  141 + var SCASpeechParam = merge(prompt.getOptions());
  142 +
  143 + Flowable<SpeechSynthesisResult> resultFlowable = speechSynthesizer
  144 + .streamCall(toSpeechSynthesisParams(SCASpeechParam));
  145 +
  146 + return Flux.from(resultFlowable)
  147 + .flatMap(
  148 + res -> Flux.just(res.getAudioFrame())
  149 + .map(audio -> {
  150 + var speech = new Speech(audio);
  151 + var respMetadata = TongYiAudioSpeechResponseMetadata.from(res);
  152 + return new SpeechResponse(speech, respMetadata);
  153 + })
  154 + ).publishOn(Schedulers.parallel());
  155 + }
  156 +
  157 + public TongYiAudioSpeechOptions merge(TongYiAudioSpeechOptions target) {
  158 +
  159 + var mergeBuilder = TongYiAudioSpeechOptions.builder();
  160 +
  161 + mergeBuilder.withModel(defaultOptions.getModel() != null ? defaultOptions.getModel() : target.getModel());
  162 + mergeBuilder.withPitch(defaultOptions.getPitch() != null ? defaultOptions.getPitch() : target.getPitch());
  163 + mergeBuilder.withRate(defaultOptions.getRate() != null ? defaultOptions.getRate() : target.getRate());
  164 + mergeBuilder.withFormat(defaultOptions.getFormat() != null ? defaultOptions.getFormat() : target.getFormat());
  165 + mergeBuilder.withSampleRate(defaultOptions.getSampleRate() != null ? defaultOptions.getSampleRate() : target.getSampleRate());
  166 + mergeBuilder.withTextType(defaultOptions.getTextType() != null ? defaultOptions.getTextType() : target.getTextType());
  167 + mergeBuilder.withVolume(defaultOptions.getVolume() != null ? defaultOptions.getVolume() : target.getVolume());
  168 + mergeBuilder.withEnablePhonemeTimestamp(defaultOptions.isEnablePhonemeTimestamp() != null ? defaultOptions.isEnablePhonemeTimestamp() : target.isEnablePhonemeTimestamp());
  169 + mergeBuilder.withEnableWordTimestamp(defaultOptions.isEnableWordTimestamp() != null ? defaultOptions.isEnableWordTimestamp() : target.isEnableWordTimestamp());
  170 +
  171 + return mergeBuilder.build();
  172 + }
  173 +
  174 + public SpeechSynthesisParam toSpeechSynthesisParams(TongYiAudioSpeechOptions source) {
  175 +
  176 + var mergeBuilder = SpeechSynthesisParam.builder();
  177 +
  178 + mergeBuilder.model(source.getModel() != null ? source.getModel() : AudioSpeechModels.SAMBERT_ZHICHU_V1);
  179 + mergeBuilder.text(source.getText() != null ? source.getText() : "");
  180 +
  181 + if (source.getFormat() != null) {
  182 + mergeBuilder.format(source.getFormat());
  183 + }
  184 + if (source.getRate() != null) {
  185 + mergeBuilder.rate(source.getRate());
  186 + }
  187 + if (source.getPitch() != null) {
  188 + mergeBuilder.pitch(source.getPitch());
  189 + }
  190 + if (source.getTextType() != null) {
  191 + mergeBuilder.textType(source.getTextType());
  192 + }
  193 + if (source.getSampleRate() != null) {
  194 + mergeBuilder.sampleRate(source.getSampleRate());
  195 + }
  196 + if (source.isEnablePhonemeTimestamp() != null) {
  197 + mergeBuilder.enablePhonemeTimestamp(source.isEnablePhonemeTimestamp());
  198 + }
  199 + if (source.isEnableWordTimestamp() != null) {
  200 + mergeBuilder.enableWordTimestamp(source.isEnableWordTimestamp());
  201 + }
  202 + if (source.getVolume() != null) {
  203 + mergeBuilder.volume(source.getVolume());
  204 + }
  205 +
  206 + return mergeBuilder.build();
  207 + }
  208 +
  209 + /**
  210 + * Convert the TongYi audio speech service result to the speech response.
  211 + * @param result the audio byte buffer.
  212 + * @param synthesisResult the synthesis result.
  213 + * @return the speech response.
  214 + */
  215 + private SpeechResponse convert(ByteBuffer result, SpeechSynthesisResult synthesisResult) {
  216 +
  217 + if (synthesisResult == null) {
  218 +
  219 + return new SpeechResponse(new Speech(result));
  220 + }
  221 +
  222 + var responseMetadata = TongYiAudioSpeechResponseMetadata.from(synthesisResult);
  223 + var speech = new Speech(synthesisResult.getAudioFrame());
  224 +
  225 + return new SpeechResponse(speech, responseMetadata);
  226 + }
  227 +
  228 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechOptions.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.speech;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.audio.AudioSpeechModels;
  20 +import com.alibaba.dashscope.audio.tts.SpeechSynthesisAudioFormat;
  21 +import com.alibaba.dashscope.audio.tts.SpeechSynthesisTextType;
  22 +import org.springframework.ai.model.ModelOptions;
  23 +
  24 +/**
  25 + * @author yuluo
  26 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  27 + * @since 2023.0.1.0
  28 + */
  29 +
  30 +public class TongYiAudioSpeechOptions implements ModelOptions {
  31 +
  32 +
  33 + /**
  34 + * Audio Speech models.
  35 + */
  36 + private String model = AudioSpeechModels.SAMBERT_ZHICHU_V1;
  37 +
  38 + /**
  39 + * Text content.
  40 + */
  41 + private String text;
  42 +
  43 + /**
  44 + * Input text type.
  45 + */
  46 + private SpeechSynthesisTextType textType = SpeechSynthesisTextType.PLAIN_TEXT;
  47 +
  48 + /**
  49 + * synthesis audio format.
  50 + */
  51 + private SpeechSynthesisAudioFormat format = SpeechSynthesisAudioFormat.WAV;
  52 +
  53 + /**
  54 + * synthesis audio sample rate.
  55 + */
  56 + private Integer sampleRate = 16000;
  57 +
  58 + /**
  59 + * synthesis audio volume.
  60 + */
  61 + private Integer volume = 50;
  62 +
  63 + /**
  64 + * synthesis audio speed.
  65 + */
  66 + private Float rate = 1.0f;
  67 +
  68 + /**
  69 + * synthesis audio pitch.
  70 + */
  71 + private Float pitch = 1.0f;
  72 +
  73 + /**
  74 + * enable word level timestamp.
  75 + */
  76 + private Boolean enableWordTimestamp = false;
  77 +
  78 + /**
  79 + * enable phoneme level timestamp.
  80 + */
  81 + private Boolean enablePhonemeTimestamp = false;
  82 +
  83 + public static Builder builder() {
  84 + return new Builder();
  85 + }
  86 +
  87 + public String getModel() {
  88 +
  89 + return model;
  90 + }
  91 +
  92 + public void setModel(String model) {
  93 +
  94 + this.model = model;
  95 + }
  96 +
  97 + public String getText() {
  98 +
  99 + return text;
  100 + }
  101 +
  102 + public void setText(String text) {
  103 +
  104 + this.text = text;
  105 + }
  106 +
  107 + public SpeechSynthesisTextType getTextType() {
  108 +
  109 + return textType;
  110 + }
  111 +
  112 + public void setTextType(SpeechSynthesisTextType textType) {
  113 +
  114 + this.textType = textType;
  115 + }
  116 +
  117 + public SpeechSynthesisAudioFormat getFormat() {
  118 +
  119 + return format;
  120 + }
  121 +
  122 + public void setFormat(SpeechSynthesisAudioFormat format) {
  123 +
  124 + this.format = format;
  125 + }
  126 +
  127 + public Integer getSampleRate() {
  128 +
  129 + return sampleRate;
  130 + }
  131 +
  132 + public void setSampleRate(Integer sampleRate) {
  133 +
  134 + this.sampleRate = sampleRate;
  135 + }
  136 +
  137 + public Integer getVolume() {
  138 +
  139 + return volume;
  140 + }
  141 +
  142 + public void setVolume(Integer volume) {
  143 +
  144 + this.volume = volume;
  145 + }
  146 +
  147 + public Float getRate() {
  148 +
  149 + return rate;
  150 + }
  151 +
  152 + public void setRate(Float rate) {
  153 +
  154 + this.rate = rate;
  155 + }
  156 +
  157 + public Float getPitch() {
  158 +
  159 + return pitch;
  160 + }
  161 +
  162 + public void setPitch(Float pitch) {
  163 +
  164 + this.pitch = pitch;
  165 + }
  166 +
  167 + public Boolean isEnableWordTimestamp() {
  168 +
  169 + return enableWordTimestamp;
  170 + }
  171 +
  172 + public void setEnableWordTimestamp(Boolean enableWordTimestamp) {
  173 +
  174 + this.enableWordTimestamp = enableWordTimestamp;
  175 + }
  176 +
  177 + public Boolean isEnablePhonemeTimestamp() {
  178 +
  179 + return enablePhonemeTimestamp;
  180 + }
  181 +
  182 + public void setEnablePhonemeTimestamp(Boolean enablePhonemeTimestamp) {
  183 +
  184 + this.enablePhonemeTimestamp = enablePhonemeTimestamp;
  185 + }
  186 +
  187 + /**
  188 + * Build a options instances.
  189 + */
  190 + public static class Builder {
  191 +
  192 + private final TongYiAudioSpeechOptions options = new TongYiAudioSpeechOptions();
  193 +
  194 + public Builder withModel(String model) {
  195 +
  196 + options.model = model;
  197 + return this;
  198 + }
  199 +
  200 + public Builder withText(String text) {
  201 +
  202 + options.text = text;
  203 + return this;
  204 + }
  205 +
  206 + public Builder withTextType(SpeechSynthesisTextType textType) {
  207 +
  208 + options.textType = textType;
  209 + return this;
  210 + }
  211 +
  212 + public Builder withFormat(SpeechSynthesisAudioFormat format) {
  213 +
  214 + options.format = format;
  215 + return this;
  216 + }
  217 +
  218 + public Builder withSampleRate(Integer sampleRate) {
  219 +
  220 + options.sampleRate = sampleRate;
  221 + return this;
  222 + }
  223 +
  224 + public Builder withVolume(Integer volume) {
  225 +
  226 + options.volume = volume;
  227 + return this;
  228 + }
  229 +
  230 + public Builder withRate(Float rate) {
  231 +
  232 + options.rate = rate;
  233 + return this;
  234 + }
  235 +
  236 + public Builder withPitch(Float pitch) {
  237 +
  238 + options.pitch = pitch;
  239 + return this;
  240 + }
  241 +
  242 + public Builder withEnableWordTimestamp(Boolean enableWordTimestamp) {
  243 +
  244 + options.enableWordTimestamp = enableWordTimestamp;
  245 + return this;
  246 + }
  247 +
  248 + public Builder withEnablePhonemeTimestamp(Boolean enablePhonemeTimestamp) {
  249 +
  250 + options.enablePhonemeTimestamp = enablePhonemeTimestamp;
  251 + return this;
  252 + }
  253 +
  254 + public TongYiAudioSpeechOptions build() {
  255 +
  256 + return options;
  257 + }
  258 +
  259 + }
  260 +
  261 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechProperties.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.speech;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.audio.AudioSpeechModels;
  20 +import com.alibaba.dashscope.audio.tts.SpeechSynthesisAudioFormat;
  21 +import org.springframework.boot.context.properties.ConfigurationProperties;
  22 +import org.springframework.boot.context.properties.NestedConfigurationProperty;
  23 +
  24 +import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION;
  25 +
  26 +/**
  27 + * TongYi audio speech configuration properties.
  28 + *
  29 + * @author yuluo
  30 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  31 + * @since 2023.0.1.0
  32 + */
  33 +
  34 +@ConfigurationProperties(TongYiAudioSpeechProperties.CONFIG_PREFIX)
  35 +public class TongYiAudioSpeechProperties {
  36 +
  37 + /**
  38 + * Spring Cloud Alibaba AI configuration prefix.
  39 + */
  40 + public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "audio.speech";
  41 + /**
  42 + * Default TongYi Chat model.
  43 + */
  44 + public static final String DEFAULT_AUDIO_MODEL_NAME = AudioSpeechModels.SAMBERT_ZHICHU_V1;
  45 +
  46 + /**
  47 + * Enable TongYiQWEN ai audio client.
  48 + */
  49 + private boolean enabled = true;
  50 +
  51 + @NestedConfigurationProperty
  52 + private TongYiAudioSpeechOptions options = TongYiAudioSpeechOptions.builder()
  53 + .withModel(DEFAULT_AUDIO_MODEL_NAME)
  54 + .withFormat(SpeechSynthesisAudioFormat.WAV)
  55 + .build();
  56 +
  57 + public TongYiAudioSpeechOptions getOptions() {
  58 +
  59 + return this.options;
  60 + }
  61 +
  62 + public void setOptions(TongYiAudioSpeechOptions options) {
  63 +
  64 + this.options = options;
  65 + }
  66 +
  67 + public boolean isEnabled() {
  68 +
  69 + return this.enabled;
  70 + }
  71 +
  72 + public void setEnabled(boolean enabled) {
  73 +
  74 + this.enabled = enabled;
  75 + }
  76 +
  77 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/Speech.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.speech.api;
  18 +
  19 +import org.springframework.ai.model.ModelResult;
  20 +import org.springframework.lang.Nullable;
  21 +
  22 +import java.nio.ByteBuffer;
  23 +import java.util.Arrays;
  24 +import java.util.Objects;
  25 +
  26 +/**
  27 + * @author yuluo
  28 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  29 + * @since 2023.0.1.0
  30 + */
  31 +
  32 +public class Speech implements ModelResult<ByteBuffer> {
  33 +
  34 + private final ByteBuffer audio;
  35 +
  36 + private SpeechMetadata speechMetadata;
  37 +
  38 + public Speech(ByteBuffer audio) {
  39 + this.audio = audio;
  40 + }
  41 +
  42 + @Override
  43 + public ByteBuffer getOutput() {
  44 + return this.audio;
  45 + }
  46 +
  47 + @Override
  48 + public SpeechMetadata getMetadata() {
  49 +
  50 + return speechMetadata != null ? speechMetadata : SpeechMetadata.NULL;
  51 + }
  52 +
  53 + public Speech withSpeechMetadata(@Nullable SpeechMetadata speechMetadata) {
  54 +
  55 + this.speechMetadata = speechMetadata;
  56 + return this;
  57 + }
  58 +
  59 + @Override
  60 + public boolean equals(Object o) {
  61 +
  62 + if (this == o) {
  63 +
  64 + return true;
  65 + }
  66 + if (!(o instanceof Speech that)) {
  67 +
  68 + return false;
  69 + }
  70 +
  71 + return Arrays.equals(audio.array(), that.audio.array())
  72 + && Objects.equals(speechMetadata, that.speechMetadata);
  73 + }
  74 +
  75 + @Override
  76 + public int hashCode() {
  77 +
  78 + return Objects.hash(Arrays.hashCode(audio.array()), speechMetadata);
  79 + }
  80 +
  81 + @Override
  82 + public String toString() {
  83 +
  84 + return "Speech{" + "text=" + audio + ", speechMetadata=" + speechMetadata + '}';
  85 + }
  86 +
  87 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMessage.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.speech.api;
  18 +
  19 +import java.util.Objects;
  20 +
  21 +/**
  22 + * The {@link SpeechMessage} class represents a single text message to
  23 + * be converted to speech by the TongYi LLM TTS.
  24 + *
  25 + * @author yuluo
  26 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  27 + * @since 2023.0.1.0
  28 + */
  29 +
  30 +public class SpeechMessage {
  31 +
  32 + private String text;
  33 +
  34 + /**
  35 + * Constructs a new {@link SpeechMessage} object with the given text.
  36 + * @param text the text to be converted to speech
  37 + */
  38 + public SpeechMessage(String text) {
  39 + this.text = text;
  40 + }
  41 +
  42 + /**
  43 + * Returns the text of this speech message.
  44 + * @return the text of this speech message
  45 + */
  46 + public String getText() {
  47 + return text;
  48 + }
  49 +
  50 + /**
  51 + * Sets the text of this speech message.
  52 + * @param text the new text for this speech message
  53 + */
  54 + public void setText(String text) {
  55 + this.text = text;
  56 + }
  57 +
  58 + @Override
  59 + public boolean equals(Object o) {
  60 +
  61 + if (this == o) {
  62 +
  63 + return true;
  64 + }
  65 +
  66 + if (!(o instanceof SpeechMessage that)) {
  67 +
  68 + return false;
  69 + }
  70 +
  71 + return Objects.equals(text, that.text);
  72 + }
  73 +
  74 + @Override
  75 + public int hashCode() {
  76 +
  77 + return Objects.hash(text);
  78 + }
  79 +
  80 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMetadata.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.speech.api;
  18 +
  19 +import org.springframework.ai.model.ResultMetadata;
  20 +
  21 +/**
  22 + * @author yuluo
  23 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  24 + * @since 2023.0.1.0
  25 + */
  26 +
  27 +public interface SpeechMetadata extends ResultMetadata {
  28 +
  29 + /**
  30 + * Null Object.
  31 + */
  32 + SpeechMetadata NULL = SpeechMetadata.create();
  33 +
  34 + /**
  35 + * Factory method used to construct a new {@link SpeechMetadata}.
  36 + * @return a new {@link SpeechMetadata}
  37 + */
  38 + static SpeechMetadata create() {
  39 + return new SpeechMetadata() {
  40 + };
  41 + }
  42 +
  43 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechModel.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.speech.api;
  18 +
  19 +import org.springframework.ai.model.Model;
  20 +
  21 +import java.nio.ByteBuffer;
  22 +
  23 +/**
  24 + * @author yuluo
  25 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  26 + * @since 2023.0.0.0-RC1
  27 + */
  28 +
  29 +@FunctionalInterface
  30 +public interface SpeechModel extends Model<SpeechPrompt, SpeechResponse> {
  31 +
  32 + /**
  33 + * Generates spoken audio from the provided text message.
  34 + * @param message the text message to be converted to audio.
  35 + * @return the resulting audio bytes.
  36 + */
  37 + default ByteBuffer call(String message) {
  38 +
  39 + SpeechPrompt prompt = new SpeechPrompt(message);
  40 +
  41 + return call(prompt).getResult().getOutput();
  42 + }
  43 +
  44 + /**
  45 + * Sends a speech request to the TongYi TTS API and returns the resulting speech response.
  46 + * @param request the speech prompt containing the input text and other parameters.
  47 + * @return the speech response containing the generated audio.
  48 + */
  49 + SpeechResponse call(SpeechPrompt request);
  50 +
  51 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechPrompt.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.speech.api;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.audio.speech.TongYiAudioSpeechOptions;
  20 +import org.springframework.ai.model.ModelRequest;
  21 +
  22 +import java.util.Objects;
  23 +
  24 +/**
  25 + * @author yuluo
  26 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  27 + * @since 2023.0.1.0
  28 + */
  29 +
  30 +public class SpeechPrompt implements ModelRequest<SpeechMessage> {
  31 +
  32 + private TongYiAudioSpeechOptions speechOptions;
  33 +
  34 + private final SpeechMessage message;
  35 +
  36 + public SpeechPrompt(String instructions) {
  37 +
  38 + this(new SpeechMessage(instructions), TongYiAudioSpeechOptions.builder().build());
  39 + }
  40 +
  41 + public SpeechPrompt(String instructions, TongYiAudioSpeechOptions speechOptions) {
  42 +
  43 + this(new SpeechMessage(instructions), speechOptions);
  44 + }
  45 +
  46 + public SpeechPrompt(SpeechMessage speechMessage) {
  47 + this(speechMessage, TongYiAudioSpeechOptions.builder().build());
  48 + }
  49 +
  50 + public SpeechPrompt(SpeechMessage speechMessage, TongYiAudioSpeechOptions speechOptions) {
  51 +
  52 + this.message = speechMessage;
  53 + this.speechOptions = speechOptions;
  54 + }
  55 +
  56 + @Override
  57 + public SpeechMessage getInstructions() {
  58 + return this.message;
  59 + }
  60 +
  61 + @Override
  62 + public TongYiAudioSpeechOptions getOptions() {
  63 +
  64 + return speechOptions;
  65 + }
  66 +
  67 + @Override
  68 + public boolean equals(Object o) {
  69 +
  70 + if (this == o) {
  71 +
  72 + return true;
  73 + }
  74 + if (!(o instanceof SpeechPrompt that)) {
  75 +
  76 + return false;
  77 + }
  78 +
  79 + return Objects.equals(speechOptions, that.speechOptions) && Objects.equals(message, that.message);
  80 + }
  81 +
  82 + @Override
  83 + public int hashCode() {
  84 +
  85 + return Objects.hash(speechOptions, message);
  86 + }
  87 +
  88 +
  89 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechResponse.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.speech.api;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioSpeechResponseMetadata;
  20 +import org.springframework.ai.model.ModelResponse;
  21 +
  22 +import java.util.Collections;
  23 +import java.util.List;
  24 +import java.util.Objects;
  25 +
  26 +/**
  27 + * @author yuluo
  28 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  29 + * @since 2023.0.1.0
  30 + */
  31 +
  32 +public class SpeechResponse implements ModelResponse<Speech> {
  33 +
  34 + private final Speech speech;
  35 +
  36 + private final TongYiAudioSpeechResponseMetadata speechResponseMetadata;
  37 +
  38 + /**
  39 + * Creates a new instance of SpeechResponse with the given speech result.
  40 + * @param speech the speech result to be set in the SpeechResponse
  41 + * @see Speech
  42 + */
  43 + public SpeechResponse(Speech speech) {
  44 + this(speech, TongYiAudioSpeechResponseMetadata.NULL);
  45 + }
  46 +
  47 + /**
  48 + * Creates a new instance of SpeechResponse with the given speech result and speech
  49 + * response metadata.
  50 + * @param speech the speech result to be set in the SpeechResponse
  51 + * @param speechResponseMetadata the speech response metadata to be set in the
  52 + * SpeechResponse
  53 + * @see Speech
  54 + * @see TongYiAudioSpeechResponseMetadata
  55 + */
  56 + public SpeechResponse(Speech speech, TongYiAudioSpeechResponseMetadata speechResponseMetadata) {
  57 +
  58 + this.speech = speech;
  59 + this.speechResponseMetadata = speechResponseMetadata;
  60 + }
  61 +
  62 + @Override
  63 + public Speech getResult() {
  64 + return speech;
  65 + }
  66 +
  67 + @Override
  68 + public List<Speech> getResults() {
  69 + return Collections.singletonList(speech);
  70 + }
  71 +
  72 + @Override
  73 + public TongYiAudioSpeechResponseMetadata getMetadata() {
  74 + return speechResponseMetadata;
  75 + }
  76 +
  77 + @Override
  78 + public boolean equals(Object o) {
  79 +
  80 + if (this == o) {
  81 +
  82 + return true;
  83 + }
  84 + if (!(o instanceof SpeechResponse that)) {
  85 +
  86 + return false;
  87 + }
  88 +
  89 + return Objects.equals(speech, that.speech)
  90 + && Objects.equals(speechResponseMetadata, that.speechResponseMetadata);
  91 + }
  92 +
  93 + @Override
  94 + public int hashCode() {
  95 +
  96 + return Objects.hash(speech, speechResponseMetadata);
  97 + }
  98 +
  99 +
  100 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechStreamModel.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.speech.api;
  18 +
  19 +import org.springframework.ai.model.StreamingModel;
  20 +import reactor.core.publisher.Flux;
  21 +
  22 +import java.nio.ByteBuffer;
  23 +
  24 +/**
  25 + * @author yuluo
  26 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  27 + * @since 2023.0.1.0
  28 + */
  29 +
  30 +@FunctionalInterface
  31 +public interface SpeechStreamModel extends StreamingModel<SpeechPrompt, SpeechResponse> {
  32 +
  33 + /**
  34 + * Generates a stream of audio bytes from the provided text message.
  35 + *
  36 + * @param message the text message to be converted to audio
  37 + * @return a Flux of audio bytes representing the generated speech
  38 + */
  39 + default Flux<ByteBuffer> stream(String message) {
  40 +
  41 + SpeechPrompt prompt = new SpeechPrompt(message);
  42 + return stream(prompt).map(SpeechResponse::getResult).map(Speech::getOutput);
  43 + }
  44 +
  45 + /**
  46 + * Sends a speech request to the TongYi TTS API and returns a stream of the resulting
  47 + * speech responses.
  48 + * @param prompt the speech prompt containing the input text and other parameters.
  49 + * @return a Flux of speech responses, each containing a portion of the generated audio.
  50 + */
  51 + @Override
  52 + Flux<SpeechResponse> stream(SpeechPrompt prompt);
  53 +
  54 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.transcription;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.audio.AudioTranscriptionModels;
  20 +import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionPrompt;
  21 +import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionResponse;
  22 +import com.alibaba.cloud.ai.tongyi.audio.transcription.api.AudioTranscriptionResult;
  23 +import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException;
  24 +import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioTranscriptionResponseMetadata;
  25 +import com.alibaba.dashscope.audio.asr.transcription.*;
  26 +import org.springframework.ai.model.Model;
  27 +import org.springframework.core.io.Resource;
  28 +import org.springframework.util.Assert;
  29 +
  30 +import java.io.IOException;
  31 +import java.util.ArrayList;
  32 +import java.util.Arrays;
  33 +import java.util.List;
  34 +import java.util.stream.Collectors;
  35 +
  36 +/**
  37 + * TongYiAudioTranscriptionModel is a client for TongYi audio transcription service for
  38 + * Spring Cloud Alibaba AI.
  39 + * @author xYLiu
  40 + * @author yuluo
  41 + * @since 2023.0.1.0
  42 + */
  43 +
  44 +public class TongYiAudioTranscriptionModel
  45 + implements Model<AudioTranscriptionPrompt, AudioTranscriptionResponse> {
  46 +
  47 + /**
  48 + * TongYi models options.
  49 + */
  50 + private final TongYiAudioTranscriptionOptions defaultOptions;
  51 +
  52 + /**
  53 + * TongYi models api.
  54 + */
  55 + private final Transcription transcription;
  56 +
  57 + public TongYiAudioTranscriptionModel(Transcription transcription) {
  58 + this(null, transcription);
  59 + }
  60 +
  61 + public TongYiAudioTranscriptionModel(TongYiAudioTranscriptionOptions defaultOptions,
  62 + Transcription transcription) {
  63 + Assert.notNull(transcription, "transcription must not be null");
  64 + Assert.notNull(defaultOptions, "defaultOptions must not be null");
  65 +
  66 + this.defaultOptions = defaultOptions;
  67 + this.transcription = transcription;
  68 + }
  69 +
  70 + @Override
  71 + public AudioTranscriptionResponse call(AudioTranscriptionPrompt prompt) {
  72 +
  73 + TranscriptionParam transcriptionParam;
  74 +
  75 + if (prompt.getOptions() != null) {
  76 + var param = merge(prompt.getOptions());
  77 + transcriptionParam = toTranscriptionParam(param);
  78 + transcriptionParam.setFileUrls(prompt.getOptions().getFileUrls());
  79 + }
  80 + else {
  81 + Resource instructions = prompt.getInstructions();
  82 + try {
  83 + transcriptionParam = TranscriptionParam.builder()
  84 + .model(AudioTranscriptionModels.Paraformer_V1)
  85 + .fileUrls(List.of(String.valueOf(instructions.getURL())))
  86 + .build();
  87 + }
  88 + catch (IOException e) {
  89 + throw new TongYiException("Failed to create resource", e);
  90 + }
  91 + }
  92 +
  93 + List<TranscriptionTaskResult> taskResultList;
  94 + try {
  95 + // Submit a transcription request
  96 + TranscriptionResult result = transcription.asyncCall(transcriptionParam);
  97 + // Wait for the transcription to complete
  98 + result = transcription.wait(TranscriptionQueryParam
  99 + .FromTranscriptionParam(transcriptionParam, result.getTaskId()));
  100 + // Get the transcription results
  101 + System.out.println(result.getOutput().getAsJsonObject());
  102 + taskResultList = result.getResults();
  103 + System.out.println(Arrays.toString(taskResultList.toArray()));
  104 +
  105 + return new AudioTranscriptionResponse(
  106 + taskResultList.stream().map(taskResult ->
  107 + new AudioTranscriptionResult(taskResult.getTranscriptionUrl())
  108 + ).collect(Collectors.toList()),
  109 + TongYiAudioTranscriptionResponseMetadata.from(result)
  110 + );
  111 + }
  112 + catch (Exception e) {
  113 + throw new TongYiException("Failed to call audio transcription", e);
  114 + }
  115 +
  116 + }
  117 +
  118 + public TongYiAudioTranscriptionOptions merge(TongYiAudioTranscriptionOptions target) {
  119 + var mergeBuilder = TongYiAudioTranscriptionOptions.builder();
  120 +
  121 + mergeBuilder
  122 + .withModel(defaultOptions.getModel() != null ? defaultOptions.getModel()
  123 + : target.getModel());
  124 + mergeBuilder.withChannelId(
  125 + defaultOptions.getChannelId() != null ? defaultOptions.getChannelId()
  126 + : target.getChannelId());
  127 + mergeBuilder.withDiarizationEnabled(defaultOptions.getDiarizationEnabled() != null
  128 + ? defaultOptions.getDiarizationEnabled()
  129 + : target.getDiarizationEnabled());
  130 + mergeBuilder.withDisfluencyRemovalEnabled(
  131 + defaultOptions.getDisfluencyRemovalEnabled() != null
  132 + ? defaultOptions.getDisfluencyRemovalEnabled()
  133 + : target.getDisfluencyRemovalEnabled());
  134 + mergeBuilder.withTimestampAlignmentEnabled(
  135 + defaultOptions.getTimestampAlignmentEnabled() != null
  136 + ? defaultOptions.getTimestampAlignmentEnabled()
  137 + : target.getTimestampAlignmentEnabled());
  138 + mergeBuilder.withSpecialWordFilter(defaultOptions.getSpecialWordFilter() != null
  139 + ? defaultOptions.getSpecialWordFilter()
  140 + : target.getSpecialWordFilter());
  141 + mergeBuilder.withAudioEventDetectionEnabled(
  142 + defaultOptions.getAudioEventDetectionEnabled() != null
  143 + ? defaultOptions.getAudioEventDetectionEnabled()
  144 + : target.getAudioEventDetectionEnabled());
  145 +
  146 + return mergeBuilder.build();
  147 + }
  148 +
  149 + public TranscriptionParam toTranscriptionParam(
  150 + TongYiAudioTranscriptionOptions source) {
  151 + var mergeBuilder = TranscriptionParam.builder();
  152 +
  153 + mergeBuilder.model(source.getModel() != null ? source.getModel()
  154 + : AudioTranscriptionModels.Paraformer_V1);
  155 + mergeBuilder.fileUrls(
  156 + source.getFileUrls() != null ? source.getFileUrls() : new ArrayList<>());
  157 + if (source.getPhraseId() != null) {
  158 + mergeBuilder.phraseId(source.getPhraseId());
  159 + }
  160 + if (source.getChannelId() != null) {
  161 + mergeBuilder.channelId(source.getChannelId());
  162 + }
  163 + if (source.getDiarizationEnabled() != null) {
  164 + mergeBuilder.diarizationEnabled(source.getDiarizationEnabled());
  165 + }
  166 + if (source.getSpeakerCount() != null) {
  167 + mergeBuilder.speakerCount(source.getSpeakerCount());
  168 + }
  169 + if (source.getDisfluencyRemovalEnabled() != null) {
  170 + mergeBuilder.disfluencyRemovalEnabled(source.getDisfluencyRemovalEnabled());
  171 + }
  172 + if (source.getTimestampAlignmentEnabled() != null) {
  173 + mergeBuilder.timestampAlignmentEnabled(source.getTimestampAlignmentEnabled());
  174 + }
  175 + if (source.getSpecialWordFilter() != null) {
  176 + mergeBuilder.specialWordFilter(source.getSpecialWordFilter());
  177 + }
  178 + if (source.getAudioEventDetectionEnabled() != null) {
  179 + mergeBuilder
  180 + .audioEventDetectionEnabled(source.getAudioEventDetectionEnabled());
  181 + }
  182 +
  183 + return mergeBuilder.build();
  184 + }
  185 +
  186 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionOptions.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.transcription;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.audio.AudioTranscriptionModels;
  20 +import org.springframework.ai.model.ModelOptions;
  21 +
  22 +import java.util.ArrayList;
  23 +import java.util.Collections;
  24 +import java.util.List;
  25 +
  26 +/**
  27 + * @author xYLiu
  28 + * @author yuluo
  29 + * @since 2023.0.1.0
  30 + */
  31 +
  32 +public class TongYiAudioTranscriptionOptions implements ModelOptions {
  33 +
  34 + private String model = AudioTranscriptionModels.Paraformer_V1;
  35 +
  36 + private List<String> fileUrls = new ArrayList<>();
  37 +
  38 + private String phraseId = null;
  39 +
  40 + private List<Integer> channelId = Collections.singletonList(0);
  41 +
  42 + private Boolean diarizationEnabled = false;
  43 +
  44 + private Integer speakerCount = null;
  45 +
  46 + private Boolean disfluencyRemovalEnabled = false;
  47 +
  48 + private Boolean timestampAlignmentEnabled = false;
  49 +
  50 + private String specialWordFilter = "";
  51 +
  52 + private Boolean audioEventDetectionEnabled = false;
  53 +
  54 + public static Builder builder() {
  55 +
  56 + return new Builder();
  57 + }
  58 +
  59 + public String getModel() {
  60 + return model;
  61 + }
  62 +
  63 + public void setModel(String model) {
  64 + this.model = model;
  65 + }
  66 +
  67 + public List<String> getFileUrls() {
  68 + return fileUrls;
  69 + }
  70 +
  71 + public void setFileUrls(List<String> fileUrls) {
  72 + this.fileUrls = fileUrls;
  73 + }
  74 +
  75 + public String getPhraseId() {
  76 + return phraseId;
  77 + }
  78 +
  79 + public void setPhraseId(String phraseId) {
  80 + this.phraseId = phraseId;
  81 + }
  82 +
  83 + public List<Integer> getChannelId() {
  84 + return channelId;
  85 + }
  86 +
  87 + public void setChannelId(List<Integer> channelId) {
  88 + this.channelId = channelId;
  89 + }
  90 +
  91 + public Boolean getDiarizationEnabled() {
  92 + return diarizationEnabled;
  93 + }
  94 +
  95 + public void setDiarizationEnabled(Boolean diarizationEnabled) {
  96 + this.diarizationEnabled = diarizationEnabled;
  97 + }
  98 +
  99 + public Integer getSpeakerCount() {
  100 + return speakerCount;
  101 + }
  102 +
  103 + public void setSpeakerCount(Integer speakerCount) {
  104 + this.speakerCount = speakerCount;
  105 + }
  106 +
  107 + public Boolean getDisfluencyRemovalEnabled() {
  108 + return disfluencyRemovalEnabled;
  109 + }
  110 +
  111 + public void setDisfluencyRemovalEnabled(Boolean disfluencyRemovalEnabled) {
  112 + this.disfluencyRemovalEnabled = disfluencyRemovalEnabled;
  113 + }
  114 +
  115 + public Boolean getTimestampAlignmentEnabled() {
  116 + return timestampAlignmentEnabled;
  117 + }
  118 +
  119 + public void setTimestampAlignmentEnabled(Boolean timestampAlignmentEnabled) {
  120 + this.timestampAlignmentEnabled = timestampAlignmentEnabled;
  121 + }
  122 +
  123 + public String getSpecialWordFilter() {
  124 + return specialWordFilter;
  125 + }
  126 +
  127 + public void setSpecialWordFilter(String specialWordFilter) {
  128 + this.specialWordFilter = specialWordFilter;
  129 + }
  130 +
  131 + public Boolean getAudioEventDetectionEnabled() {
  132 + return audioEventDetectionEnabled;
  133 + }
  134 +
  135 + public void setAudioEventDetectionEnabled(Boolean audioEventDetectionEnabled) {
  136 + this.audioEventDetectionEnabled = audioEventDetectionEnabled;
  137 + }
  138 +
  139 + /**
  140 + * Builder class for constructing TongYiAudioTranscriptionOptions instances.
  141 + */
  142 + public static class Builder {
  143 +
  144 + private final TongYiAudioTranscriptionOptions options = new TongYiAudioTranscriptionOptions();
  145 +
  146 + public Builder withModel(String model) {
  147 + options.model = model;
  148 + return this;
  149 + }
  150 +
  151 + public Builder withFileUrls(List<String> fileUrls) {
  152 + options.fileUrls = fileUrls;
  153 + return this;
  154 + }
  155 +
  156 + public Builder withPhraseId(String phraseId) {
  157 + options.phraseId = phraseId;
  158 + return this;
  159 + }
  160 +
  161 + public Builder withChannelId(List<Integer> channelId) {
  162 + options.channelId = channelId;
  163 + return this;
  164 + }
  165 +
  166 + public Builder withDiarizationEnabled(Boolean diarizationEnabled) {
  167 + options.diarizationEnabled = diarizationEnabled;
  168 + return this;
  169 + }
  170 +
  171 + public Builder withSpeakerCount(Integer speakerCount) {
  172 + options.speakerCount = speakerCount;
  173 + return this;
  174 + }
  175 +
  176 + public Builder withDisfluencyRemovalEnabled(Boolean disfluencyRemovalEnabled) {
  177 + options.disfluencyRemovalEnabled = disfluencyRemovalEnabled;
  178 + return this;
  179 + }
  180 +
  181 + public Builder withTimestampAlignmentEnabled(Boolean timestampAlignmentEnabled) {
  182 + options.timestampAlignmentEnabled = timestampAlignmentEnabled;
  183 + return this;
  184 + }
  185 +
  186 + public Builder withSpecialWordFilter(String specialWordFilter) {
  187 + options.specialWordFilter = specialWordFilter;
  188 + return this;
  189 + }
  190 +
  191 + public Builder withAudioEventDetectionEnabled(
  192 + Boolean audioEventDetectionEnabled) {
  193 + options.audioEventDetectionEnabled = audioEventDetectionEnabled;
  194 + return this;
  195 + }
  196 +
  197 + public TongYiAudioTranscriptionOptions build() {
  198 + // Perform any necessary validation here before returning the built object
  199 + return options;
  200 + }
  201 + }
  202 +
  203 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionProperties.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.transcription;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.audio.AudioTranscriptionModels;
  20 +import org.springframework.boot.context.properties.ConfigurationProperties;
  21 +import org.springframework.boot.context.properties.NestedConfigurationProperty;
  22 +
  23 +import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION;
  24 +
  25 +/**
  26 + * @author xYLiu
  27 + * @author yuluo
  28 + * @since 2023.0.1.0
  29 + */
  30 +
  31 +@ConfigurationProperties(TongYiAudioTranscriptionProperties.CONFIG_PREFIX)
  32 +public class TongYiAudioTranscriptionProperties {
  33 +
  34 + /**
  35 + * Spring Cloud Alibaba AI configuration prefix.
  36 + */
  37 + public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "audio.transcription";
  38 +
  39 + /**
  40 + * Default TongYi Chat model.
  41 + */
  42 + public static final String DEFAULT_AUDIO_MODEL_NAME = AudioTranscriptionModels.Paraformer_V1;
  43 +
  44 + /**
  45 + * Enable TongYiQWEN ai audio client.
  46 + */
  47 + private boolean enabled = true;
  48 +
  49 + @NestedConfigurationProperty
  50 + private TongYiAudioTranscriptionOptions options = TongYiAudioTranscriptionOptions
  51 + .builder().withModel(DEFAULT_AUDIO_MODEL_NAME).build();
  52 +
  53 + public TongYiAudioTranscriptionOptions getOptions() {
  54 +
  55 + return this.options;
  56 + }
  57 +
  58 + public void setOptions(TongYiAudioTranscriptionOptions options) {
  59 +
  60 + this.options = options;
  61 + }
  62 +
  63 + public boolean isEnabled() {
  64 +
  65 + return this.enabled;
  66 + }
  67 +
  68 + public void setEnabled(boolean enabled) {
  69 +
  70 + this.enabled = enabled;
  71 + }
  72 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionPrompt.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.transcription.api;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.audio.transcription.TongYiAudioTranscriptionOptions;
  20 +import org.springframework.ai.model.ModelRequest;
  21 +import org.springframework.core.io.Resource;
  22 +
  23 +/**
  24 + * @author xYLiu
  25 + * @author yuluo
  26 + * @since 2023.0.1.0
  27 + */
  28 +
  29 +public class AudioTranscriptionPrompt implements ModelRequest<Resource> {
  30 +
  31 + private Resource audioResource;
  32 +
  33 + private TongYiAudioTranscriptionOptions transcriptionOptions;
  34 +
  35 + public AudioTranscriptionPrompt(Resource resource) {
  36 + this.audioResource = resource;
  37 + }
  38 +
  39 + public AudioTranscriptionPrompt(Resource resource, TongYiAudioTranscriptionOptions transcriptionOptions) {
  40 + this.audioResource = resource;
  41 + this.transcriptionOptions = transcriptionOptions;
  42 + }
  43 +
  44 + @Override
  45 + public Resource getInstructions() {
  46 +
  47 + return audioResource;
  48 + }
  49 +
  50 + @Override
  51 + public TongYiAudioTranscriptionOptions getOptions() {
  52 +
  53 + return transcriptionOptions;
  54 + }
  55 +
  56 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResponse.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.transcription.api;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioTranscriptionResponseMetadata;
  20 +import org.springframework.ai.model.ModelResponse;
  21 +import org.springframework.ai.model.ResponseMetadata;
  22 +
  23 +import java.util.List;
  24 +
  25 +/**
  26 + * @author xYLiu
  27 + * @author yuluo
  28 + * @since 2023.0.1.0
  29 + */
  30 +
  31 +public class AudioTranscriptionResponse implements ModelResponse<AudioTranscriptionResult> {
  32 +
  33 + private List<AudioTranscriptionResult> resultList;
  34 +
  35 + private TongYiAudioTranscriptionResponseMetadata transcriptionResponseMetadata;
  36 +
  37 + public AudioTranscriptionResponse(List<AudioTranscriptionResult> result) {
  38 +
  39 + this(result, TongYiAudioTranscriptionResponseMetadata.NULL);
  40 + }
  41 +
  42 + public AudioTranscriptionResponse(List<AudioTranscriptionResult> result,
  43 + TongYiAudioTranscriptionResponseMetadata transcriptionResponseMetadata) {
  44 +
  45 + this.resultList = List.copyOf(result);
  46 + this.transcriptionResponseMetadata = transcriptionResponseMetadata;
  47 + }
  48 +
  49 + @Override
  50 + public AudioTranscriptionResult getResult() {
  51 +
  52 + return this.resultList.get(0);
  53 + }
  54 +
  55 + @Override
  56 + public List<AudioTranscriptionResult> getResults() {
  57 +
  58 + return this.resultList;
  59 + }
  60 +
  61 + @Override
  62 + public ResponseMetadata getMetadata() {
  63 +
  64 + return this.transcriptionResponseMetadata;
  65 + }
  66 +
  67 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResult.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.audio.transcription.api;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.metadata.audio.TongYiAudioTranscriptionMetadata;
  20 +import org.springframework.ai.model.ModelResult;
  21 +import org.springframework.ai.model.ResultMetadata;
  22 +
  23 +import java.util.Objects;
  24 +
  25 +/**
  26 + * @author yuluo
  27 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  28 + * @since 2023.0.1.0
  29 + */
  30 +public class AudioTranscriptionResult implements ModelResult<String> {
  31 +
  32 + private String text;
  33 +
  34 + private TongYiAudioTranscriptionMetadata transcriptionMetadata;
  35 +
  36 + public AudioTranscriptionResult(String text) {
  37 + this.text = text;
  38 + }
  39 +
  40 + @Override
  41 + public String getOutput() {
  42 +
  43 + return this.text;
  44 + }
  45 +
  46 + @Override
  47 + public ResultMetadata getMetadata() {
  48 +
  49 + return transcriptionMetadata != null ? transcriptionMetadata : TongYiAudioTranscriptionMetadata.NULL;
  50 + }
  51 +
  52 + @Override
  53 + public boolean equals(Object o) {
  54 + if (this == o) {
  55 + return true;
  56 + }
  57 + if (o == null || getClass() != o.getClass()) {
  58 + return false;
  59 + }
  60 + AudioTranscriptionResult that = (AudioTranscriptionResult) o;
  61 + return Objects.equals(text, that.text) && Objects.equals(transcriptionMetadata, that.transcriptionMetadata);
  62 + }
  63 +
  64 + @Override
  65 + public int hashCode() {
  66 + return Objects.hash(text, transcriptionMetadata);
  67 + }
  68 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.chat;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException;
  20 +import com.alibaba.dashscope.aigc.conversation.ConversationParam;
  21 +import com.alibaba.dashscope.aigc.generation.Generation;
  22 +import com.alibaba.dashscope.aigc.generation.GenerationOutput;
  23 +import com.alibaba.dashscope.aigc.generation.GenerationResult;
  24 +import com.alibaba.dashscope.common.MessageManager;
  25 +import com.alibaba.dashscope.common.Role;
  26 +import com.alibaba.dashscope.exception.InputRequiredException;
  27 +import com.alibaba.dashscope.exception.NoApiKeyException;
  28 +import com.alibaba.dashscope.tools.FunctionDefinition;
  29 +import com.alibaba.dashscope.tools.ToolCallBase;
  30 +import com.alibaba.dashscope.tools.ToolCallFunction;
  31 +import com.alibaba.dashscope.utils.ApiKeywords;
  32 +import com.alibaba.dashscope.utils.JsonUtils;
  33 +import io.reactivex.Flowable;
  34 +import org.slf4j.Logger;
  35 +import org.slf4j.LoggerFactory;
  36 +import org.springframework.ai.chat.messages.Message;
  37 +import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
  38 +import org.springframework.ai.chat.model.ChatModel;
  39 +import org.springframework.ai.chat.model.ChatResponse;
  40 +import org.springframework.ai.chat.model.StreamingChatModel;
  41 +import org.springframework.ai.chat.prompt.ChatOptions;
  42 +import org.springframework.ai.chat.prompt.Prompt;
  43 +import org.springframework.ai.model.ModelOptionsUtils;
  44 +import org.springframework.ai.model.function.AbstractFunctionCallSupport;
  45 +import org.springframework.ai.model.function.FunctionCallbackContext;
  46 +import org.springframework.beans.factory.annotation.Autowired;
  47 +import org.springframework.util.CollectionUtils;
  48 +import reactor.core.publisher.Flux;
  49 +import reactor.core.scheduler.Schedulers;
  50 +
  51 +import java.util.HashSet;
  52 +import java.util.List;
  53 +import java.util.Objects;
  54 +import java.util.Set;
  55 +
  56 +
  57 +/**
  58 + * {@link ChatModel} and {@link StreamingChatModel} implementation for {@literal Alibaba DashScope}
  59 + * backed by {@link Generation}.
  60 + *
  61 + * @author yuluo
  62 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  63 + * @since 2023.0.1.0
  64 + * @see ChatModel
  65 + * @see com.alibaba.dashscope.aigc.generation
  66 + */
  67 +
  68 +public class TongYiChatModel extends
  69 + AbstractFunctionCallSupport<
  70 + com.alibaba.dashscope.common.Message,
  71 + ConversationParam,
  72 + GenerationResult>
  73 + implements ChatModel, StreamingChatModel {
  74 +
  75 + private static final Logger logger = LoggerFactory.getLogger(TongYiChatModel.class);
  76 +
  77 + /**
  78 + * DashScope generation client.
  79 + */
  80 + private final Generation generation;
  81 +
  82 + /**
  83 + * The TongYi models default chat completion api.
  84 + */
  85 + private TongYiChatOptions defaultOptions;
  86 +
  87 + /**
  88 + * User role message manager.
  89 + */
  90 + @Autowired
  91 + private MessageManager msgManager;
  92 +
  93 + /**
  94 + * Initializes an instance of the TongYiChatClient.
  95 + * @param generation DashScope generation client.
  96 + */
  97 + public TongYiChatModel(Generation generation) {
  98 +
  99 + this(generation,
  100 + TongYiChatOptions.builder()
  101 + .withTopP(0.8)
  102 + .withEnableSearch(true)
  103 + .withResultFormat(ConversationParam.ResultFormat.MESSAGE)
  104 + .build(),
  105 + null
  106 + );
  107 + }
  108 +
  109 + /**
  110 + * Initializes an instance of the TongYiChatClient.
  111 + * @param generation DashScope generation client.
  112 + * @param options TongYi model params.
  113 + */
  114 + public TongYiChatModel(Generation generation, TongYiChatOptions options) {
  115 +
  116 + this(generation, options, null);
  117 + }
  118 +
  119 + /**
  120 + * Create a TongYi models client.
  121 + * @param generation DashScope model generation client.
  122 + * @param options TongYi default chat completion api.
  123 + */
  124 + public TongYiChatModel(Generation generation, TongYiChatOptions options,
  125 + FunctionCallbackContext functionCallbackContext) {
  126 +
  127 + super(functionCallbackContext);
  128 + this.generation = generation;
  129 + this.defaultOptions = options;
  130 + }
  131 +
  132 + /**
  133 + * Get default sca chat options.
  134 + *
  135 + * @return TongYiChatOptions default object.
  136 + */
  137 + public TongYiChatOptions getDefaultOptions() {
  138 +
  139 + return this.defaultOptions;
  140 + }
  141 +
  142 + @Override
  143 + public ChatResponse call(Prompt prompt) {
  144 +
  145 + ConversationParam params = toTongYiChatParams(prompt);
  146 +
  147 + // TongYi models context loader.
  148 + com.alibaba.dashscope.common.Message message = new com.alibaba.dashscope.common.Message();
  149 + message.setRole(Role.USER.getValue());
  150 + message.setContent(prompt.getContents());
  151 + msgManager.add(message);
  152 + params.setMessages(msgManager.get());
  153 +
  154 + logger.trace("TongYi ConversationOptions: {}", params);
  155 + GenerationResult chatCompletions = this.callWithFunctionSupport(params);
  156 + logger.trace("TongYi ConversationOptions: {}", params);
  157 +
  158 + msgManager.add(chatCompletions);
  159 +
  160 + List<org.springframework.ai.chat.model.Generation> generations =
  161 + chatCompletions
  162 + .getOutput()
  163 + .getChoices()
  164 + .stream()
  165 + .map(choice ->
  166 + new org.springframework.ai.chat.model.Generation(
  167 + choice
  168 + .getMessage()
  169 + .getContent()
  170 + ).withGenerationMetadata(generateChoiceMetadata(choice)
  171 + ))
  172 + .toList();
  173 +
  174 + return new ChatResponse(generations);
  175 +
  176 + }
  177 +
  178 + @Override
  179 + public Flux<ChatResponse> stream(Prompt prompt) {
  180 +
  181 + Flowable<GenerationResult> genRes;
  182 + ConversationParam tongYiChatParams = toTongYiChatParams(prompt);
  183 +
  184 + // See https://help.aliyun.com/zh/dashscope/developer-reference/api-details?spm=a2c4g.11186623.0.0.655fc11aRR0jj7#b9ad0a10cfhpe
  185 + // tongYiChatParams.setIncrementalOutput(true);
  186 +
  187 + try {
  188 + genRes = generation.streamCall(tongYiChatParams);
  189 + }
  190 + catch (NoApiKeyException | InputRequiredException e) {
  191 + logger.warn("TongYi chat client: " + e.getMessage());
  192 + throw new TongYiException(e.getMessage());
  193 + }
  194 +
  195 + return Flux.from(genRes)
  196 + .flatMap(
  197 + message -> Flux.just(
  198 + message.getOutput()
  199 + .getChoices()
  200 + .get(0)
  201 + .getMessage()
  202 + .getContent())
  203 + .map(content -> {
  204 + var gen = new org.springframework.ai.chat.model.Generation(content)
  205 + .withGenerationMetadata(generateChoiceMetadata(
  206 + message.getOutput()
  207 + .getChoices()
  208 + .get(0)
  209 + ));
  210 + return new ChatResponse(List.of(gen));
  211 + })
  212 + )
  213 + .publishOn(Schedulers.parallel());
  214 +
  215 + }
  216 +
  217 + /**
  218 + * Configuration properties to Qwen model params.
  219 + * Test access.
  220 + *
  221 + * @param prompt {@link Prompt}
  222 + * @return Qwen models params {@link ConversationParam}
  223 + */
  224 + public ConversationParam toTongYiChatParams(Prompt prompt) {
  225 +
  226 + Set<String> functionsForThisRequest = new HashSet<>();
  227 +
  228 + List<com.alibaba.dashscope.common.Message> tongYiMessage = prompt.getInstructions().stream()
  229 + .map(this::fromSpringAIMessage)
  230 + .toList();
  231 +
  232 + ConversationParam chatParams = ConversationParam.builder()
  233 + .messages(tongYiMessage)
  234 + // models setting
  235 + // {@link HalfDuplexServiceParam#models}
  236 + .model(Generation.Models.QWEN_TURBO)
  237 + // {@link GenerationOutput}
  238 + .resultFormat(ConversationParam.ResultFormat.MESSAGE)
  239 +
  240 + .build();
  241 +
  242 + if (this.defaultOptions != null) {
  243 +
  244 + chatParams = merge(chatParams, this.defaultOptions);
  245 + Set<String> defaultEnabledFunctions = this.handleFunctionCallbackConfigurations(this.defaultOptions, !IS_RUNTIME_CALL);
  246 + functionsForThisRequest.addAll(defaultEnabledFunctions);
  247 + }
  248 + if (prompt.getOptions() != null) {
  249 + if (prompt.getOptions() instanceof ChatOptions runtimeOptions) {
  250 + TongYiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions,
  251 + ChatOptions.class, TongYiChatOptions.class);
  252 +
  253 + chatParams = merge(updatedRuntimeOptions, chatParams);
  254 +
  255 + Set<String> promptEnabledFunctions = this.handleFunctionCallbackConfigurations(updatedRuntimeOptions,
  256 + IS_RUNTIME_CALL);
  257 + functionsForThisRequest.addAll(promptEnabledFunctions);
  258 +
  259 + }
  260 + else {
  261 + throw new IllegalArgumentException("Prompt options are not of type ConversationParam:"
  262 + + prompt.getOptions().getClass().getSimpleName());
  263 + }
  264 + }
  265 +
  266 + // Add the enabled functions definitions to the request's tools parameter.
  267 +
  268 + if (!CollectionUtils.isEmpty(functionsForThisRequest)) {
  269 + List<FunctionDefinition> tools = this.getFunctionTools(functionsForThisRequest);
  270 +
  271 + // todo chatParams.setTools(tools)
  272 + }
  273 +
  274 + return chatParams;
  275 + }
  276 +
  277 + private ChatGenerationMetadata generateChoiceMetadata(GenerationOutput.Choice choice) {
  278 +
  279 + return ChatGenerationMetadata.from(
  280 + String.valueOf(choice.getFinishReason()),
  281 + choice.getMessage().getContent()
  282 + );
  283 + }
  284 +
  285 + private List<FunctionDefinition> getFunctionTools(Set<String> functionNames) {
  286 + return this.resolveFunctionCallbacks(functionNames).stream().map(functionCallback -> {
  287 +
  288 + FunctionDefinition functionDefinition = FunctionDefinition.builder()
  289 + .name(functionCallback.getName())
  290 + .description(functionCallback.getDescription())
  291 + .parameters(JsonUtils.parametersToJsonObject(
  292 + ModelOptionsUtils.jsonToMap(functionCallback.getInputTypeSchema())
  293 + ))
  294 + .build();
  295 +
  296 + return functionDefinition;
  297 + }).toList();
  298 + }
  299 +
  300 +
  301 + private ConversationParam merge(ConversationParam tongYiParams, TongYiChatOptions scaChatParams) {
  302 +
  303 + if (scaChatParams == null) {
  304 +
  305 + return tongYiParams;
  306 + }
  307 +
  308 + return ConversationParam.builder()
  309 + .messages(tongYiParams.getMessages())
  310 + .maxTokens((tongYiParams.getMaxTokens() != null) ? tongYiParams.getMaxTokens() : scaChatParams.getMaxTokens())
  311 + // When merge options. Because ConversationParams is must not null. So is setting.
  312 + .model(scaChatParams.getModel())
  313 + .resultFormat((tongYiParams.getResultFormat() != null) ? tongYiParams.getResultFormat() : scaChatParams.getResultFormat())
  314 + .enableSearch((tongYiParams.getEnableSearch() != null) ? tongYiParams.getEnableSearch() : scaChatParams.getEnableSearch())
  315 + .topK((tongYiParams.getTopK() != null) ? tongYiParams.getTopK() : scaChatParams.getTopK())
  316 + .topP((tongYiParams.getTopP() != null) ? tongYiParams.getTopP() : scaChatParams.getTopP())
  317 + .incrementalOutput((tongYiParams.getIncrementalOutput() != null) ? tongYiParams.getIncrementalOutput() : scaChatParams.getIncrementalOutput())
  318 + .temperature((tongYiParams.getTemperature() != null) ? tongYiParams.getTemperature() : scaChatParams.getTemperature())
  319 + .repetitionPenalty((tongYiParams.getRepetitionPenalty() != null) ? tongYiParams.getRepetitionPenalty() : scaChatParams.getRepetitionPenalty())
  320 + .seed((tongYiParams.getSeed() != null) ? tongYiParams.getSeed() : scaChatParams.getSeed())
  321 + .build();
  322 +
  323 + }
  324 +
  325 + private ConversationParam merge(TongYiChatOptions scaChatParams, ConversationParam tongYiParams) {
  326 +
  327 + if (scaChatParams == null) {
  328 +
  329 + return tongYiParams;
  330 + }
  331 +
  332 + ConversationParam mergedTongYiParams = ConversationParam.builder()
  333 + .model(Generation.Models.QWEN_TURBO)
  334 + .messages(tongYiParams.getMessages())
  335 + .build();
  336 + mergedTongYiParams = merge(tongYiParams, scaChatParams);
  337 +
  338 + if (scaChatParams.getMaxTokens() != null) {
  339 + mergedTongYiParams.setMaxTokens(scaChatParams.getMaxTokens());
  340 + }
  341 +
  342 + if (scaChatParams.getStop() != null) {
  343 + mergedTongYiParams.setStopStrings(scaChatParams.getStop());
  344 + }
  345 +
  346 + if (scaChatParams.getTemperature() != null) {
  347 + mergedTongYiParams.setTemperature(scaChatParams.getTemperature());
  348 + }
  349 +
  350 + if (scaChatParams.getTopK() != null) {
  351 + mergedTongYiParams.setTopK(scaChatParams.getTopK());
  352 + }
  353 +
  354 + if (scaChatParams.getTopK() != null) {
  355 + mergedTongYiParams.setTopK(scaChatParams.getTopK());
  356 + }
  357 +
  358 + return mergedTongYiParams;
  359 + }
  360 +
  361 + private com.alibaba.dashscope.common.Message fromSpringAIMessage(Message message) {
  362 +
  363 + return switch (message.getMessageType()) {
  364 + case USER -> com.alibaba.dashscope.common.Message.builder()
  365 + .role(Role.USER.getValue())
  366 + .content(message.getContent())
  367 + .build();
  368 + case SYSTEM -> com.alibaba.dashscope.common.Message.builder()
  369 + .role(Role.SYSTEM.getValue())
  370 + .content(message.getContent())
  371 + .build();
  372 + case ASSISTANT -> com.alibaba.dashscope.common.Message.builder()
  373 + .role(Role.ASSISTANT.getValue())
  374 + .content(message.getContent())
  375 + .build();
  376 + default -> throw new IllegalArgumentException("Unknown message type " + message.getMessageType());
  377 + };
  378 +
  379 + }
  380 +
  381 + @Override
  382 + protected ConversationParam doCreateToolResponseRequest(
  383 + ConversationParam previousRequest,
  384 + com.alibaba.dashscope.common.Message responseMessage,
  385 + List<com.alibaba.dashscope.common.Message> conversationHistory
  386 + ) {
  387 + for (ToolCallBase toolCall : responseMessage.getToolCalls()) {
  388 + if (toolCall instanceof ToolCallFunction toolCallFunction) {
  389 + if (toolCallFunction.getFunction() != null) {
  390 + var functionName = toolCallFunction.getFunction().getName();
  391 + var functionArguments = toolCallFunction.getFunction().getArguments();
  392 +
  393 + if (!this.functionCallbackRegister.containsKey(functionName)) {
  394 + throw new IllegalStateException("No function callback found for function name: " + functionName);
  395 + }
  396 +
  397 + String functionResponse = this.functionCallbackRegister.get(functionName).call(functionArguments);
  398 +
  399 + // Add the function response to the conversation.
  400 + conversationHistory
  401 + .add(com.alibaba.dashscope.common.Message.builder()
  402 + .content(functionResponse)
  403 + .role(Role.BOT.getValue())
  404 + .toolCallId(toolCall.getId())
  405 + .build()
  406 + );
  407 + }
  408 + }
  409 +
  410 + }
  411 +
  412 + ConversationParam newRequest = ConversationParam.builder().messages(conversationHistory).build();
  413 +
  414 + // todo: No @JsonProperty fields.
  415 + newRequest = ModelOptionsUtils.merge(newRequest, previousRequest, ConversationParam.class);
  416 +
  417 + return newRequest;
  418 +
  419 + }
  420 +
  421 + @Override
  422 + protected List<com.alibaba.dashscope.common.Message> doGetUserMessages(ConversationParam request) {
  423 +
  424 + return request.getMessages();
  425 + }
  426 +
  427 + @Override
  428 + protected com.alibaba.dashscope.common.Message doGetToolResponseMessage(GenerationResult response) {
  429 +
  430 + var message = response.getOutput().getChoices().get(0).getMessage();
  431 + var assistantMessage = com.alibaba.dashscope.common.Message.builder().role(Role.ASSISTANT.getValue())
  432 + .content("").build();
  433 + assistantMessage.setToolCalls(message.getToolCalls());
  434 +
  435 + return assistantMessage;
  436 + }
  437 +
  438 + @Override
  439 + protected GenerationResult doChatCompletion(ConversationParam request) {
  440 +
  441 + GenerationResult result;
  442 + try {
  443 + result = generation.call(request);
  444 + }
  445 + catch (NoApiKeyException | InputRequiredException e) {
  446 + throw new RuntimeException(e);
  447 + }
  448 +
  449 + return result;
  450 + }
  451 +
  452 + @Override
  453 + protected Flux<GenerationResult> doChatCompletionStream(ConversationParam request) {
  454 + final Flowable<GenerationResult> genRes;
  455 + try {
  456 + genRes = generation.streamCall(request);
  457 + }
  458 + catch (NoApiKeyException | InputRequiredException e) {
  459 + logger.warn("TongYi chat client: " + e.getMessage());
  460 + throw new TongYiException(e.getMessage());
  461 + }
  462 + return Flux.from(genRes);
  463 +
  464 + }
  465 +
  466 + @Override
  467 + protected boolean isToolFunctionCall(GenerationResult response) {
  468 +
  469 + if (response == null || CollectionUtils.isEmpty(response.getOutput().getChoices())) {
  470 +
  471 + return false;
  472 + }
  473 + var choice = response.getOutput().getChoices().get(0);
  474 + if (choice == null || choice.getFinishReason() == null) {
  475 +
  476 + return false;
  477 + }
  478 +
  479 + return Objects.equals(choice.getFinishReason(), ApiKeywords.TOOL_CALLS);
  480 + }
  481 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatOptions.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.chat;
  18 +
  19 +import com.alibaba.dashscope.aigc.generation.Generation;
  20 +import com.alibaba.dashscope.aigc.generation.GenerationParam;
  21 +import org.springframework.ai.chat.prompt.ChatOptions;
  22 +import org.springframework.ai.model.function.FunctionCallback;
  23 +import org.springframework.ai.model.function.FunctionCallingOptions;
  24 +import org.springframework.util.Assert;
  25 +
  26 +import java.util.*;
  27 +
  28 +/**
  29 + * @author yuluo
  30 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  31 + * @since 2023.0.1.0
  32 + */
  33 +
  34 +public class TongYiChatOptions implements FunctionCallingOptions, ChatOptions {
  35 +
  36 + /**
  37 + * TongYi Models.
  38 + * {@link Generation.Models}
  39 + */
  40 + private String model = Generation.Models.QWEN_TURBO;
  41 +
  42 + /**
  43 + * The random number seed used in generation, the user controls the randomness of the content generated by the model.
  44 + * seed supports unsigned 64-bit integers, with a default value of 1234.
  45 + * when using seed, the model will generate the same or similar results as much as possible, but there is currently no guarantee that the results will be exactly the same each time.
  46 + */
  47 + private Integer seed = 1234;
  48 +
  49 + /**
  50 + * Used to specify the maximum number of tokens that the model can generate when generating content,
  51 + * it defines the upper limit of generation but does not guarantee that this number will be generated every time.
  52 + * For qwen-turbo the maximum and default values are 1500 tokens.
  53 + * The qwen-max, qwen-max-1201, qwen-max-longcontext, and qwen-plus models have a maximum and default value of 2000 tokens.
  54 + */
  55 + private Integer maxTokens = 1500;
  56 +
  57 + /**
  58 + * The generation process kernel sampling method probability threshold,
  59 + * for example, takes the value of 0.8, only retains the smallest set of the most probable tokens with probabilities that add up to greater than or equal to 0.8 as the candidate set.
  60 + * The range of values is (0,1.0), the larger the value, the higher the randomness of generation; the lower the value, the higher the certainty of generation.
  61 + */
  62 + private Double topP = 0.8;
  63 +
  64 + /**
  65 + * The size of the sampling candidate set at the time of generation.
  66 + * For example, with a value of 50, only the 50 highest scoring tokens in a single generation will form a randomly sampled candidate set.
  67 + * The larger the value, the higher the randomness of the generation; the smaller the value, the higher the certainty of the generation.
  68 + * This parameter is not passed by default, and a value of None or when top_k is greater than 100 indicates that the top_k policy is not enabled,
  69 + * at which time, only the top_p policy is in effect.
  70 + */
  71 + private Integer topK;
  72 +
  73 + /**
  74 + * Used to control the repeatability of model generation.
  75 + * Increasing repetition_penalty reduces the repetition of model generation. 1.0 means no penalty.
  76 + */
  77 + private Double repetitionPenalty = 1.1;
  78 +
  79 + /**
  80 + * is used to control the degree of randomness and diversity.
  81 + * Specifically, the temperature value controls the extent to which the probability distribution of each candidate word is smoothed when generating text.
  82 + * Higher values of temperature reduce the peak of the probability distribution, allowing more low-probability words to be selected and generating more diverse results,
  83 + * while lower values of temperature enhance the peak of the probability distribution, making it easier for high-probability words to be selected and generating more certain results.
  84 + * Range: [0, 2), 0 is not recommended, meaningless.
  85 + * java version >= 2.5.1
  86 + */
  87 + private Double temperature = 0.85;
  88 +
  89 + /**
  90 + * The stop parameter is used to realize precise control of the content generation process, automatically stopping when the generated content is about to contain the specified string or token_ids,
  91 + * and the generated content does not contain the specified content.
  92 + * For example, if stop is specified as "Hello", it means stop when "Hello" will be generated; if stop is specified as [37763, 367], it means stop when "Observation" will be generated.
  93 + * The stop parameter can be passed as a list of arrays of strings or token_ids to support the scenario of using multiple stops.
  94 + * Explanation: Do not mix strings and token_ids in list mode, the element types should be the same in list mode.
  95 + */
  96 + private List<String> stop;
  97 +
  98 + /**
  99 + * Whether or not to use stream output. When outputting the result in stream mode, the interface returns the result as generator,
  100 + * you need to iterate to get the result, the default output is the whole sequence of the current generation for each output,
  101 + * the last output is the final result of all the generation, you can change the output mode to non-incremental output by the parameter incremental_output to False.
  102 + */
  103 + private Boolean stream = false;
  104 +
  105 + /**
  106 + * The model has a built-in Internet search service.
  107 + * This parameter controls whether the model refers to the use of Internet search results when generating text. The values are as follows:
  108 + * True: enable internet search, the model will use the search result as the reference information in the text generation process, but the model will "judge by itself" whether to use the internet search result based on its internal logic.
  109 + * False (default): Internet search is disabled.
  110 + */
  111 + private Boolean enableSearch = false;
  112 +
  113 + /**
  114 + * [text|message], defaults to text, when it is message,
  115 + * the output refers to the message result example.
  116 + * It is recommended to prioritize the use of message format.
  117 + */
  118 + private String resultFormat = GenerationParam.ResultFormat.MESSAGE;
  119 +
  120 + /**
  121 + * Control the streaming output mode, that is, the content will contain the content has been output;
  122 + * set to True, will open the incremental output mode, the output will not contain the content has been output,
  123 + * you need to splice the whole output, refer to the streaming output sample code.
  124 + */
  125 + private Boolean incrementalOutput = false;
  126 +
  127 + /**
  128 + * A list of tools that the model can optionally call.
  129 + * Currently only functions are supported, and even if multiple functions are entered, the model will only select one to generate the result.
  130 + */
  131 + private List<String> tools;
  132 +
  133 + @Override
  134 + public Float getTemperature() {
  135 +
  136 + return this.temperature.floatValue();
  137 + }
  138 +
  139 + public void setTemperature(Float temperature) {
  140 +
  141 + this.temperature = temperature.doubleValue();
  142 + }
  143 +
  144 + @Override
  145 + public Float getTopP() {
  146 +
  147 + return this.topP.floatValue();
  148 + }
  149 +
  150 + public void setTopP(Float topP) {
  151 +
  152 + this.topP = topP.doubleValue();
  153 + }
  154 +
  155 + @Override
  156 + public Integer getTopK() {
  157 +
  158 + return this.topK;
  159 + }
  160 +
  161 + public void setTopK(Integer topK) {
  162 +
  163 + this.topK = topK;
  164 + }
  165 +
  166 + public String getModel() {
  167 +
  168 + return model;
  169 + }
  170 +
  171 + public void setModel(String model) {
  172 +
  173 + this.model = model;
  174 + }
  175 +
  176 + public Integer getSeed() {
  177 +
  178 + return seed;
  179 + }
  180 +
  181 + public String getResultFormat() {
  182 +
  183 + return resultFormat;
  184 + }
  185 +
  186 + public void setResultFormat(String resultFormat) {
  187 +
  188 + this.resultFormat = resultFormat;
  189 + }
  190 +
  191 + public void setSeed(Integer seed) {
  192 +
  193 + this.seed = seed;
  194 + }
  195 +
  196 + public Integer getMaxTokens() {
  197 +
  198 + return maxTokens;
  199 + }
  200 +
  201 + public void setMaxTokens(Integer maxTokens) {
  202 +
  203 + this.maxTokens = maxTokens;
  204 + }
  205 +
  206 + public Float getRepetitionPenalty() {
  207 +
  208 + return repetitionPenalty.floatValue();
  209 + }
  210 +
  211 + public void setRepetitionPenalty(Float repetitionPenalty) {
  212 +
  213 + this.repetitionPenalty = repetitionPenalty.doubleValue();
  214 + }
  215 +
  216 + public List<String> getStop() {
  217 +
  218 + return stop;
  219 + }
  220 +
  221 + public void setStop(List<String> stop) {
  222 +
  223 + this.stop = stop;
  224 + }
  225 +
  226 + public Boolean getStream() {
  227 +
  228 + return stream;
  229 + }
  230 +
  231 + public void setStream(Boolean stream) {
  232 +
  233 + this.stream = stream;
  234 + }
  235 +
  236 + public Boolean getEnableSearch() {
  237 +
  238 + return enableSearch;
  239 + }
  240 +
  241 + public void setEnableSearch(Boolean enableSearch) {
  242 +
  243 + this.enableSearch = enableSearch;
  244 + }
  245 +
  246 + public Boolean getIncrementalOutput() {
  247 +
  248 + return incrementalOutput;
  249 + }
  250 +
  251 + public void setIncrementalOutput(Boolean incrementalOutput) {
  252 +
  253 + this.incrementalOutput = incrementalOutput;
  254 + }
  255 +
  256 + public List<String> getTools() {
  257 +
  258 + return tools;
  259 + }
  260 +
  261 + public void setTools(List<String> tools) {
  262 +
  263 + this.tools = tools;
  264 + }
  265 +
  266 + private List<FunctionCallback> functionCallbacks = new ArrayList<>();
  267 +
  268 + private Set<String> functions = new HashSet<>();
  269 +
  270 + @Override
  271 + public List<FunctionCallback> getFunctionCallbacks() {
  272 +
  273 + return this.functionCallbacks;
  274 + }
  275 +
  276 + @Override
  277 + public void setFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
  278 +
  279 + this.functionCallbacks = functionCallbacks;
  280 + }
  281 +
  282 + @Override
  283 + public Set<String> getFunctions() {
  284 +
  285 + return this.functions;
  286 + }
  287 +
  288 + @Override
  289 + public void setFunctions(Set<String> functions) {
  290 +
  291 + this.functions = functions;
  292 + }
  293 +
  294 + @Override
  295 + public boolean equals(Object o) {
  296 +
  297 + if (this == o) {
  298 + return true;
  299 + }
  300 +
  301 + if (o == null || getClass() != o.getClass()) {
  302 + return false;
  303 + }
  304 +
  305 + TongYiChatOptions that = (TongYiChatOptions) o;
  306 +
  307 + return Objects.equals(model, that.model)
  308 + && Objects.equals(seed, that.seed)
  309 + && Objects.equals(maxTokens, that.maxTokens)
  310 + && Objects.equals(topP, that.topP)
  311 + && Objects.equals(topK, that.topK)
  312 + && Objects.equals(repetitionPenalty, that.repetitionPenalty)
  313 + && Objects.equals(temperature, that.temperature)
  314 + && Objects.equals(stop, that.stop)
  315 + && Objects.equals(stream, that.stream)
  316 + && Objects.equals(enableSearch, that.enableSearch)
  317 + && Objects.equals(resultFormat, that.resultFormat)
  318 + && Objects.equals(incrementalOutput, that.incrementalOutput)
  319 + && Objects.equals(tools, that.tools)
  320 + && Objects.equals(functionCallbacks, that.functionCallbacks)
  321 + && Objects.equals(functions, that.functions);
  322 + }
  323 +
  324 + @Override
  325 + public int hashCode() {
  326 +
  327 + return Objects.hash(
  328 + model,
  329 + seed,
  330 + maxTokens,
  331 + topP,
  332 + topK,
  333 + repetitionPenalty,
  334 + temperature,
  335 + stop,
  336 + stream,
  337 + enableSearch,
  338 + resultFormat,
  339 + incrementalOutput,
  340 + tools,
  341 + functionCallbacks,
  342 + functions
  343 + );
  344 + }
  345 +
  346 + @Override
  347 + public String toString() {
  348 +
  349 + final StringBuilder sb = new StringBuilder("TongYiChatOptions{");
  350 +
  351 + sb.append(", model='").append(model).append('\'');
  352 + sb.append(", seed=").append(seed);
  353 + sb.append(", maxTokens=").append(maxTokens);
  354 + sb.append(", topP=").append(topP);
  355 + sb.append(", topK=").append(topK);
  356 + sb.append(", repetitionPenalty=").append(repetitionPenalty);
  357 + sb.append(", temperature=").append(temperature);
  358 + sb.append(", stop=").append(stop);
  359 + sb.append(", stream=").append(stream);
  360 + sb.append(", enableSearch=").append(enableSearch);
  361 + sb.append(", resultFormat='").append(resultFormat).append('\'');
  362 + sb.append(", incrementalOutput=").append(incrementalOutput);
  363 + sb.append(", tools=").append(tools);
  364 + sb.append(", functionCallbacks=").append(functionCallbacks);
  365 + sb.append(", functions=").append(functions);
  366 + sb.append('}');
  367 +
  368 + return sb.toString();
  369 + }
  370 +
  371 + public static Builder builder() {
  372 +
  373 + return new Builder();
  374 + }
  375 +
  376 + public static class Builder {
  377 +
  378 + protected TongYiChatOptions options;
  379 +
  380 + public Builder() {
  381 +
  382 + this.options = new TongYiChatOptions();
  383 + }
  384 +
  385 + public Builder(TongYiChatOptions options) {
  386 +
  387 + this.options = options;
  388 + }
  389 +
  390 + public Builder withModel(String model) {
  391 + this.options.model = model;
  392 + return this;
  393 + }
  394 +
  395 + public Builder withMaxTokens(Integer maxTokens) {
  396 + this.options.maxTokens = maxTokens;
  397 + return this;
  398 + }
  399 +
  400 + public Builder withResultFormat(String rf) {
  401 + this.options.resultFormat = rf;
  402 + return this;
  403 + }
  404 +
  405 + public Builder withEnableSearch(Boolean enableSearch) {
  406 + this.options.enableSearch = enableSearch;
  407 + return this;
  408 + }
  409 +
  410 + public Builder withFunctionCallbacks(List<FunctionCallback> functionCallbacks) {
  411 + this.options.functionCallbacks = functionCallbacks;
  412 + return this;
  413 + }
  414 +
  415 + public Builder withFunctions(Set<String> functionNames) {
  416 + Assert.notNull(functionNames, "Function names must not be null");
  417 + this.options.functions = functionNames;
  418 + return this;
  419 + }
  420 +
  421 + public Builder withFunction(String functionName) {
  422 + Assert.hasText(functionName, "Function name must not be empty");
  423 + this.options.functions.add(functionName);
  424 + return this;
  425 + }
  426 +
  427 + public Builder withSeed(Integer seed) {
  428 + this.options.seed = seed;
  429 + return this;
  430 + }
  431 +
  432 + public Builder withStop(List<String> stop) {
  433 + this.options.stop = stop;
  434 + return this;
  435 + }
  436 +
  437 + public Builder withTemperature(Double temperature) {
  438 + this.options.temperature = temperature;
  439 + return this;
  440 + }
  441 +
  442 + public Builder withTopP(Double topP) {
  443 + this.options.topP = topP;
  444 + return this;
  445 + }
  446 +
  447 + public Builder withTopK(Integer topK) {
  448 + this.options.topK = topK;
  449 + return this;
  450 + }
  451 +
  452 + public Builder withRepetitionPenalty(Double repetitionPenalty) {
  453 + this.options.repetitionPenalty = repetitionPenalty;
  454 + return this;
  455 + }
  456 +
  457 + public TongYiChatOptions build() {
  458 +
  459 + return this.options;
  460 + }
  461 + }
  462 +
  463 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatProperties.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.chat;
  18 +
  19 +import com.alibaba.dashscope.aigc.generation.Generation;
  20 +import com.alibaba.dashscope.aigc.generation.GenerationParam;
  21 +import org.springframework.boot.context.properties.ConfigurationProperties;
  22 +import org.springframework.boot.context.properties.NestedConfigurationProperty;
  23 +
  24 +import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION;
  25 +
  26 +/**
  27 + * @author yuluo
  28 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  29 + * @since 2023.0.1.0
  30 + */
  31 +
  32 +@ConfigurationProperties(TongYiChatProperties.CONFIG_PREFIX)
  33 +public class TongYiChatProperties {
  34 +
  35 + /**
  36 + * Spring Cloud Alibaba AI configuration prefix.
  37 + */
  38 + public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "chat";
  39 +
  40 + /**
  41 + * Default TongYi Chat model.
  42 + */
  43 + public static final String DEFAULT_DEPLOYMENT_NAME = Generation.Models.QWEN_TURBO;
  44 +
  45 + /**
  46 + * Default temperature speed.
  47 + */
  48 + private static final Double DEFAULT_TEMPERATURE = 0.8;
  49 +
  50 + /**
  51 + * Enable TongYiQWEN ai chat client.
  52 + */
  53 + private boolean enabled = true;
  54 +
  55 + @NestedConfigurationProperty
  56 + private TongYiChatOptions options = TongYiChatOptions.builder()
  57 + .withModel(DEFAULT_DEPLOYMENT_NAME)
  58 + .withTemperature(DEFAULT_TEMPERATURE)
  59 + .withEnableSearch(true)
  60 + .withResultFormat(GenerationParam.ResultFormat.MESSAGE)
  61 + .build();
  62 +
  63 + public TongYiChatOptions getOptions() {
  64 +
  65 + return this.options;
  66 + }
  67 +
  68 + public void setOptions(TongYiChatOptions options) {
  69 +
  70 + this.options = options;
  71 + }
  72 +
  73 + public boolean isEnabled() {
  74 +
  75 + return this.enabled;
  76 + }
  77 +
  78 + public void setEnabled(boolean enabled) {
  79 +
  80 + this.enabled = enabled;
  81 + }
  82 +
  83 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/constants/TongYiConstants.java 0 → 100644
  1 +/*
  2 + * Copyright 2024-2025 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.common.constants;
  18 +
  19 +/**
  20 + * @author yuluo
  21 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  22 + */
  23 +
  24 +public final class TongYiConstants {
  25 +
  26 + private TongYiConstants() {
  27 + }
  28 +
  29 + /**
  30 + * Spring Cloud Alibaba AI configuration prefix.
  31 + */
  32 + public static final String SCA_AI_CONFIGURATION = "spring.cloud.ai.tongyi.";
  33 +
  34 + /**
  35 + * Spring Cloud Alibaba AI constants prefix.
  36 + */
  37 + public static final String SCA_AI = "SPRING_CLOUD_ALIBABA_";
  38 +
  39 + /**
  40 + * TongYi AI apikey env name.
  41 + */
  42 + public static final String SCA_AI_TONGYI_API_KEY = SCA_AI + "TONGYI_API_KEY";
  43 +
  44 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiException.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.common.exception;
  18 +
  19 +/**
  20 + * TongYi models runtime exception.
  21 + *
  22 + * @author yuluo
  23 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  24 + * @since 2023.0.1.0
  25 + */
  26 +
  27 +public class TongYiException extends RuntimeException {
  28 +
  29 + public TongYiException(String message) {
  30 +
  31 + super(message);
  32 + }
  33 +
  34 + public TongYiException(String message, Throwable cause) {
  35 +
  36 + super(message, cause);
  37 + }
  38 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiImagesException.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.common.exception;
  18 +
  19 +/**
  20 + * TongYi models images exception.
  21 + *
  22 + * @author yuluo
  23 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  24 + * @since 2023.0.1.0
  25 + */
  26 +
  27 +public class TongYiImagesException extends TongYiException {
  28 +
  29 + public TongYiImagesException(String message) {
  30 +
  31 + super(message);
  32 + }
  33 +
  34 + public TongYiImagesException(String message, Throwable cause) {
  35 +
  36 + super(message, cause);
  37 + }
  38 +
  39 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiEmbeddingOptions.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.embedding;
  18 +
  19 +import com.alibaba.dashscope.embeddings.TextEmbeddingParam;
  20 +import org.springframework.ai.embedding.EmbeddingOptions;
  21 +
  22 +import java.util.List;
  23 +
  24 +/**
  25 + * @author why_ohh
  26 + * @author yuluo
  27 + * @author <a href="mailto:550588941@qq.com">why_ohh</a>
  28 + * @since 2023.0.1.0
  29 + */
  30 +
  31 +public final class TongYiEmbeddingOptions implements EmbeddingOptions {
  32 +
  33 + private List<String> texts;
  34 +
  35 + private TextEmbeddingParam.TextType textType;
  36 +
  37 + public List<String> getTexts() {
  38 + return texts;
  39 + }
  40 +
  41 + public void setTexts(List<String> texts) {
  42 + this.texts = texts;
  43 + }
  44 +
  45 + public TextEmbeddingParam.TextType getTextType() {
  46 + return textType;
  47 + }
  48 +
  49 + public void setTextType(TextEmbeddingParam.TextType textType) {
  50 + this.textType = textType;
  51 + }
  52 +
  53 + public static Builder builder() {
  54 + return new Builder();
  55 + }
  56 +
  57 + public final static class Builder {
  58 +
  59 + private final TongYiEmbeddingOptions options;
  60 +
  61 + private Builder() {
  62 + this.options = new TongYiEmbeddingOptions();
  63 + }
  64 +
  65 + public Builder withtexts(List<String> texts) {
  66 +
  67 + options.setTexts(texts);
  68 + return this;
  69 + }
  70 +
  71 + public Builder withtextType(TextEmbeddingParam.TextType textType) {
  72 +
  73 + options.setTextType(textType);
  74 + return this;
  75 + }
  76 +
  77 + public TongYiEmbeddingOptions build() {
  78 +
  79 + return options;
  80 + }
  81 +
  82 + }
  83 +
  84 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.embedding;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.common.exception.TongYiException;
  20 +import com.alibaba.cloud.ai.tongyi.metadata.TongYiTextEmbeddingResponseMetadata;
  21 +import com.alibaba.dashscope.embeddings.TextEmbedding;
  22 +import com.alibaba.dashscope.embeddings.TextEmbeddingParam;
  23 +import com.alibaba.dashscope.embeddings.TextEmbeddingResult;
  24 +import com.alibaba.dashscope.embeddings.TextEmbeddingResultItem;
  25 +import com.alibaba.dashscope.exception.InputRequiredException;
  26 +import com.alibaba.dashscope.exception.NoApiKeyException;
  27 +import org.slf4j.Logger;
  28 +import org.slf4j.LoggerFactory;
  29 +import org.springframework.ai.document.Document;
  30 +import org.springframework.ai.document.MetadataMode;
  31 +import org.springframework.ai.embedding.AbstractEmbeddingModel;
  32 +import org.springframework.ai.embedding.Embedding;
  33 +import org.springframework.ai.embedding.EmbeddingRequest;
  34 +import org.springframework.ai.embedding.EmbeddingResponse;
  35 +import org.springframework.util.Assert;
  36 +
  37 +import java.util.List;
  38 +import java.util.stream.Collectors;
  39 +
  40 +/**
  41 + * {@link TongYiTextEmbeddingModel} implementation for {@literal Alibaba DashScope}.
  42 + *
  43 + * @author why_ohh
  44 + * @author yuluo
  45 + * @author <a href="mailto:550588941@qq.com">why_ohh</a>
  46 + * @since 2023.0.1.0
  47 + */
  48 +
  49 +public class TongYiTextEmbeddingModel extends AbstractEmbeddingModel {
  50 +
  51 + private final Logger logger = LoggerFactory.getLogger(TongYiTextEmbeddingModel.class);
  52 +
  53 + /**
  54 + * TongYi Text Embedding client.
  55 + */
  56 + private final TextEmbedding textEmbedding;
  57 +
  58 + /**
  59 + * {@link MetadataMode}.
  60 + */
  61 + private final MetadataMode metadataMode;
  62 +
  63 + private final TongYiEmbeddingOptions defaultOptions;
  64 +
  65 + public TongYiTextEmbeddingModel(TextEmbedding textEmbedding) {
  66 +
  67 + this(textEmbedding, MetadataMode.EMBED);
  68 + }
  69 +
  70 + public TongYiTextEmbeddingModel(TextEmbedding textEmbedding, MetadataMode metadataMode) {
  71 +
  72 + this(textEmbedding, metadataMode,
  73 + TongYiEmbeddingOptions.builder()
  74 + .withtextType(TextEmbeddingParam.TextType.DOCUMENT)
  75 + .build()
  76 + );
  77 + }
  78 +
  79 + public TongYiTextEmbeddingModel(
  80 + TextEmbedding textEmbedding,
  81 + MetadataMode metadataMode,
  82 + TongYiEmbeddingOptions options
  83 + ) {
  84 + Assert.notNull(textEmbedding, "textEmbedding must not be null");
  85 + Assert.notNull(metadataMode, "Metadata mode must not be null");
  86 + Assert.notNull(options, "TongYiEmbeddingOptions must not be null");
  87 +
  88 + this.metadataMode = metadataMode;
  89 + this.textEmbedding = textEmbedding;
  90 + this.defaultOptions = options;
  91 + }
  92 +
  93 + public TongYiEmbeddingOptions getDefaultOptions() {
  94 +
  95 + return this.defaultOptions;
  96 + }
  97 +
  98 + @Override
  99 + public List<Double> embed(Document document) {
  100 +
  101 + return this.call(
  102 + new EmbeddingRequest(
  103 + List.of(document.getFormattedContent(this.metadataMode)),
  104 + null)
  105 + ).getResults().stream()
  106 + .map(Embedding::getOutput)
  107 + .flatMap(List::stream)
  108 + .toList();
  109 + }
  110 +
  111 + @Override
  112 + public EmbeddingResponse call(EmbeddingRequest request) {
  113 +
  114 + TextEmbeddingParam embeddingParams = toEmbeddingParams(request);
  115 + logger.debug("Embedding request: {}", embeddingParams);
  116 + TextEmbeddingResult resp;
  117 +
  118 + try {
  119 + resp = textEmbedding.call(embeddingParams);
  120 + }
  121 + catch (NoApiKeyException e) {
  122 + throw new TongYiException(e.getMessage());
  123 + }
  124 +
  125 + return genEmbeddingResp(resp);
  126 + }
  127 +
  128 + private EmbeddingResponse genEmbeddingResp(TextEmbeddingResult result) {
  129 +
  130 + return new EmbeddingResponse(
  131 + genEmbeddingList(result.getOutput().getEmbeddings()),
  132 + TongYiTextEmbeddingResponseMetadata.from(result.getUsage())
  133 + );
  134 + }
  135 +
  136 + private List<Embedding> genEmbeddingList(List<TextEmbeddingResultItem> embeddings) {
  137 +
  138 + return embeddings.stream()
  139 + .map(embedding ->
  140 + new Embedding(
  141 + embedding.getEmbedding(),
  142 + embedding.getTextIndex()
  143 + ))
  144 + .collect(Collectors.toList());
  145 + }
  146 +
  147 + /**
  148 + * We recommend setting the model parameters by passing the embedding parameters through the code;
  149 + * yml configuration compatibility is not considered here.
  150 + * It is not recommended that users set parameters from yml,
  151 + * as this reduces the flexibility of the configuration.
  152 + * Because the model name keeps changing, strings are used here and constants are undefined:
  153 + * Model list: <a href="https://help.aliyun.com/zh/dashscope/developer-reference/text-embedding-quick-start">Text Embedding Model List</a>
  154 + * @param requestOptions Client params. {@link EmbeddingRequest}
  155 + * @return {@link TextEmbeddingParam}
  156 + */
  157 + private TextEmbeddingParam toEmbeddingParams(EmbeddingRequest requestOptions) {
  158 +
  159 + TextEmbeddingParam tongYiEmbeddingParams = TextEmbeddingParam.builder()
  160 + .texts(requestOptions.getInstructions())
  161 + .textType(defaultOptions.getTextType() != null ? defaultOptions.getTextType() : TextEmbeddingParam.TextType.DOCUMENT)
  162 + .model("text-embedding-v1")
  163 + .build();
  164 +
  165 + try {
  166 + tongYiEmbeddingParams.validate();
  167 + }
  168 + catch (InputRequiredException e) {
  169 + throw new TongYiException(e.getMessage());
  170 + }
  171 +
  172 + return tongYiEmbeddingParams;
  173 + }
  174 +
  175 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingProperties.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.embedding;
  18 +
  19 +import org.springframework.boot.context.properties.ConfigurationProperties;
  20 +
  21 +import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION;
  22 +
  23 +/**
  24 + * @author why_ohh
  25 + * @author yuluo
  26 + * @author <a href="mailto:550588941@qq.com">why_ohh</a>
  27 + * @since 2023.0.1.0
  28 + */
  29 +
  30 +@ConfigurationProperties(TongYiTextEmbeddingProperties.CONFIG_PREFIX)
  31 +public class TongYiTextEmbeddingProperties {
  32 +
  33 + /**
  34 + * Prefix of TongYi Text Embedding properties.
  35 + */
  36 + public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "embedding";
  37 +
  38 + private boolean enabled = true;
  39 +
  40 + public boolean isEnabled() {
  41 +
  42 + return this.enabled;
  43 + }
  44 +
  45 + public void setEnabled(boolean enabled) {
  46 +
  47 + this.enabled = enabled;
  48 + }
  49 +
  50 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesModel.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.image;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.common.exception.TongYiImagesException;
  20 +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
  21 +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam;
  22 +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult;
  23 +import com.alibaba.dashscope.exception.NoApiKeyException;
  24 +import org.slf4j.Logger;
  25 +import org.slf4j.LoggerFactory;
  26 +import org.springframework.ai.image.*;
  27 +import org.springframework.util.Assert;
  28 +
  29 +import java.io.ByteArrayOutputStream;
  30 +import java.net.URL;
  31 +import java.util.Base64;
  32 +import java.util.stream.Collectors;
  33 +
  34 +import static com.alibaba.cloud.ai.tongyi.metadata.TongYiImagesResponseMetadata.from;
  35 +
  36 +/**
  37 + * TongYiImagesClient is a class that implements the ImageClient interface. It provides a
  38 + * client for calling the TongYi AI image generation API.
  39 + *
  40 + * @author yuluo
  41 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  42 + * @since 2023.0.1.0
  43 + */
  44 +
  45 +public class TongYiImagesModel implements ImageModel {
  46 +
  47 + private final Logger logger = LoggerFactory.getLogger(TongYiImagesModel.class);
  48 +
  49 + /**
  50 + * Gen Images API.
  51 + */
  52 + private final ImageSynthesis imageSynthesis;
  53 +
  54 + /**
  55 + * TongYi Gen images properties.
  56 + */
  57 + private TongYiImagesOptions defaultOptions;
  58 +
  59 + /**
  60 + * Adapt TongYi images api size properties.
  61 + */
  62 + private final String sizeConnection = "*";
  63 +
  64 + /**
  65 + * Get default images options.
  66 + *
  67 + * @return Default TongYiImagesOptions.
  68 + */
  69 + public TongYiImagesOptions getDefaultOptions() {
  70 +
  71 + return this.defaultOptions;
  72 + }
  73 +
  74 + /**
  75 + * TongYiImagesClient constructor.
  76 + * @param imageSynthesis the image synthesis
  77 + * {@link ImageSynthesis}
  78 + */
  79 + public TongYiImagesModel(ImageSynthesis imageSynthesis) {
  80 +
  81 + this(imageSynthesis, TongYiImagesOptions.
  82 + builder()
  83 + .withModel(ImageSynthesis.Models.WANX_V1)
  84 + .withN(1)
  85 + .build()
  86 + );
  87 + }
  88 +
  89 + /**
  90 + * TongYiImagesClient constructor.
  91 + * @param imageSynthesis {@link ImageSynthesis}
  92 + * @param imagesOptions {@link TongYiImagesOptions}
  93 + */
  94 + public TongYiImagesModel(ImageSynthesis imageSynthesis, TongYiImagesOptions imagesOptions) {
  95 +
  96 + Assert.notNull(imageSynthesis, "ImageSynthesis must not be null");
  97 + Assert.notNull(imagesOptions, "TongYiImagesOptions must not be null");
  98 +
  99 + this.imageSynthesis = imageSynthesis;
  100 + this.defaultOptions = imagesOptions;
  101 + }
  102 +
  103 + /**
  104 + * Call the TongYi images service.
  105 + * @param imagePrompt the image prompt.
  106 + * @return the image response.
  107 + * {@link ImageSynthesis#call(ImageSynthesisParam)}
  108 + */
  109 + @Override
  110 + public ImageResponse call(ImagePrompt imagePrompt) {
  111 +
  112 + ImageSynthesisResult result;
  113 + String prompt = imagePrompt.getInstructions().get(0).getText();
  114 + var imgParams = ImageSynthesisParam.builder()
  115 + .prompt("")
  116 + .model(ImageSynthesis.Models.WANX_V1)
  117 + .build();
  118 +
  119 + if (this.defaultOptions != null) {
  120 +
  121 + imgParams = merge(this.defaultOptions);
  122 + }
  123 +
  124 + if (imagePrompt.getOptions() != null) {
  125 +
  126 + imgParams = merge(toTingYiImageOptions(imagePrompt.getOptions()));
  127 + }
  128 + imgParams.setPrompt(prompt);
  129 +
  130 + try {
  131 + result = imageSynthesis.call(imgParams);
  132 + }
  133 + catch (NoApiKeyException e) {
  134 +
  135 + logger.error("TongYi models gen images failed: {}.", e.getMessage());
  136 + throw new TongYiImagesException(e.getMessage());
  137 + }
  138 +
  139 + return convert(result);
  140 + }
  141 +
  142 + public ImageSynthesisParam merge(TongYiImagesOptions target) {
  143 +
  144 + var builder = ImageSynthesisParam.builder();
  145 +
  146 + builder.model(this.defaultOptions.getModel() != null ? this.defaultOptions.getModel() : target.getModel());
  147 + builder.n(this.defaultOptions.getN() != null ? this.defaultOptions.getN() : target.getN());
  148 + builder.size((this.defaultOptions.getHeight() != null && this.defaultOptions.getWidth() != null)
  149 + ? this.defaultOptions.getHeight() + "*" + this.defaultOptions.getWidth()
  150 + : target.getHeight() + "*" + target.getWidth()
  151 + );
  152 +
  153 + // prompt is marked non-null but is null.
  154 + builder.prompt("");
  155 +
  156 + return builder.build();
  157 + }
  158 +
  159 + private ImageResponse convert(ImageSynthesisResult result) {
  160 +
  161 + return new ImageResponse(
  162 + result.getOutput().getResults().stream()
  163 + .flatMap(value -> value.entrySet().stream())
  164 + .map(entry -> {
  165 + String key = entry.getKey();
  166 + String value = entry.getValue();
  167 + try {
  168 + String base64Image = convertImageToBase64(value);
  169 + Image image = new Image(value, base64Image);
  170 + return new ImageGeneration(image);
  171 + }
  172 + catch (Exception e) {
  173 + throw new RuntimeException(e);
  174 + }
  175 + })
  176 + .collect(Collectors.toList()),
  177 + from(result)
  178 + );
  179 + }
  180 +
  181 + public TongYiImagesOptions toTingYiImageOptions(ImageOptions runtimeImageOptions) {
  182 +
  183 + var builder = TongYiImagesOptions.builder();
  184 +
  185 + if (runtimeImageOptions != null) {
  186 + if (runtimeImageOptions.getN() != null) {
  187 +
  188 + builder.withN(runtimeImageOptions.getN());
  189 + }
  190 + if (runtimeImageOptions.getModel() != null) {
  191 +
  192 + builder.withModel(runtimeImageOptions.getModel());
  193 + }
  194 + if (runtimeImageOptions.getHeight() != null) {
  195 +
  196 + builder.withHeight(runtimeImageOptions.getHeight());
  197 + }
  198 + if (runtimeImageOptions.getWidth() != null) {
  199 +
  200 + builder.withWidth(runtimeImageOptions.getWidth());
  201 + }
  202 +
  203 + // todo ImagesParams.
  204 + }
  205 +
  206 + return builder.build();
  207 + }
  208 +
  209 + /**
  210 + * Convert image to base64.
  211 + * @param imageUrl the image url.
  212 + * @return the base64 image.
  213 + * @throws Exception the exception.
  214 + */
  215 + public String convertImageToBase64(String imageUrl) throws Exception {
  216 +
  217 + var url = new URL(imageUrl);
  218 + var inputStream = url.openStream();
  219 + var outputStream = new ByteArrayOutputStream();
  220 + var buffer = new byte[4096];
  221 + int bytesRead;
  222 +
  223 + while ((bytesRead = inputStream.read(buffer)) != -1) {
  224 + outputStream.write(buffer, 0, bytesRead);
  225 + }
  226 +
  227 + var imageBytes = outputStream.toByteArray();
  228 +
  229 + String base64Image = Base64.getEncoder().encodeToString(imageBytes);
  230 +
  231 + inputStream.close();
  232 + outputStream.close();
  233 +
  234 + return base64Image;
  235 + }
  236 +
  237 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesOptions.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.image;
  18 +
  19 +import com.alibaba.cloud.ai.tongyi.common.exception.TongYiImagesException;
  20 +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
  21 +import org.springframework.ai.image.ImageOptions;
  22 +
  23 +import java.util.Objects;
  24 +
  25 +/**
  26 + * TongYi Image API options.
  27 + *
  28 + * @author yuluo
  29 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  30 + * @since 2023.0.1.0
  31 + */
  32 +
  33 +public class TongYiImagesOptions implements ImageOptions {
  34 +
  35 + /**
  36 + * Specify the model name, currently only wanx-v1 is supported.
  37 + */
  38 + private String model = ImageSynthesis.Models.WANX_V1;
  39 +
  40 + /**
  41 + * Gen images number.
  42 + */
  43 + private Integer n;
  44 +
  45 + /**
  46 + * The width of the generated images.
  47 + */
  48 + private Integer width = 1024;
  49 +
  50 + /**
  51 + * The height of the generated images.
  52 + */
  53 + private Integer height = 1024;
  54 +
  55 + @Override
  56 + public Integer getN() {
  57 +
  58 + return this.n;
  59 + }
  60 +
  61 + @Override
  62 + public String getModel() {
  63 +
  64 + return this.model;
  65 + }
  66 +
  67 + @Override
  68 + public Integer getWidth() {
  69 +
  70 + return this.width;
  71 + }
  72 +
  73 + @Override
  74 + public Integer getHeight() {
  75 +
  76 + return this.height;
  77 + }
  78 +
  79 + @Override
  80 + public String getResponseFormat() {
  81 +
  82 + throw new TongYiImagesException("unimplemented!");
  83 + }
  84 +
  85 + public void setModel(String model) {
  86 +
  87 + this.model = model;
  88 + }
  89 +
  90 + public void setN(Integer n) {
  91 +
  92 + this.n = n;
  93 + }
  94 +
  95 + public void setWidth(Integer width) {
  96 +
  97 + this.width = width;
  98 + }
  99 +
  100 + public void setHeight(Integer height) {
  101 +
  102 + this.height = height;
  103 + }
  104 +
  105 + @Override
  106 + public boolean equals(Object o) {
  107 + if (this == o) {
  108 +
  109 + return true;
  110 + }
  111 + if (o == null || getClass() != o.getClass()) {
  112 +
  113 + return false;
  114 + }
  115 +
  116 + TongYiImagesOptions that = (TongYiImagesOptions) o;
  117 +
  118 + return Objects.equals(model, that.model)
  119 + && Objects.equals(n, that.n)
  120 + && Objects.equals(width, that.width)
  121 + && Objects.equals(height, that.height);
  122 + }
  123 +
  124 + @Override
  125 + public int hashCode() {
  126 +
  127 + return Objects.hash(model, n, width, height);
  128 + }
  129 +
  130 + @Override
  131 + public String toString() {
  132 +
  133 + final StringBuilder sb = new StringBuilder("TongYiImagesOptions{");
  134 +
  135 + sb.append("model='").append(model).append('\'');
  136 + sb.append(", n=").append(n);
  137 + sb.append(", width=").append(width);
  138 + sb.append(", height=").append(height);
  139 + sb.append('}');
  140 +
  141 + return sb.toString();
  142 + }
  143 +
  144 + public static Builder builder() {
  145 + return new Builder();
  146 + }
  147 +
  148 + public final static class Builder {
  149 +
  150 + private final TongYiImagesOptions options;
  151 +
  152 + private Builder() {
  153 + this.options = new TongYiImagesOptions();
  154 + }
  155 +
  156 + public Builder withN(Integer n) {
  157 +
  158 + options.setN(n);
  159 + return this;
  160 + }
  161 +
  162 + public Builder withModel(String model) {
  163 +
  164 + options.setModel(model);
  165 + return this;
  166 + }
  167 +
  168 + public Builder withWidth(Integer width) {
  169 +
  170 + options.setWidth(width);
  171 + return this;
  172 + }
  173 +
  174 + public Builder withHeight(Integer height) {
  175 +
  176 + options.setHeight(height);
  177 + return this;
  178 + }
  179 +
  180 + public TongYiImagesOptions build() {
  181 +
  182 + return options;
  183 + }
  184 +
  185 + }
  186 +
  187 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesProperties.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.image;
  18 +
  19 +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
  20 +import org.springframework.boot.context.properties.ConfigurationProperties;
  21 +import org.springframework.boot.context.properties.NestedConfigurationProperty;
  22 +
  23 +import static com.alibaba.cloud.ai.tongyi.common.constants.TongYiConstants.SCA_AI_CONFIGURATION;
  24 +
  25 +/**
  26 + * TongYi Image API properties.
  27 + *
  28 + * @author yuluo
  29 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  30 + * @since 2023.0.1.0
  31 + */
  32 +
  33 +@ConfigurationProperties(TongYiImagesProperties.CONFIG_PREFIX)
  34 +public class TongYiImagesProperties {
  35 +
  36 + /**
  37 + * Spring Cloud Alibaba AI configuration prefix.
  38 + */
  39 + public static final String CONFIG_PREFIX = SCA_AI_CONFIGURATION + "images";
  40 +
  41 + /**
  42 + * Default TongYi Chat model.
  43 + */
  44 + public static final String DEFAULT_IMAGES_MODEL_NAME = ImageSynthesis.Models.WANX_V1;
  45 +
  46 + /**
  47 + * Enable TongYiQWEN ai images client.
  48 + */
  49 + private boolean enabled = true;
  50 +
  51 + @NestedConfigurationProperty
  52 + private TongYiImagesOptions options = TongYiImagesOptions.builder()
  53 + .withModel(DEFAULT_IMAGES_MODEL_NAME)
  54 + .withN(1)
  55 + .build();
  56 +
  57 + public TongYiImagesOptions getOptions() {
  58 +
  59 + return this.options;
  60 + }
  61 +
  62 + public void setOptions(TongYiImagesOptions options) {
  63 +
  64 + this.options = options;
  65 + }
  66 +
  67 + public boolean isEnabled() {
  68 +
  69 + return this.enabled;
  70 + }
  71 +
  72 + public void setEnabled(boolean enabled) {
  73 +
  74 + this.enabled = enabled;
  75 + }
  76 +
  77 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiChatResponseMetadata.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.metadata;
  18 +
  19 +import com.alibaba.dashscope.aigc.generation.GenerationResult;
  20 +import org.springframework.ai.chat.metadata.ChatResponseMetadata;
  21 +import org.springframework.ai.chat.metadata.PromptMetadata;
  22 +import org.springframework.ai.chat.metadata.Usage;
  23 +import org.springframework.util.Assert;
  24 +
  25 +import java.util.HashMap;
  26 +
  27 +
  28 +
  29 +/**
  30 + * {@link ChatResponseMetadata} implementation for {@literal Alibaba DashScope}.
  31 + *
  32 + * @author yuluo
  33 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  34 + * @since 2023.0.1.0
  35 + */
  36 +
  37 +public class TongYiAiChatResponseMetadata extends HashMap<String, Object> implements ChatResponseMetadata {
  38 +
  39 + protected static final String AI_METADATA_STRING = "{ @type: %1$s, id: %2$s, usage: %3$s, rateLimit: %4$s }";
  40 +
  41 + @SuppressWarnings("all")
  42 + public static TongYiAiChatResponseMetadata from(GenerationResult chatCompletions,
  43 + PromptMetadata promptFilterMetadata) {
  44 +
  45 + Assert.notNull(chatCompletions, "Alibaba ai ChatCompletions must not be null");
  46 + String id = chatCompletions.getRequestId();
  47 + TongYiAiUsage usage = TongYiAiUsage.from(chatCompletions);
  48 +
  49 + return new TongYiAiChatResponseMetadata(
  50 + id,
  51 + usage,
  52 + promptFilterMetadata
  53 + );
  54 + }
  55 +
  56 + private final String id;
  57 +
  58 + private final Usage usage;
  59 +
  60 + private final PromptMetadata promptMetadata;
  61 +
  62 + protected TongYiAiChatResponseMetadata(String id, TongYiAiUsage usage, PromptMetadata promptMetadata) {
  63 +
  64 + this.id = id;
  65 + this.usage = usage;
  66 + this.promptMetadata = promptMetadata;
  67 + }
  68 +
  69 + public String getId() {
  70 + return this.id;
  71 + }
  72 +
  73 + @Override
  74 + public Usage getUsage() {
  75 + return this.usage;
  76 + }
  77 +
  78 + @Override
  79 + public PromptMetadata getPromptMetadata() {
  80 + return this.promptMetadata;
  81 + }
  82 +
  83 + @Override
  84 + public String toString() {
  85 +
  86 + return AI_METADATA_STRING.formatted(getClass().getTypeName(), getId(), getUsage(), getRateLimit());
  87 + }
  88 +
  89 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiUsage.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.metadata;
  18 +
  19 +import com.alibaba.dashscope.aigc.generation.GenerationResult;
  20 +import com.alibaba.dashscope.aigc.generation.GenerationUsage;
  21 +import org.springframework.ai.chat.metadata.Usage;
  22 +import org.springframework.util.Assert;
  23 +
  24 +/**
  25 + * {@link Usage} implementation for {@literal Alibaba DashScope}.
  26 + *
  27 + * @author yuluo
  28 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  29 + * @since 2023.0.1.0
  30 + */
  31 +
  32 +public class TongYiAiUsage implements Usage {
  33 +
  34 + private final GenerationUsage usage;
  35 +
  36 + public TongYiAiUsage(GenerationUsage usage) {
  37 +
  38 + Assert.notNull(usage, "GenerationUsage must not be null");
  39 + this.usage = usage;
  40 + }
  41 +
  42 + public static TongYiAiUsage from(GenerationResult chatCompletions) {
  43 +
  44 + Assert.notNull(chatCompletions, "ChatCompletions must not be null");
  45 + return from(chatCompletions.getUsage());
  46 + }
  47 +
  48 + public static TongYiAiUsage from(GenerationUsage usage) {
  49 +
  50 + return new TongYiAiUsage(usage);
  51 + }
  52 +
  53 + protected GenerationUsage getUsage() {
  54 +
  55 + return this.usage;
  56 + }
  57 +
  58 + @Override
  59 + public Long getPromptTokens() {
  60 +
  61 + throw new UnsupportedOperationException("Unimplemented method 'getPromptTokens'");
  62 + }
  63 +
  64 + @Override
  65 + public Long getGenerationTokens() {
  66 +
  67 + return this.getUsage().getOutputTokens().longValue();
  68 + }
  69 +
  70 + @Override
  71 + public Long getTotalTokens() {
  72 +
  73 + return this.getUsage().getTotalTokens().longValue();
  74 + }
  75 +
  76 + @Override
  77 + public String toString() {
  78 +
  79 + return this.getUsage().toString();
  80 + }
  81 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiImagesResponseMetadata.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.metadata;
  18 +
  19 +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult;
  20 +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisTaskMetrics;
  21 +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisUsage;
  22 +import org.springframework.ai.image.ImageResponseMetadata;
  23 +import org.springframework.util.Assert;
  24 +
  25 +import java.util.HashMap;
  26 +import java.util.Objects;
  27 +
  28 +/**
  29 + * @author yuluo
  30 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  31 + * @since 2023.0.1.0
  32 + */
  33 +
  34 +public class TongYiImagesResponseMetadata extends HashMap<String, Object> implements ImageResponseMetadata {
  35 +
  36 + private final Long created;
  37 +
  38 + private String taskId;
  39 +
  40 + private ImageSynthesisTaskMetrics metrics;
  41 +
  42 + private ImageSynthesisUsage usage;
  43 +
  44 + public static TongYiImagesResponseMetadata from(ImageSynthesisResult synthesisResult) {
  45 +
  46 + Assert.notNull(synthesisResult, "TongYiAiImageResponse must not be null");
  47 +
  48 + return new TongYiImagesResponseMetadata(
  49 + System.currentTimeMillis(),
  50 + synthesisResult.getOutput().getTaskMetrics(),
  51 + synthesisResult.getOutput().getTaskId(),
  52 + synthesisResult.getUsage()
  53 + );
  54 + }
  55 +
  56 + protected TongYiImagesResponseMetadata(
  57 + Long created,
  58 + ImageSynthesisTaskMetrics metrics,
  59 + String taskId,
  60 + ImageSynthesisUsage usage
  61 + ) {
  62 +
  63 + this.taskId = taskId;
  64 + this.metrics = metrics;
  65 + this.created = created;
  66 + this.usage = usage;
  67 + }
  68 +
  69 + public ImageSynthesisUsage getUsage() {
  70 + return usage;
  71 + }
  72 +
  73 + public void setUsage(ImageSynthesisUsage usage) {
  74 + this.usage = usage;
  75 + }
  76 +
  77 + @Override
  78 + public Long getCreated() {
  79 + return created;
  80 + }
  81 +
  82 + public String getTaskId() {
  83 + return taskId;
  84 + }
  85 +
  86 + public void setTaskId(String taskId) {
  87 + this.taskId = taskId;
  88 + }
  89 +
  90 + public ImageSynthesisTaskMetrics getMetrics() {
  91 + return metrics;
  92 + }
  93 +
  94 + void setMetrics(ImageSynthesisTaskMetrics metrics) {
  95 + this.metrics = metrics;
  96 + }
  97 +
  98 +
  99 + public Long created() {
  100 + return this.created;
  101 + }
  102 +
  103 + @Override
  104 + public String toString() {
  105 + return "TongYiImagesResponseMetadata {" + "created=" + created + '}';
  106 + }
  107 +
  108 + @Override
  109 + public boolean equals(Object o) {
  110 + if (this == o) {
  111 +
  112 + return true;
  113 + }
  114 + if (o == null || getClass() != o.getClass()) {
  115 +
  116 + return false;
  117 + }
  118 +
  119 + TongYiImagesResponseMetadata that = (TongYiImagesResponseMetadata) o;
  120 +
  121 + return Objects.equals(created, that.created)
  122 + && Objects.equals(taskId, that.taskId)
  123 + && Objects.equals(metrics, that.metrics);
  124 + }
  125 +
  126 + @Override
  127 + public int hashCode() {
  128 + return Objects.hash(created, taskId, metrics);
  129 + }
  130 +
  131 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiTextEmbeddingResponseMetadata.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.metadata;
  18 +
  19 +import com.alibaba.dashscope.embeddings.TextEmbeddingUsage;
  20 +import org.springframework.ai.embedding.EmbeddingResponseMetadata;
  21 +
  22 +/**
  23 + * @author why_ohh
  24 + * @author yuluo
  25 + * @author <a href="mailto:550588941@qq.com">why_ohh</a>
  26 + * @since 2023.0.1.0
  27 + */
  28 +
  29 +public class TongYiTextEmbeddingResponseMetadata extends EmbeddingResponseMetadata {
  30 +
  31 + private Integer totalTokens;
  32 +
  33 + protected TongYiTextEmbeddingResponseMetadata(Integer totalTokens) {
  34 +
  35 + this.totalTokens = totalTokens;
  36 + }
  37 +
  38 + public static TongYiTextEmbeddingResponseMetadata from(TextEmbeddingUsage usage) {
  39 +
  40 + return new TongYiTextEmbeddingResponseMetadata(usage.getTotalTokens());
  41 + }
  42 +
  43 + public Integer getTotalTokens() {
  44 +
  45 + return totalTokens;
  46 + }
  47 +
  48 + public void setTotalTokens(Integer totalTokens) {
  49 +
  50 + this.totalTokens = totalTokens;
  51 + }
  52 +
  53 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioSpeechResponseMetadata.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.metadata.audio;
  18 +
  19 +import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult;
  20 +import com.alibaba.dashscope.audio.tts.SpeechSynthesisUsage;
  21 +import com.alibaba.dashscope.audio.tts.timestamp.Sentence;
  22 +import org.springframework.ai.chat.metadata.EmptyRateLimit;
  23 +import org.springframework.ai.chat.metadata.RateLimit;
  24 +import org.springframework.ai.model.ResponseMetadata;
  25 +import org.springframework.lang.Nullable;
  26 +import org.springframework.util.Assert;
  27 +
  28 +import java.util.HashMap;
  29 +
  30 +
  31 +
  32 +/**
  33 + * @author yuluo
  34 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  35 + * @since 2023.0.1.0
  36 + */
  37 +
  38 +public class TongYiAudioSpeechResponseMetadata extends HashMap<String, Object> implements ResponseMetadata {
  39 +
  40 + private SpeechSynthesisUsage usage;
  41 +
  42 + private String requestId;
  43 +
  44 + private Sentence time;
  45 +
  46 + protected static final String AI_METADATA_STRING = "{ @type: %1$s, requestsLimit: %2$s }";
  47 +
  48 + /**
  49 + * NULL objects.
  50 + */
  51 + public static final TongYiAudioSpeechResponseMetadata NULL = new TongYiAudioSpeechResponseMetadata() {
  52 + };
  53 +
  54 + public static TongYiAudioSpeechResponseMetadata from(SpeechSynthesisResult result) {
  55 +
  56 + Assert.notNull(result, "TongYi AI speech must not be null");
  57 + TongYiAudioSpeechResponseMetadata speechResponseMetadata = new TongYiAudioSpeechResponseMetadata();
  58 +
  59 +
  60 +
  61 + return speechResponseMetadata;
  62 + }
  63 +
  64 + public static TongYiAudioSpeechResponseMetadata from(String result) {
  65 +
  66 + Assert.notNull(result, "TongYi AI speech must not be null");
  67 + TongYiAudioSpeechResponseMetadata speechResponseMetadata = new TongYiAudioSpeechResponseMetadata();
  68 +
  69 + return speechResponseMetadata;
  70 + }
  71 +
  72 + @Nullable
  73 + private RateLimit rateLimit;
  74 +
  75 + public TongYiAudioSpeechResponseMetadata() {
  76 +
  77 + this(null);
  78 + }
  79 +
  80 + public TongYiAudioSpeechResponseMetadata(@Nullable RateLimit rateLimit) {
  81 +
  82 + this.rateLimit = rateLimit;
  83 + }
  84 +
  85 + @Nullable
  86 + public RateLimit getRateLimit() {
  87 +
  88 + RateLimit rateLimit = this.rateLimit;
  89 + return rateLimit != null ? rateLimit : new EmptyRateLimit();
  90 + }
  91 +
  92 + public TongYiAudioSpeechResponseMetadata withRateLimit(RateLimit rateLimit) {
  93 +
  94 + this.rateLimit = rateLimit;
  95 + return this;
  96 + }
  97 +
  98 + public TongYiAudioSpeechResponseMetadata withUsage(SpeechSynthesisUsage usage) {
  99 +
  100 + this.usage = usage;
  101 + return this;
  102 + }
  103 +
  104 + public TongYiAudioSpeechResponseMetadata withRequestId(String id) {
  105 +
  106 + this.requestId = id;
  107 + return this;
  108 + }
  109 +
  110 + public TongYiAudioSpeechResponseMetadata withSentence(Sentence sentence) {
  111 +
  112 + this.time = sentence;
  113 + return this;
  114 + }
  115 +
  116 + public SpeechSynthesisUsage getUsage() {
  117 + return usage;
  118 + }
  119 +
  120 + public String getRequestId() {
  121 + return requestId;
  122 + }
  123 +
  124 + public Sentence getTime() {
  125 + return time;
  126 + }
  127 +
  128 + @Override
  129 + public String toString() {
  130 + return AI_METADATA_STRING.formatted(getClass().getName(), getRateLimit());
  131 + }
  132 +
  133 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionMetadata.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.metadata.audio;
  18 +
  19 +import org.springframework.ai.model.ResultMetadata;
  20 +
  21 +/**
  22 + * @author xYLiu
  23 + * @author yuluo
  24 + * @since 2023.0.1.0
  25 + */
  26 +
  27 +public interface TongYiAudioTranscriptionMetadata extends ResultMetadata {
  28 +
  29 + /**
  30 + * A constant instance of {@link TongYiAudioTranscriptionMetadata} that represents a null or empty metadata.
  31 + */
  32 + TongYiAudioTranscriptionMetadata NULL = TongYiAudioTranscriptionMetadata.create();
  33 +
  34 + /**
  35 + * Factory method for creating a new instance of {@link TongYiAudioTranscriptionMetadata}.
  36 + * @return a new instance of {@link TongYiAudioTranscriptionMetadata}
  37 + */
  38 + static TongYiAudioTranscriptionMetadata create() {
  39 + return new TongYiAudioTranscriptionMetadata() {
  40 + };
  41 + }
  42 +
  43 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionResponseMetadata.java 0 → 100644
  1 +/*
  2 + * Copyright 2023-2024 the original author or authors.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * https://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package com.alibaba.cloud.ai.tongyi.metadata.audio;
  18 +
  19 +import com.alibaba.dashscope.audio.asr.transcription.TranscriptionResult;
  20 +import com.google.gson.JsonObject;
  21 +import org.springframework.ai.chat.metadata.EmptyRateLimit;
  22 +import org.springframework.ai.chat.metadata.RateLimit;
  23 +import org.springframework.ai.model.ResponseMetadata;
  24 +import org.springframework.util.Assert;
  25 +
  26 +import javax.annotation.Nullable;
  27 +import java.util.HashMap;
  28 +
  29 +
  30 +
  31 +/**
  32 + * @author yuluo
  33 + * @author <a href="mailto:yuluo08290126@gmail.com">yuluo</a>
  34 + * @since 2023.0.1.0
  35 + */
  36 +
  37 +public class TongYiAudioTranscriptionResponseMetadata extends HashMap<String, Object> implements ResponseMetadata {
  38 +
  39 + @Nullable
  40 + private RateLimit rateLimit;
  41 +
  42 + private JsonObject usage;
  43 +
  44 + protected static final String AI_METADATA_STRING = "{ @type: %1$s, rateLimit: %4$s }";
  45 +
  46 + /**
  47 + * NULL objects.
  48 + */
  49 + public static final TongYiAudioTranscriptionResponseMetadata NULL = new TongYiAudioTranscriptionResponseMetadata() {
  50 + };
  51 +
  52 + protected TongYiAudioTranscriptionResponseMetadata() {
  53 +
  54 + this(null, new JsonObject());
  55 + }
  56 +
  57 + protected TongYiAudioTranscriptionResponseMetadata(JsonObject usage) {
  58 +
  59 + this(null, usage);
  60 + }
  61 +
  62 + protected TongYiAudioTranscriptionResponseMetadata(@Nullable RateLimit rateLimit, JsonObject usage) {
  63 +
  64 + this.rateLimit = rateLimit;
  65 + this.usage = usage;
  66 + }
  67 +
  68 + public static TongYiAudioTranscriptionResponseMetadata from(TranscriptionResult result) {
  69 +
  70 + Assert.notNull(result, "TongYi Transcription must not be null");
  71 + return new TongYiAudioTranscriptionResponseMetadata(result.getUsage());
  72 + }
  73 +
  74 + @Nullable
  75 + public RateLimit getRateLimit() {
  76 +
  77 + return this.rateLimit != null ? this.rateLimit : new EmptyRateLimit();
  78 + }
  79 +
  80 + public void setRateLimit(@Nullable RateLimit rateLimit) {
  81 + this.rateLimit = rateLimit;
  82 + }
  83 +
  84 + public JsonObject getUsage() {
  85 + return usage;
  86 + }
  87 +
  88 + public void setUsage(JsonObject usage) {
  89 + this.usage = usage;
  90 + }
  91 +
  92 + @Override
  93 + public String toString() {
  94 +
  95 + return AI_METADATA_STRING.formatted(getClass().getName(), getRateLimit());
  96 + }
  97 +
  98 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
1 1 -cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration
  2 +cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration
2 3 \ No newline at end of file
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/QianWenChatClientTests.java
1   -package cn.iocoder.yudao.framework.ai.chat;
2   -
3   -import org.springframework.ai.chat.ChatResponse;
4   -import org.springframework.ai.chat.messages.SystemMessage;
5   -import org.springframework.ai.chat.messages.UserMessage;
6   -import org.springframework.ai.chat.prompt.Prompt;
7   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient;
8   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal;
9   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenOptions;
10   -import cn.iocoder.yudao.framework.ai.core.model.tongyi.api.QianWenApi;
11   -import com.alibaba.dashscope.aigc.generation.GenerationResult;
12   -import com.alibaba.dashscope.aigc.generation.models.QwenParam;
13   -import com.alibaba.dashscope.common.Message;
14   -import com.alibaba.dashscope.common.MessageManager;
15   -import com.alibaba.dashscope.common.Role;
16   -import com.alibaba.dashscope.exception.InputRequiredException;
17   -import com.alibaba.dashscope.exception.NoApiKeyException;
18   -import org.junit.Before;
19   -import org.junit.Test;
20   -import reactor.core.publisher.Flux;
21   -
22   -import java.util.ArrayList;
23   -import java.util.List;
24   -import java.util.Scanner;
25   -import java.util.function.Consumer;
26   -
27   -// TODO 芋艿:整理单测
28   -/**
29   - * author: fansili
30   - * time: 2024/3/13 21:37
31   - */
32   -public class QianWenChatClientTests {
33   -
34   - private QianWenChatClient qianWenChatClient;
35   -
36   - @Before
37   - public void setup() {
38   - QianWenApi qianWenApi = new QianWenApi("sk-Zsd81gZYg7", QianWenChatModal.QWEN_72B_CHAT);
39   - QianWenOptions qianWenOptions = new QianWenOptions();
40   - qianWenOptions.setTopP(0.8F);
41   -// qianWenOptions.setTopK(3); TODO 芋艿:临时处理
42   -// qianWenOptions.setTemperature(0.6F); TODO 芋艿:临时处理
43   - qianWenChatClient = new QianWenChatClient(
44   - qianWenApi,
45   - qianWenOptions
46   - );
47   - }
48   -
49   - @Test
50   - public void callTest() {
51   - List<org.springframework.ai.chat.messages.Message> messages = new ArrayList<>();
52   - messages.add(new SystemMessage("你是一个优质的小红书文艺作者,抒写着各城市的美好文化和风景。"));
53   - messages.add(new UserMessage("长沙怎么样?"));
54   -
55   - ChatResponse call = qianWenChatClient.call(new Prompt(messages));
56   - System.err.println(call.getResult());
57   - }
58   -
59   - @Test
60   - public void streamTest() {
61   - List<org.springframework.ai.chat.messages.Message> messages = new ArrayList<>();
62   - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
63   - messages.add(new UserMessage("长沙怎么样?"));
64   -
65   - Flux<ChatResponse> flux = qianWenChatClient.stream(new Prompt(messages));
66   - flux.subscribe(new Consumer<ChatResponse>() {
67   - @Override
68   - public void accept(ChatResponse chatResponse) {
69   - System.err.print(chatResponse.getResult().getOutput().getContent());
70   - }
71   - });
72   -
73   - // 阻止退出
74   - Scanner scanner = new Scanner(System.in);
75   - scanner.nextLine();
76   - }
77   -
78   - @Test
79   - public void qianwenDemoTest() throws NoApiKeyException, InputRequiredException {
80   - com.alibaba.dashscope.aigc.generation.Generation gen = new com.alibaba.dashscope.aigc.generation.Generation();
81   - MessageManager msgManager = new MessageManager(10);
82   - Message systemMsg =
83   - Message.builder().role(Role.SYSTEM.getValue()).content("You are a helpful assistant.").build();
84   - Message userMsg = Message.builder().role(Role.USER.getValue()).content("就当前的海洋污染的情况,写一份限塑的倡议书提纲,需要有理有据地号召大家克制地使用塑料制品").build();
85   - msgManager.add(systemMsg);
86   - msgManager.add(userMsg);
87   - QwenParam param =
88   - QwenParam.builder().model("qwen-72b-chat").messages(msgManager.get())
89   - .resultFormat(QwenParam.ResultFormat.MESSAGE)
90   - .topP(0.8)
91   - /* set the random seed, optional, default to 1234 if not set */
92   - .seed(100)
93   - .apiKey("sk-Zsd81gZYg7")
94   - .build();
95   - GenerationResult result = gen.call(param);
96   - System.out.println(result);
97   - System.out.println("-----------------");
98   - System.out.println("-----------------");
99   - msgManager.add(result);
100   - param.setPrompt("能否缩短一些,只讲三点");
101   - param.setMessages(msgManager.get());
102   - result = gen.call(param);
103   - System.out.println(result);
104   - }
105   -}
  1 +//package cn.iocoder.yudao.framework.ai.chat;
  2 +//
  3 +//import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatClient;
  4 +//import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenChatModal;
  5 +//import cn.iocoder.yudao.framework.ai.core.model.tongyi.QianWenOptions;
  6 +//import cn.iocoder.yudao.framework.ai.core.model.tongyi.api.QianWenApi;
  7 +//import com.alibaba.dashscope.aigc.generation.GenerationResult;
  8 +//import com.alibaba.dashscope.aigc.generation.models.QwenParam;
  9 +//import com.alibaba.dashscope.common.Message;
  10 +//import com.alibaba.dashscope.common.MessageManager;
  11 +//import com.alibaba.dashscope.common.Role;
  12 +//import com.alibaba.dashscope.exception.InputRequiredException;
  13 +//import com.alibaba.dashscope.exception.NoApiKeyException;
  14 +//import org.junit.Before;
  15 +//import org.junit.Test;
  16 +//import org.springframework.ai.chat.messages.SystemMessage;
  17 +//import org.springframework.ai.chat.messages.UserMessage;
  18 +//import org.springframework.ai.chat.model.ChatResponse;
  19 +//import org.springframework.ai.chat.prompt.Prompt;
  20 +//import reactor.core.publisher.Flux;
  21 +//
  22 +//import java.util.ArrayList;
  23 +//import java.util.List;
  24 +//import java.util.Scanner;
  25 +//import java.util.function.Consumer;
  26 +//
  27 +//// TODO 芋艿:整理单测
  28 +///**
  29 +// * author: fansili
  30 +// * time: 2024/3/13 21:37
  31 +// */
  32 +//public class QianWenChatClientTests {
  33 +//
  34 +// private QianWenChatClient qianWenChatClient;
  35 +//
  36 +// @Before
  37 +// public void setup() {
  38 +// QianWenApi qianWenApi = new QianWenApi("sk-Zsd81gZYg7", QianWenChatModal.QWEN_72B_CHAT);
  39 +// QianWenOptions qianWenOptions = new QianWenOptions();
  40 +// qianWenOptions.setTopP(0.8F);
  41 +//// qianWenOptions.setTopK(3); TODO 芋艿:临时处理
  42 +//// qianWenOptions.setTemperature(0.6F); TODO 芋艿:临时处理
  43 +// qianWenChatClient = new QianWenChatClient(
  44 +// qianWenApi,
  45 +// qianWenOptions
  46 +// );
  47 +// }
  48 +//
  49 +// @Test
  50 +// public void callTest() {
  51 +// List<org.springframework.ai.chat.messages.Message> messages = new ArrayList<>();
  52 +// messages.add(new SystemMessage("你是一个优质的小红书文艺作者,抒写着各城市的美好文化和风景。"));
  53 +// messages.add(new UserMessage("长沙怎么样?"));
  54 +//
  55 +// ChatResponse call = qianWenChatClient.call(new Prompt(messages));
  56 +// System.err.println(call.getResult());
  57 +// }
  58 +//
  59 +// @Test
  60 +// public void streamTest() {
  61 +// List<org.springframework.ai.chat.messages.Message> messages = new ArrayList<>();
  62 +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  63 +// messages.add(new UserMessage("长沙怎么样?"));
  64 +//
  65 +// Flux<ChatResponse> flux = qianWenChatClient.stream(new Prompt(messages));
  66 +// flux.subscribe(new Consumer<ChatResponse>() {
  67 +// @Override
  68 +// public void accept(ChatResponse chatResponse) {
  69 +// System.err.print(chatResponse.getResult().getOutput().getContent());
  70 +// }
  71 +// });
  72 +//
  73 +// // 阻止退出
  74 +// Scanner scanner = new Scanner(System.in);
  75 +// scanner.nextLine();
  76 +// }
  77 +//
  78 +// @Test
  79 +// public void qianwenDemoTest() throws NoApiKeyException, InputRequiredException {
  80 +// com.alibaba.dashscope.aigc.generation.Generation gen = new com.alibaba.dashscope.aigc.generation.Generation();
  81 +// MessageManager msgManager = new MessageManager(10);
  82 +// Message systemMsg =
  83 +// Message.builder().role(Role.SYSTEM.getValue()).content("You are a helpful assistant.").build();
  84 +// Message userMsg = Message.builder().role(Role.USER.getValue()).content("就当前的海洋污染的情况,写一份限塑的倡议书提纲,需要有理有据地号召大家克制地使用塑料制品").build();
  85 +// msgManager.add(systemMsg);
  86 +// msgManager.add(userMsg);
  87 +// QwenParam param =
  88 +// QwenParam.builder().model("qwen-72b-chat").messages(msgManager.get())
  89 +// .resultFormat(QwenParam.ResultFormat.MESSAGE)
  90 +// .topP(0.8)
  91 +// /* set the random seed, optional, default to 1234 if not set */
  92 +// .seed(100)
  93 +// .apiKey("sk-Zsd81gZYg7")
  94 +// .build();
  95 +// GenerationResult result = gen.call(param);
  96 +// System.out.println(result);
  97 +// System.out.println("-----------------");
  98 +// System.out.println("-----------------");
  99 +// msgManager.add(result);
  100 +// param.setPrompt("能否缩短一些,只讲三点");
  101 +// param.setMessages(msgManager.get());
  102 +// result = gen.call(param);
  103 +// System.out.println(result);
  104 +// }
  105 +//}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatClientTests.java
1 1 package cn.iocoder.yudao.framework.ai.chat;
2 2  
3   -import org.springframework.ai.chat.ChatResponse;
4   -import org.springframework.ai.chat.messages.Message;
5   -import org.springframework.ai.chat.messages.SystemMessage;
6   -import org.springframework.ai.chat.messages.UserMessage;
7   -import org.springframework.ai.chat.prompt.Prompt;
8 3 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatClient;
9 4 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
10 5 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoOptions;
11 6 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.api.XingHuoApi;
12 7 import org.junit.Before;
13 8 import org.junit.Test;
  9 +import org.springframework.ai.chat.messages.Message;
  10 +import org.springframework.ai.chat.messages.SystemMessage;
  11 +import org.springframework.ai.chat.messages.UserMessage;
  12 +import org.springframework.ai.chat.model.ChatResponse;
  13 +import org.springframework.ai.chat.prompt.Prompt;
14 14 import reactor.core.publisher.Flux;
15 15  
16 16 import java.util.ArrayList;
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatTests.java
1 1 package cn.iocoder.yudao.framework.ai.chat;
2 2  
3   -import org.springframework.ai.chat.ChatResponse;
4   -import org.springframework.ai.chat.messages.Message;
5   -import org.springframework.ai.chat.messages.SystemMessage;
6   -import org.springframework.ai.chat.messages.UserMessage;
7   -import org.springframework.ai.chat.prompt.Prompt;
8   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatClient;
9   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanChatModel;
10   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatOptions;
11   -import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanApi;
12   -import org.junit.Before;
13   -import org.junit.Test;
14   -import reactor.core.publisher.Flux;
15   -
16   -import java.util.ArrayList;
17   -import java.util.List;
18   -import java.util.Scanner;
  3 +//import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatClient;
  4 +//import cn.iocoder.yudao.framework.ai.core.model.yiyan.YiYanChatOptions;
  5 +//import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanApi;
  6 +//import cn.iocoder.yudao.framework.ai.core.model.yiyan.api.YiYanChatModel;
  7 +//import org.junit.Before;
  8 +//import org.junit.Test;
  9 +//import org.springframework.ai.chat.messages.Message;
  10 +//import org.springframework.ai.chat.messages.SystemMessage;
  11 +//import org.springframework.ai.chat.messages.UserMessage;
  12 +//import org.springframework.ai.chat.model.ChatResponse;
  13 +//import org.springframework.ai.chat.prompt.Prompt;
  14 +//import reactor.core.publisher.Flux;
  15 +//
  16 +//import java.util.ArrayList;
  17 +//import java.util.List;
  18 +//import java.util.Scanner;
19 19  
20 20 // TODO 芋艿:整理单测
21 21 /**
... ... @@ -26,49 +26,49 @@ import java.util.Scanner;
26 26 */
27 27 public class YiYanChatTests {
28 28  
29   - private YiYanChatClient yiYanChatClient;
30   -
31   - @Before
32   - public void setup() {
33   - YiYanApi yiYanApi = new YiYanApi(
34   - "x0cuLZ7XsaTCU08vuJWO87Lg",
35   - "R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK",
36   - YiYanChatModel.ERNIE4_3_5_8K,
37   - 86400
38   - );
39   - YiYanChatOptions yiYanOptions = new YiYanChatOptions();
40   - yiYanOptions.setMaxOutputTokens(2048);
41   - yiYanOptions.setTopP(0.6f);
42   - yiYanOptions.setTemperature(0.85f);
43   - yiYanChatClient = new YiYanChatClient(
44   - yiYanApi,
45   - yiYanOptions
46   - );
47   - }
48   -
49   - @Test
50   - public void callTest() {
51   -
52   - // tip: 百度的message 有特殊规则(最后一个message为当前请求的信息,前面的message为历史对话信息)
53   - // tip: 地址 https://cloud.baidu.com/doc/WENXINWORKSHOP/s/jlil56u11
54   - List<Message> messages = new ArrayList<>();
55   - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景,所有问题都采用文言文回答。"));
56   - messages.add(new UserMessage("长沙怎么样?"));
57   -
58   - ChatResponse call = yiYanChatClient.call(new Prompt(messages));
59   - System.err.println(call.getResult());
60   - }
61   -
62   - @Test
63   - public void streamTest() {
64   - List<Message> messages = new ArrayList<>();
65   - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景,所有问题都采用文言文回答。"));
66   - messages.add(new UserMessage("长沙怎么样?"));
67   -
68   - Flux<ChatResponse> fluxResponse = yiYanChatClient.stream(new Prompt(messages));
69   - fluxResponse.subscribe(chatResponse -> System.err.print(chatResponse.getResult().getOutput().getContent()));
70   - // 阻止退出
71   - Scanner scanner = new Scanner(System.in);
72   - scanner.nextLine();
73   - }
  29 +// private YiYanChatClient yiYanChatClient;
  30 +//
  31 +// @Before
  32 +// public void setup() {
  33 +// YiYanApi yiYanApi = new YiYanApi(
  34 +// "x0cuLZ7XsaTCU08vuJWO87Lg",
  35 +// "R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK",
  36 +// YiYanChatModel.ERNIE4_3_5_8K,
  37 +// 86400
  38 +// );
  39 +// YiYanChatOptions yiYanOptions = new YiYanChatOptions();
  40 +// yiYanOptions.setMaxOutputTokens(2048);
  41 +// yiYanOptions.setTopP(0.6f);
  42 +// yiYanOptions.setTemperature(0.85f);
  43 +// yiYanChatClient = new YiYanChatClient(
  44 +// yiYanApi,
  45 +// yiYanOptions
  46 +// );
  47 +// }
  48 +//
  49 +// @Test
  50 +// public void callTest() {
  51 +//
  52 +// // tip: 百度的message 有特殊规则(最后一个message为当前请求的信息,前面的message为历史对话信息)
  53 +// // tip: 地址 https://cloud.baidu.com/doc/WENXINWORKSHOP/s/jlil56u11
  54 +// List<Message> messages = new ArrayList<>();
  55 +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景,所有问题都采用文言文回答。"));
  56 +// messages.add(new UserMessage("长沙怎么样?"));
  57 +//
  58 +// ChatResponse call = yiYanChatClient.call(new Prompt(messages));
  59 +// System.err.println(call.getResult());
  60 +// }
  61 +//
  62 +// @Test
  63 +// public void streamTest() {
  64 +// List<Message> messages = new ArrayList<>();
  65 +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景,所有问题都采用文言文回答。"));
  66 +// messages.add(new UserMessage("长沙怎么样?"));
  67 +//
  68 +// Flux<ChatResponse> fluxResponse = yiYanChatClient.stream(new Prompt(messages));
  69 +// fluxResponse.subscribe(chatResponse -> System.err.print(chatResponse.getResult().getOutput().getContent()));
  70 +// // 阻止退出
  71 +// Scanner scanner = new Scanner(System.in);
  72 +// scanner.nextLine();
  73 +// }
74 74 }
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageClientTests.java
... ... @@ -4,7 +4,7 @@ import org.springframework.ai.image.ImagePrompt;
4 4 import org.springframework.ai.image.ImageResponse;
5 5 import org.junit.Before;
6 6 import org.junit.Test;
7   -import org.springframework.ai.openai.OpenAiImageClient;
  7 +import org.springframework.ai.openai.OpenAiImageModel;
8 8 import org.springframework.ai.openai.api.OpenAiImageApi;
9 9  
10 10 import javax.imageio.ImageIO;
... ... @@ -23,12 +23,12 @@ import java.util.Scanner;
23 23 public class OpenAiImageClientTests {
24 24  
25 25  
26   - private OpenAiImageClient openAiImageClient;
  26 + private OpenAiImageModel openAiImageClient;
27 27  
28 28 @Before
29 29 public void setup() {
30 30 // 初始化 openAiImageClient
31   - this.openAiImageClient = new OpenAiImageClient(
  31 + this.openAiImageClient = new OpenAiImageModel(
32 32 new OpenAiImageApi("")
33 33 // new OpenAiImageOptions().setResponseFormat(OpenAiImageOptions.ResponseFormatEnum.URL.getValue()) TODO 芋艿:临时处理
34 34 );
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoTests.java
... ... @@ -17,7 +17,8 @@ public class SunoTests {
17 17  
18 18 @Before
19 19 public void setup() {
20   - String url = "https://suno-om0w1cy6e-status2xxs-projects.vercel.app";
  20 + String url = "https://suno-55ishh05u-status2xxs-projects.vercel.app";
  21 +// String url = "http://127.0.0.1:3001";
21 22 this.sunoApi = new SunoApi(url);
22 23 }
23 24  
... ... @@ -53,5 +54,4 @@ public class SunoTests {
53 54 System.out.println(limitUsageData);
54 55 }
55 56  
56   -
57 57 }
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -160,19 +160,16 @@ spring:
160 160 gemini:
161 161 project-id: 1 # TODO 芋艿:缺配置
162 162 location: 2
  163 + qianfan: # 文心一言
  164 + api-key: x0cuLZ7XsaTCU08vuJWO87Lg
  165 + secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK
  166 + cloud:
  167 + ai:
  168 + tongyi: # 通义千问
  169 + tongyi:
  170 + api-key: sk-Zsd81gZYg7
163 171  
164 172 yudao.ai:
165   - yiyan:
166   - enable: true
167   - aiPlatform: YI_YAN # TODO @fan:建议每个都独立配置属性类
168   - max-tokens: 1500
169   - temperature: 0.85
170   - topP: 0.8
171   - topK: 0
172   - appKey: x0cuLZ7XsaTCU08vuJWO87Lg
173   - secretKey: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK
174   - refreshTokenSecondTime: 86400
175   - model: ERNIE4_3_5_8K
176 173 xinghuo:
177 174 enable: true
178 175 aiPlatform: XING_HUO # TODO @fan:建议每个都独立配置属性类
... ... @@ -192,6 +189,7 @@ yudao.ai:
192 189 topP: 0.8
193 190 topK: 0
194 191 api-key: sk-Zsd81gZYg7
  192 + model: QWEN_TURBO
195 193 midjourney:
196 194 enable: true
197 195 # base-url: https://api.holdai.top/mj-relax/mj
... ... @@ -200,7 +198,8 @@ yudao.ai:
200 198 notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify
201 199 suno:
202 200 enable: true
203   - base-url: https://suno-om0w1cy6e-status2xxs-projects.vercel.app
  201 +# base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
  202 + base-url: http://127.0.0.1:3001
204 203  
205 204 --- #################### 芋道相关配置 ####################
206 205  
... ...