Commit 7288e6c980ea2ea310d0790f3f686ef6f22d1076

Authored by YunaiV
2 parents fa585578 83d86bcf

Merge branch 'master-jdk21-ai' of https://gitee.com/cherishsince/ruoyi-vue-pro into develop

Showing 28 changed files with 953 additions and 26 deletions
yudao-module-ai/pom.xml
... ... @@ -18,7 +18,7 @@
18 18  
19 19 <name>${project.artifactId}</name>
20 20 <description>
21   - ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。
  21 + ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维导图等功能。
22 22 目前已接入各种模型,不限于:
23 23 国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
24 24 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
... ...
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java
... ... @@ -22,7 +22,7 @@ public enum AiChatRoleEnum implements IntArrayValuable {
22 22 除此之外不需要除了正文内容外的其他回复,如标题、开头、任何解释性语句或道歉。
23 23 """),
24 24  
25   - AI_MIND_MAP_ROLE(2, "脑图助手", """
  25 + AI_MIND_MAP_ROLE(2, "导图助手", """
26 26 你是一位非常优秀的思维导图助手,你会把用户的所有提问都总结成思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子:
27 27 # Geek-AI 助手
28 28 ## 完整的开源系统
... ...
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
... ... @@ -45,9 +45,11 @@ public interface ErrorCodeConstants {
45 45 // ========== API 音乐 1-040-006-000 ==========
46 46 ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!");
47 47  
48   -
49 48 // ========== API 写作 1-022-007-000 ==========
50 49 ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_022_007_000, "作文不存在!");
51 50 ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "写作生成异常!");
52 51  
  52 + // ========== API 思维导图 1-040-008-000 ==========
  53 + ErrorCode MIND_MAP_NOT_EXISTS = new ErrorCode(1_040_008_000, "思维导图不存在!");
  54 +
53 55 }
... ...
yudao-module-ai/yudao-module-ai-biz/pom.xml
... ... @@ -12,7 +12,7 @@
12 12  
13 13 <name>${project.artifactId}</name>
14 14 <description>
15   - ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。
  15 + ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维导图等功能。
16 16 目前已接入各种模型,不限于:
17 17 国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
18 18 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
... ... @@ -5,10 +5,7 @@ import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
5 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
6 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
7 7 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
8   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
9   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
10   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageRespVO;
11   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
  8 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
12 9 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
13 10 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
14 11 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
... ... @@ -45,6 +42,13 @@ public class AiImageController {
45 42 return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
46 43 }
47 44  
  45 + @GetMapping("/public-page")
  46 + @Operation(summary = "获取公开的绘图分页")
  47 + public CommonResult<PageResult<AiImageRespVO>> getImagePagePublic(AiImagePublicPageReqVO pageReqVO) {
  48 + PageResult<AiImageDO> pageResult = imageService.getImagePagePublic(pageReqVO);
  49 + return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
  50 + }
  51 +
48 52 @GetMapping("/get-my")
49 53 @Operation(summary = "获取【我的】绘图记录")
50 54 @Parameter(name = "id", required = true, description = "绘画编号", example = "1024")
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +
  7 +@Schema(description = "管理后台 - AI 绘画公开的分页 Request VO")
  8 +@Data
  9 +public class AiImagePublicPageReqVO extends PageParam {
  10 +
  11 + @Schema(description = "提示词")
  12 + private String prompt;
  13 +
  14 +}
0 15 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java
1 1 package cn.iocoder.yudao.module.ai.controller.admin.mindmap;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 6 import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
  7 +import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
  8 +import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapRespVO;
  9 +import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
5 10 import cn.iocoder.yudao.module.ai.service.mindmap.AiMindMapService;
6 11 import io.swagger.v3.oas.annotations.Operation;
  12 +import io.swagger.v3.oas.annotations.Parameter;
7 13 import io.swagger.v3.oas.annotations.tags.Tag;
8 14 import jakarta.annotation.Resource;
9 15 import jakarta.annotation.security.PermitAll;
10 16 import jakarta.validation.Valid;
11 17 import org.springframework.http.MediaType;
12   -import org.springframework.web.bind.annotation.PostMapping;
13   -import org.springframework.web.bind.annotation.RequestBody;
14   -import org.springframework.web.bind.annotation.RequestMapping;
15   -import org.springframework.web.bind.annotation.RestController;
  18 +import org.springframework.security.access.prepost.PreAuthorize;
  19 +import org.springframework.web.bind.annotation.*;
16 20 import reactor.core.publisher.Flux;
17 21  
  22 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
18 23 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
19 24  
20 25 @Tag(name = "管理后台 - AI 思维导图")
... ... @@ -26,10 +31,29 @@ public class AiMindMapController {
26 31 private AiMindMapService mindMapService;
27 32  
28 33 @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
29   - @Operation(summary = "脑图生成(流式)", description = "流式返回,响应较快")
  34 + @Operation(summary = "导图生成(流式)", description = "流式返回,响应较快")
30 35 @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
31 36 public Flux<CommonResult<String>> generateMindMap(@RequestBody @Valid AiMindMapGenerateReqVO generateReqVO) {
32 37 return mindMapService.generateMindMap(generateReqVO, getLoginUserId());
33 38 }
34 39  
  40 + // ================ 导图管理 ================
  41 +
  42 + @DeleteMapping("/delete")
  43 + @Operation(summary = "删除思维导图")
  44 + @Parameter(name = "id", description = "编号", required = true)
  45 + @PreAuthorize("@ss.hasPermission('ai:mind-map:delete')")
  46 + public CommonResult<Boolean> deleteMindMap(@RequestParam("id") Long id) {
  47 + mindMapService.deleteMindMap(id);
  48 + return success(true);
  49 + }
  50 +
  51 + @GetMapping("/page")
  52 + @Operation(summary = "获得思维导图分页")
  53 + @PreAuthorize("@ss.hasPermission('ai:mind-map:query')")
  54 + public CommonResult<PageResult<AiMindMapRespVO>> getMindMapPage(@Valid AiMindMapPageReqVO pageReqVO) {
  55 + PageResult<AiMindMapDO> pageResult = mindMapService.getMindMapPage(pageReqVO);
  56 + return success(BeanUtils.toBean(pageResult, AiMindMapRespVO.class));
  57 + }
  58 +
35 59 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageParam;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
  5 +import lombok.Data;
  6 +import lombok.EqualsAndHashCode;
  7 +import lombok.ToString;
  8 +import org.springframework.format.annotation.DateTimeFormat;
  9 +
  10 +import java.time.LocalDateTime;
  11 +
  12 +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
  13 +
  14 +@Schema(description = "管理后台 - AI 思维导图分页 Request VO")
  15 +@Data
  16 +@EqualsAndHashCode(callSuper = true)
  17 +@ToString(callSuper = true)
  18 +public class AiMindMapPageReqVO extends PageParam {
  19 +
  20 + @Schema(description = "用户编号", example = "4325")
  21 + private Long userId;
  22 +
  23 + @Schema(description = "生成内容提示", example = "Java 学习路线")
  24 + private String prompt;
  25 +
  26 + @Schema(description = "创建时间")
  27 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
  28 + private LocalDateTime[] createTime;
  29 +
  30 +}
0 31 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +@Schema(description = "管理后台 - AI 思维导图 Response VO")
  9 +@Data
  10 +public class AiMindMapRespVO {
  11 +
  12 + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3373")
  13 + private Long id;
  14 +
  15 + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4325")
  16 + private Long userId;
  17 +
  18 + @Schema(description = "生成内容提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 学习路线")
  19 + private String prompt;
  20 +
  21 + @Schema(description = "生成的思维导图内容")
  22 + private String generatedContent;
  23 +
  24 + @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
  25 + private String platform;
  26 +
  27 + @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-3.5-turbo-0125")
  28 + private String model;
  29 +
  30 + @Schema(description = "错误信息")
  31 + private String errorMessage;
  32 +
  33 + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
  34 + private LocalDateTime createTime;
  35 +
  36 +}
0 37 \ No newline at end of file
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java
... ... @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 6 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
  7 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePublicPageReqVO;
7 8 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
8 9 import org.apache.ibatis.annotations.Mapper;
9 10  
... ... @@ -41,6 +42,13 @@ public interface AiImageMapper extends BaseMapperX&lt;AiImageDO&gt; {
41 42 .orderByDesc(AiImageDO::getId));
42 43 }
43 44  
  45 + default PageResult<AiImageDO> selectPage(AiImagePublicPageReqVO pageReqVO) {
  46 + return selectPage(pageReqVO, new LambdaQueryWrapperX<AiImageDO>()
  47 + .eqIfPresent(AiImageDO::getPublicStatus, Boolean.TRUE)
  48 + .likeIfPresent(AiImageDO::getPrompt, pageReqVO.getPrompt())
  49 + .orderByDesc(AiImageDO::getId));
  50 + }
  51 +
44 52 default List<AiImageDO> selectListByStatusAndPlatform(Integer status, String platform) {
45 53 return selectList(AiImageDO::getStatus, status,
46 54 AiImageDO::getPlatform, platform);
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java
1 1 package cn.iocoder.yudao.module.ai.dal.mysql.mindmap;
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.mindmap.vo.AiMindMapPageReqVO;
4 7 import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
5 8 import org.apache.ibatis.annotations.Mapper;
6 9  
... ... @@ -11,4 +14,13 @@ import org.apache.ibatis.annotations.Mapper;
11 14 */
12 15 @Mapper
13 16 public interface AiMindMapMapper extends BaseMapperX<AiMindMapDO> {
  17 +
  18 + default PageResult<AiMindMapDO> selectPage(AiMindMapPageReqVO reqVO) {
  19 + return selectPage(reqVO, new LambdaQueryWrapperX<AiMindMapDO>()
  20 + .eqIfPresent(AiMindMapDO::getUserId, reqVO.getUserId())
  21 + .eqIfPresent(AiMindMapDO::getPrompt, reqVO.getPrompt())
  22 + .betweenIfPresent(AiMindMapDO::getCreateTime, reqVO.getCreateTime())
  23 + .orderByDesc(AiMindMapDO::getId));
  24 + }
  25 +
14 26 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java
... ... @@ -2,9 +2,7 @@ package cn.iocoder.yudao.module.ai.service.image;
2 2  
3 3 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
6   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
7   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
  5 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
8 6 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
9 7 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
10 8 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
... ... @@ -29,6 +27,14 @@ public interface AiImageService {
29 27 PageResult<AiImageDO> getImagePageMy(Long userId, AiImagePageReqVO pageReqVO);
30 28  
31 29 /**
  30 + * 获取公开的绘图分页
  31 + *
  32 + * @param pageReqVO 分页条件
  33 + * @return 绘图分页
  34 + */
  35 + PageResult<AiImageDO> getImagePagePublic(AiImagePublicPageReqVO pageReqVO);
  36 +
  37 + /**
32 38 * 获得绘图记录
33 39 *
34 40 * @param id 绘图编号
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
... ... @@ -12,9 +12,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
12 12 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
13 13 import cn.iocoder.yudao.framework.common.pojo.PageResult;
14 14 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
15   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
16   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
17   -import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
  15 +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
18 16 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
19 17 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
20 18 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
... ... @@ -71,6 +69,11 @@ public class AiImageServiceImpl implements AiImageService {
71 69 }
72 70  
73 71 @Override
  72 + public PageResult<AiImageDO> getImagePagePublic(AiImagePublicPageReqVO pageReqVO) {
  73 + return imageMapper.selectPage(pageReqVO);
  74 + }
  75 +
  76 + @Override
74 77 public AiImageDO getImage(Long id) {
75 78 return imageMapper.selectById(id);
76 79 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.service.knowledge;
  2 +
  3 +/**
  4 + * AI 知识库 Service 接口
  5 + *
  6 + * @author xiaoxin
  7 + */
  8 +public interface DocService {
  9 +
  10 + /**
  11 + * 向量化文档
  12 + */
  13 + void embeddingDoc();
  14 +
  15 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.ai.service.knowledge;
  2 +
  3 +import jakarta.annotation.Resource;
  4 +import lombok.extern.slf4j.Slf4j;
  5 +import org.springframework.ai.document.Document;
  6 +import org.springframework.ai.reader.tika.TikaDocumentReader;
  7 +import org.springframework.ai.transformer.splitter.TokenTextSplitter;
  8 +import org.springframework.ai.vectorstore.RedisVectorStore;
  9 +import org.springframework.beans.factory.annotation.Value;
  10 +
  11 +import java.util.List;
  12 +
  13 +/**
  14 + * AI 知识库 Service 实现类
  15 + *
  16 + * @author xiaoxin
  17 + */
  18 +//@Service // TODO 芋艿:临时注释,避免无法启动
  19 +@Slf4j
  20 +public class DocServiceImpl implements DocService {
  21 +
  22 + @Resource
  23 + private RedisVectorStore vectorStore;
  24 + @Resource
  25 + private TokenTextSplitter tokenTextSplitter;
  26 +
  27 + // TODO @xin 临时测试用,后续删
  28 + @Value("classpath:/webapp/test/Fel.pdf")
  29 + private org.springframework.core.io.Resource data;
  30 +
  31 + @Override
  32 + public void embeddingDoc() {
  33 + // 读取文件
  34 + TikaDocumentReader loader = new TikaDocumentReader(data);
  35 + List<Document> documents = loader.get();
  36 + // 文档分段
  37 + List<Document> segments = tokenTextSplitter.apply(documents);
  38 + // 向量化并存储
  39 + vectorStore.add(segments);
  40 + }
  41 +
  42 +}
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java
1 1 package cn.iocoder.yudao.module.ai.service.mindmap;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 5 import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
  6 +import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
  7 +import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
5 8 import reactor.core.publisher.Flux;
6 9  
7 10 /**
... ... @@ -20,4 +23,19 @@ public interface AiMindMapService {
20 23 */
21 24 Flux<CommonResult<String>> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId);
22 25  
  26 + /**
  27 + * 删除思维导图
  28 + *
  29 + * @param id 编号
  30 + */
  31 + void deleteMindMap(Long id);
  32 +
  33 + /**
  34 + * 获得思维导图分页
  35 + *
  36 + * @param pageReqVO 分页查询
  37 + * @return 思维导图分页
  38 + */
  39 + PageResult<AiMindMapDO> getMindMapPage(AiMindMapPageReqVO pageReqVO);
  40 +
23 41 }
... ...
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java
... ... @@ -6,9 +6,11 @@ import cn.hutool.core.util.StrUtil;
6 6 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
7 7 import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
8 8 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  9 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
9 10 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
10 11 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
11 12 import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
  13 +import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
12 14 import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
13 15 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
14 16 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
... ... @@ -33,8 +35,10 @@ import reactor.core.publisher.Flux;
33 35 import java.util.ArrayList;
34 36 import java.util.List;
35 37  
  38 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
36 39 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
37 40 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  41 +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MIND_MAP_NOT_EXISTS;
38 42  
39 43 /**
40 44 * AI 思维导图 Service 实现类
... ... @@ -57,10 +61,10 @@ public class AiMindMapServiceImpl implements AiMindMapService {
57 61  
58 62 @Override
59 63 public Flux<CommonResult<String>> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId) {
60   - // 1. 获取脑图模型。尝试获取思维导图助手角色,如果没有则使用默认模型
  64 + // 1. 获取导图模型。尝试获取思维导图助手角色,如果没有则使用默认模型
61 65 AiChatRoleDO role = CollUtil.getFirst(
62 66 chatRoleService.getChatRoleListByName(AiChatRoleEnum.AI_MIND_MAP_ROLE.getName()));
63   - // 1.1 获取脑图执行模型
  67 + // 1.1 获取导图执行模型
64 68 AiChatModelDO model = getModel(role);
65 69 // 1.2 获取角色设定消息
66 70 String systemMessage = role != null && StrUtil.isNotBlank(role.getSystemMessage())
... ... @@ -131,4 +135,23 @@ public class AiMindMapServiceImpl implements AiMindMapService {
131 135 return model;
132 136 }
133 137  
  138 + @Override
  139 + public void deleteMindMap(Long id) {
  140 + // 校验存在
  141 + validateMindMapExists(id);
  142 + // 删除
  143 + mindMapMapper.deleteById(id);
  144 + }
  145 +
  146 + private void validateMindMapExists(Long id) {
  147 + if (mindMapMapper.selectById(id) == null) {
  148 + throw exception(MIND_MAP_NOT_EXISTS);
  149 + }
  150 + }
  151 +
  152 + @Override
  153 + public PageResult<AiMindMapDO> getMindMapPage(AiMindMapPageReqVO pageReqVO) {
  154 + return mindMapMapper.selectPage(pageReqVO);
  155 + }
  156 +
134 157 }
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
... ... @@ -23,7 +23,6 @@
23 23 <artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
24 24 <version>${spring-ai.version}</version>
25 25 </dependency>
26   -
27 26 <dependency>
28 27 <groupId>org.springframework.ai</groupId>
29 28 <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
... ... @@ -31,6 +30,11 @@
31 30 </dependency>
32 31 <dependency>
33 32 <groupId>org.springframework.ai</groupId>
  33 + <artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId>
  34 + <version>${spring-ai.version}</version>
  35 + </dependency>
  36 + <dependency>
  37 + <groupId>org.springframework.ai</groupId>
34 38 <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
35 39 <version>${spring-ai.version}</version>
36 40 </dependency>
... ... @@ -40,6 +44,30 @@
40 44 <version>${spring-ai.version}</version>
41 45 </dependency>
42 46  
  47 + <!-- 向量化,基于 Redis 存储,Tika 解析内容 -->
  48 + <dependency>
  49 + <groupId>org.springframework.ai</groupId>
  50 + <artifactId>spring-ai-transformers-spring-boot-starter</artifactId>
  51 + <version>${spring-ai.version}</version>
  52 + </dependency>
  53 + <dependency>
  54 + <groupId>org.springframework.ai</groupId>
  55 + <artifactId>spring-ai-tika-document-reader</artifactId>
  56 + <version>${spring-ai.version}</version>
  57 + </dependency>
  58 + <dependency>
  59 + <groupId>org.springframework.ai</groupId>
  60 + <artifactId>spring-ai-redis-store</artifactId>
  61 + <version>${spring-ai.version}</version>
  62 + </dependency>
  63 +
  64 + <!-- TODO @xin:引入我们项目的 starter -->
  65 + <dependency>
  66 + <groupId>org.springframework.data</groupId>
  67 + <artifactId>spring-data-redis</artifactId>
  68 + <optional>true</optional>
  69 + </dependency>
  70 +
43 71 <dependency>
44 72 <groupId>cn.iocoder.boot</groupId>
45 73 <artifactId>yudao-common</artifactId>
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
... ... @@ -10,11 +10,20 @@ import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
10 10 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions;
11 11 import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration;
12 12 import lombok.extern.slf4j.Slf4j;
  13 +import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties;
  14 +import org.springframework.ai.document.MetadataMode;
  15 +import org.springframework.ai.embedding.EmbeddingModel;
  16 +import org.springframework.ai.transformer.splitter.TokenTextSplitter;
  17 +import org.springframework.ai.transformers.TransformersEmbeddingModel;
  18 +import org.springframework.ai.vectorstore.RedisVectorStore;
13 19 import org.springframework.boot.autoconfigure.AutoConfiguration;
14 20 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
  21 +import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
15 22 import org.springframework.boot.context.properties.EnableConfigurationProperties;
16 23 import org.springframework.context.annotation.Bean;
17 24 import org.springframework.context.annotation.Import;
  25 +import org.springframework.context.annotation.Lazy;
  26 +import redis.clients.jedis.JedisPooled;
18 27  
19 28 /**
20 29 * 芋道 AI 自动配置
... ... @@ -73,4 +82,36 @@ public class YudaoAiAutoConfiguration {
73 82 return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl());
74 83 }
75 84  
  85 + // ========== rag 相关 ==========
  86 + @Bean
  87 + @Lazy // TODO 芋艿:临时注释,避免无法启动
  88 + public EmbeddingModel transformersEmbeddingClient() {
  89 + return new TransformersEmbeddingModel(MetadataMode.EMBED);
  90 + }
  91 +
  92 + /**
  93 + * 我们启动有加载很多 Embedding 模型,不晓得取哪个好,先 new 个 TransformersEmbeddingModel 跑
  94 + */
  95 + @Bean
  96 + @Lazy // TODO 芋艿:临时注释,避免无法启动
  97 + public RedisVectorStore vectorStore(TransformersEmbeddingModel transformersEmbeddingModel, RedisVectorStoreProperties properties,
  98 + RedisProperties redisProperties) {
  99 + var config = RedisVectorStore.RedisVectorStoreConfig.builder()
  100 + .withIndexName(properties.getIndex())
  101 + .withPrefix(properties.getPrefix())
  102 + .build();
  103 +
  104 + RedisVectorStore redisVectorStore = new RedisVectorStore(config, transformersEmbeddingModel,
  105 + new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
  106 + properties.isInitializeSchema());
  107 + redisVectorStore.afterPropertiesSet();
  108 + return redisVectorStore;
  109 + }
  110 +
  111 + @Bean
  112 + @Lazy // TODO 芋艿:临时注释,避免无法启动
  113 + public TokenTextSplitter tokenTextSplitter() {
  114 + return new TokenTextSplitter(500, 100, 5, 10000, true);
  115 + }
  116 +
76 117 }
77 118 \ No newline at end of file
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java
... ... @@ -22,7 +22,8 @@ public enum AiPlatformEnum {
22 22  
23 23 // ========== 国外平台 ==========
24 24  
25   - OPENAI("OpenAI", "OpenAI"),
  25 + OPENAI("OpenAI", "OpenAI"), // OpenAI 官方
  26 + AZURE_OPENAI("AzureOpenAI", "AzureOpenAI"), // OpenAI 微软
26 27 OLLAMA("Ollama", "Ollama"),
27 28  
28 29 STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java
... ... @@ -21,6 +21,10 @@ import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel;
21 21 import com.alibaba.cloud.ai.tongyi.image.TongYiImagesProperties;
22 22 import com.alibaba.dashscope.aigc.generation.Generation;
23 23 import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
  24 +import com.azure.ai.openai.OpenAIClient;
  25 +import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration;
  26 +import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties;
  27 +import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties;
24 28 import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
25 29 import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
26 30 import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration;
... ... @@ -31,6 +35,7 @@ import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration;
31 35 import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiChatProperties;
32 36 import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiConnectionProperties;
33 37 import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiImageProperties;
  38 +import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
34 39 import org.springframework.ai.chat.model.ChatModel;
35 40 import org.springframework.ai.image.ImageModel;
36 41 import org.springframework.ai.model.function.FunctionCallbackContext;
... ... @@ -82,6 +87,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
82 87 return buildXingHuoChatModel(apiKey);
83 88 case OPENAI:
84 89 return buildOpenAiChatModel(apiKey, url);
  90 + case AZURE_OPENAI:
  91 + return buildAzureOpenAiChatModel(apiKey, url);
85 92 case OLLAMA:
86 93 return buildOllamaChatModel(url);
87 94 default:
... ... @@ -106,6 +113,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
106 113 return SpringUtil.getBean(XingHuoChatModel.class);
107 114 case OPENAI:
108 115 return SpringUtil.getBean(OpenAiChatModel.class);
  116 + case AZURE_OPENAI:
  117 + return SpringUtil.getBean(AzureOpenAiChatModel.class);
109 118 case OLLAMA:
110 119 return SpringUtil.getBean(OllamaChatModel.class);
111 120 default:
... ... @@ -269,6 +278,21 @@ public class AiModelFactoryImpl implements AiModelFactory {
269 278 }
270 279  
271 280 /**
  281 + * 可参考 {@link AzureOpenAiAutoConfiguration}
  282 + */
  283 + private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) {
  284 + AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration();
  285 + // 创建 OpenAIClient 对象
  286 + AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties();
  287 + connectionProperties.setApiKey(apiKey);
  288 + connectionProperties.setEndpoint(url);
  289 + OpenAIClient openAIClient = azureOpenAiAutoConfiguration.openAIClient(connectionProperties);
  290 + // 获取 AzureOpenAiChatProperties 对象
  291 + AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class);
  292 + return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, null, null);
  293 + }
  294 +
  295 + /**
272 296 * 可参考 {@link OpenAiAutoConfiguration}
273 297 */
274 298 private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) {
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java
... ... @@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
5 5 import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions;
6 6 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions;
7 7 import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
  8 +import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
8 9 import org.springframework.ai.chat.messages.*;
9 10 import org.springframework.ai.chat.prompt.ChatOptions;
10 11 import org.springframework.ai.ollama.api.OllamaOptions;
... ... @@ -35,6 +36,9 @@ public class AiUtils {
35 36 return XingHuoChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build();
36 37 case OPENAI:
37 38 return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
  39 + case AZURE_OPENAI:
  40 + // TODO 芋艿:貌似没 model 字段???!
  41 + return AzureOpenAiChatOptions.builder().withDeploymentName(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
38 42 case OLLAMA:
39 43 return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
40 44 default:
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.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 +package org.springframework.ai.autoconfigure.vectorstore.redis;
  17 +
  18 +import org.springframework.ai.embedding.EmbeddingModel;
  19 +import org.springframework.ai.vectorstore.RedisVectorStore;
  20 +import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig;
  21 +import org.springframework.boot.autoconfigure.AutoConfiguration;
  22 +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
  23 +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
  24 +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
  25 +import org.springframework.boot.context.properties.EnableConfigurationProperties;
  26 +import org.springframework.context.annotation.Bean;
  27 +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
  28 +import redis.clients.jedis.JedisPooled;
  29 +
  30 +/**
  31 + * TODO @xin 先拿 spring-ai 最新代码覆盖,1.0.0-M1 跟 redis 自动配置会冲突
  32 + *
  33 + * TODO 这个官方,有说啥时候 fix 哇?
  34 + *
  35 + * @author Christian Tzolov
  36 + * @author Eddú Meléndez
  37 + */
  38 +@AutoConfiguration(after = RedisAutoConfiguration.class)
  39 +@ConditionalOnClass({JedisPooled.class, JedisConnectionFactory.class, RedisVectorStore.class, EmbeddingModel.class})
  40 +//@ConditionalOnBean(JedisConnectionFactory.class)
  41 +@EnableConfigurationProperties(RedisVectorStoreProperties.class)
  42 +public class RedisVectorStoreAutoConfiguration {
  43 +
  44 + @Bean
  45 + @ConditionalOnMissingBean
  46 + public RedisVectorStore vectorStore(EmbeddingModel embeddingModel, RedisVectorStoreProperties properties,
  47 + JedisConnectionFactory jedisConnectionFactory) {
  48 +
  49 + var config = RedisVectorStoreConfig.builder()
  50 + .withIndexName(properties.getIndex())
  51 + .withPrefix(properties.getPrefix())
  52 + .build();
  53 +
  54 + return new RedisVectorStore(config, embeddingModel,
  55 + new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()),
  56 + properties.isInitializeSchema());
  57 + }
  58 +
  59 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.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 +package org.springframework.ai.vectorstore;
  17 +
  18 +import org.slf4j.Logger;
  19 +import org.slf4j.LoggerFactory;
  20 +import org.springframework.ai.document.Document;
  21 +import org.springframework.ai.embedding.EmbeddingModel;
  22 +import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
  23 +import org.springframework.beans.factory.InitializingBean;
  24 +import org.springframework.util.Assert;
  25 +import org.springframework.util.CollectionUtils;
  26 +import redis.clients.jedis.JedisPooled;
  27 +import redis.clients.jedis.Pipeline;
  28 +import redis.clients.jedis.json.Path2;
  29 +import redis.clients.jedis.search.*;
  30 +import redis.clients.jedis.search.Schema.FieldType;
  31 +import redis.clients.jedis.search.schemafields.*;
  32 +import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm;
  33 +
  34 +import java.text.MessageFormat;
  35 +import java.util.*;
  36 +import java.util.function.Function;
  37 +import java.util.function.Predicate;
  38 +import java.util.stream.Collectors;
  39 +
  40 +/**
  41 + * The RedisVectorStore is for managing and querying vector data in a Redis database. It
  42 + * offers functionalities like adding, deleting, and performing similarity searches on
  43 + * documents.
  44 + *
  45 + * The store utilizes RedisJSON and RedisSearch to handle JSON documents and to index and
  46 + * search vector data. It supports various vector algorithms (e.g., FLAT, HSNW) for
  47 + * efficient similarity searches. Additionally, it allows for custom metadata fields in
  48 + * the documents to be stored alongside the vector and content data.
  49 + *
  50 + * This class requires a RedisVectorStoreConfig configuration object for initialization,
  51 + * which includes settings like Redis URI, index name, field names, and vector algorithms.
  52 + * It also requires an EmbeddingModel to convert documents into embeddings before storing
  53 + * them.
  54 + *
  55 + * @author Julien Ruaux
  56 + * @author Christian Tzolov
  57 + * @author Eddú Meléndez
  58 + * @see VectorStore
  59 + * @see RedisVectorStoreConfig
  60 + * @see EmbeddingModel
  61 + */
  62 +public class RedisVectorStore implements VectorStore, InitializingBean {
  63 +
  64 + public enum Algorithm {
  65 +
  66 + FLAT, HSNW
  67 +
  68 + }
  69 +
  70 + public record MetadataField(String name, FieldType fieldType) {
  71 +
  72 + public static MetadataField text(String name) {
  73 + return new MetadataField(name, FieldType.TEXT);
  74 + }
  75 +
  76 + public static MetadataField numeric(String name) {
  77 + return new MetadataField(name, FieldType.NUMERIC);
  78 + }
  79 +
  80 + public static MetadataField tag(String name) {
  81 + return new MetadataField(name, FieldType.TAG);
  82 + }
  83 +
  84 + }
  85 +
  86 + /**
  87 + * Configuration for the Redis vector store.
  88 + */
  89 + public static final class RedisVectorStoreConfig {
  90 +
  91 + private final String indexName;
  92 +
  93 + private final String prefix;
  94 +
  95 + private final String contentFieldName;
  96 +
  97 + private final String embeddingFieldName;
  98 +
  99 + private final Algorithm vectorAlgorithm;
  100 +
  101 + private final List<MetadataField> metadataFields;
  102 +
  103 + private RedisVectorStoreConfig() {
  104 + this(builder());
  105 + }
  106 +
  107 + private RedisVectorStoreConfig(Builder builder) {
  108 + this.indexName = builder.indexName;
  109 + this.prefix = builder.prefix;
  110 + this.contentFieldName = builder.contentFieldName;
  111 + this.embeddingFieldName = builder.embeddingFieldName;
  112 + this.vectorAlgorithm = builder.vectorAlgorithm;
  113 + this.metadataFields = builder.metadataFields;
  114 + }
  115 +
  116 + /**
  117 + * Start building a new configuration.
  118 + * @return The entry point for creating a new configuration.
  119 + */
  120 + public static Builder builder() {
  121 +
  122 + return new Builder();
  123 + }
  124 +
  125 + /**
  126 + * {@return the default config}
  127 + */
  128 + public static RedisVectorStoreConfig defaultConfig() {
  129 +
  130 + return builder().build();
  131 + }
  132 +
  133 + public static class Builder {
  134 +
  135 + private String indexName = DEFAULT_INDEX_NAME;
  136 +
  137 + private String prefix = DEFAULT_PREFIX;
  138 +
  139 + private String contentFieldName = DEFAULT_CONTENT_FIELD_NAME;
  140 +
  141 + private String embeddingFieldName = DEFAULT_EMBEDDING_FIELD_NAME;
  142 +
  143 + private Algorithm vectorAlgorithm = DEFAULT_VECTOR_ALGORITHM;
  144 +
  145 + private List<MetadataField> metadataFields = new ArrayList<>();
  146 +
  147 + private Builder() {
  148 + }
  149 +
  150 + /**
  151 + * Configures the Redis index name to use.
  152 + * @param name the index name to use
  153 + * @return this builder
  154 + */
  155 + public Builder withIndexName(String name) {
  156 + this.indexName = name;
  157 + return this;
  158 + }
  159 +
  160 + /**
  161 + * Configures the Redis key prefix to use (default: "embedding:").
  162 + * @param prefix the prefix to use
  163 + * @return this builder
  164 + */
  165 + public Builder withPrefix(String prefix) {
  166 + this.prefix = prefix;
  167 + return this;
  168 + }
  169 +
  170 + /**
  171 + * Configures the Redis content field name to use.
  172 + * @param name the content field name to use
  173 + * @return this builder
  174 + */
  175 + public Builder withContentFieldName(String name) {
  176 + this.contentFieldName = name;
  177 + return this;
  178 + }
  179 +
  180 + /**
  181 + * Configures the Redis embedding field name to use.
  182 + * @param name the embedding field name to use
  183 + * @return this builder
  184 + */
  185 + public Builder withEmbeddingFieldName(String name) {
  186 + this.embeddingFieldName = name;
  187 + return this;
  188 + }
  189 +
  190 + /**
  191 + * Configures the Redis vector algorithmto use.
  192 + * @param algorithm the vector algorithm to use
  193 + * @return this builder
  194 + */
  195 + public Builder withVectorAlgorithm(Algorithm algorithm) {
  196 + this.vectorAlgorithm = algorithm;
  197 + return this;
  198 + }
  199 +
  200 + public Builder withMetadataFields(MetadataField... fields) {
  201 + return withMetadataFields(Arrays.asList(fields));
  202 + }
  203 +
  204 + public Builder withMetadataFields(List<MetadataField> fields) {
  205 + this.metadataFields = fields;
  206 + return this;
  207 + }
  208 +
  209 + /**
  210 + * {@return the immutable configuration}
  211 + */
  212 + public RedisVectorStoreConfig build() {
  213 +
  214 + return new RedisVectorStoreConfig(this);
  215 + }
  216 +
  217 + }
  218 +
  219 + }
  220 +
  221 + private final boolean initializeSchema;
  222 +
  223 + public static final String DEFAULT_INDEX_NAME = "spring-ai-index";
  224 +
  225 + public static final String DEFAULT_CONTENT_FIELD_NAME = "content";
  226 +
  227 + public static final String DEFAULT_EMBEDDING_FIELD_NAME = "embedding";
  228 +
  229 + public static final String DEFAULT_PREFIX = "embedding:";
  230 +
  231 + public static final Algorithm DEFAULT_VECTOR_ALGORITHM = Algorithm.HSNW;
  232 +
  233 + private static final String QUERY_FORMAT = "%s=>[KNN %s @%s $%s AS %s]";
  234 +
  235 + private static final Path2 JSON_SET_PATH = Path2.of("$");
  236 +
  237 + private static final String JSON_PATH_PREFIX = "$.";
  238 +
  239 + private static final Logger logger = LoggerFactory.getLogger(RedisVectorStore.class);
  240 +
  241 + private static final Predicate<Object> RESPONSE_OK = Predicate.isEqual("OK");
  242 +
  243 + private static final Predicate<Object> RESPONSE_DEL_OK = Predicate.isEqual(1l);
  244 +
  245 + private static final String VECTOR_TYPE_FLOAT32 = "FLOAT32";
  246 +
  247 + private static final String EMBEDDING_PARAM_NAME = "BLOB";
  248 +
  249 + public static final String DISTANCE_FIELD_NAME = "vector_score";
  250 +
  251 + private static final String DEFAULT_DISTANCE_METRIC = "COSINE";
  252 +
  253 + private final JedisPooled jedis;
  254 +
  255 + private final EmbeddingModel embeddingModel;
  256 +
  257 + private final RedisVectorStoreConfig config;
  258 +
  259 + private FilterExpressionConverter filterExpressionConverter;
  260 +
  261 + public RedisVectorStore(RedisVectorStoreConfig config, EmbeddingModel embeddingModel, JedisPooled jedis,
  262 + boolean initializeSchema) {
  263 +
  264 + Assert.notNull(config, "Config must not be null");
  265 + Assert.notNull(embeddingModel, "Embedding model must not be null");
  266 + this.initializeSchema = initializeSchema;
  267 +
  268 + this.jedis = jedis;
  269 + this.embeddingModel = embeddingModel;
  270 + this.config = config;
  271 + this.filterExpressionConverter = new RedisFilterExpressionConverter(this.config.metadataFields);
  272 + }
  273 +
  274 + public JedisPooled getJedis() {
  275 + return this.jedis;
  276 + }
  277 +
  278 + @Override
  279 + public void add(List<Document> documents) {
  280 + try (Pipeline pipeline = this.jedis.pipelined()) {
  281 + for (Document document : documents) {
  282 + var embedding = this.embeddingModel.embed(document);
  283 + document.setEmbedding(embedding);
  284 +
  285 + var fields = new HashMap<String, Object>();
  286 + fields.put(this.config.embeddingFieldName, embedding);
  287 + fields.put(this.config.contentFieldName, document.getContent());
  288 + fields.putAll(document.getMetadata());
  289 + pipeline.jsonSetWithEscape(key(document.getId()), JSON_SET_PATH, fields);
  290 + }
  291 + List<Object> responses = pipeline.syncAndReturnAll();
  292 + Optional<Object> errResponse = responses.stream().filter(Predicate.not(RESPONSE_OK)).findAny();
  293 + if (errResponse.isPresent()) {
  294 + String message = MessageFormat.format("Could not add document: {0}", errResponse.get());
  295 + if (logger.isErrorEnabled()) {
  296 + logger.error(message);
  297 + }
  298 + throw new RuntimeException(message);
  299 + }
  300 + }
  301 + }
  302 +
  303 + private String key(String id) {
  304 + return this.config.prefix + id;
  305 + }
  306 +
  307 + @Override
  308 + public Optional<Boolean> delete(List<String> idList) {
  309 + try (Pipeline pipeline = this.jedis.pipelined()) {
  310 + for (String id : idList) {
  311 + pipeline.jsonDel(key(id));
  312 + }
  313 + List<Object> responses = pipeline.syncAndReturnAll();
  314 + Optional<Object> errResponse = responses.stream().filter(Predicate.not(RESPONSE_DEL_OK)).findAny();
  315 + if (errResponse.isPresent()) {
  316 + if (logger.isErrorEnabled()) {
  317 + logger.error("Could not delete document: {}", errResponse.get());
  318 + }
  319 + return Optional.of(false);
  320 + }
  321 + return Optional.of(true);
  322 + }
  323 + }
  324 +
  325 + @Override
  326 + public List<Document> similaritySearch(SearchRequest request) {
  327 +
  328 + Assert.isTrue(request.getTopK() > 0, "The number of documents to returned must be greater than zero");
  329 + Assert.isTrue(request.getSimilarityThreshold() >= 0 && request.getSimilarityThreshold() <= 1,
  330 + "The similarity score is bounded between 0 and 1; least to most similar respectively.");
  331 +
  332 + String filter = nativeExpressionFilter(request);
  333 +
  334 + String queryString = String.format(QUERY_FORMAT, filter, request.getTopK(), this.config.embeddingFieldName,
  335 + EMBEDDING_PARAM_NAME, DISTANCE_FIELD_NAME);
  336 +
  337 + List<String> returnFields = new ArrayList<>();
  338 + this.config.metadataFields.stream().map(MetadataField::name).forEach(returnFields::add);
  339 + returnFields.add(this.config.embeddingFieldName);
  340 + returnFields.add(this.config.contentFieldName);
  341 + returnFields.add(DISTANCE_FIELD_NAME);
  342 + var embedding = toFloatArray(this.embeddingModel.embed(request.getQuery()));
  343 + Query query = new Query(queryString).addParam(EMBEDDING_PARAM_NAME, RediSearchUtil.toByteArray(embedding))
  344 + .returnFields(returnFields.toArray(new String[0]))
  345 + .setSortBy(DISTANCE_FIELD_NAME, true)
  346 + .dialect(2);
  347 +
  348 + SearchResult result = this.jedis.ftSearch(this.config.indexName, query);
  349 + return result.getDocuments()
  350 + .stream()
  351 + .filter(d -> similarityScore(d) >= request.getSimilarityThreshold())
  352 + .map(this::toDocument)
  353 + .toList();
  354 + }
  355 +
  356 + private Document toDocument(redis.clients.jedis.search.Document doc) {
  357 + var id = doc.getId().substring(this.config.prefix.length());
  358 + var content = doc.hasProperty(this.config.contentFieldName) ? doc.getString(this.config.contentFieldName)
  359 + : null;
  360 + Map<String, Object> metadata = this.config.metadataFields.stream()
  361 + .map(MetadataField::name)
  362 + .filter(doc::hasProperty)
  363 + .collect(Collectors.toMap(Function.identity(), doc::getString));
  364 + metadata.put(DISTANCE_FIELD_NAME, 1 - similarityScore(doc));
  365 + return new Document(id, content, metadata);
  366 + }
  367 +
  368 + private float similarityScore(redis.clients.jedis.search.Document doc) {
  369 + return (2 - Float.parseFloat(doc.getString(DISTANCE_FIELD_NAME))) / 2;
  370 + }
  371 +
  372 + private String nativeExpressionFilter(SearchRequest request) {
  373 + if (request.getFilterExpression() == null) {
  374 + return "*";
  375 + }
  376 + return "(" + this.filterExpressionConverter.convertExpression(request.getFilterExpression()) + ")";
  377 + }
  378 +
  379 + @Override
  380 + public void afterPropertiesSet() {
  381 +
  382 + if (!this.initializeSchema) {
  383 + return;
  384 + }
  385 +
  386 + // If index already exists don't do anything
  387 + if (this.jedis.ftList().contains(this.config.indexName)) {
  388 + return;
  389 + }
  390 +
  391 + String response = this.jedis.ftCreate(this.config.indexName,
  392 + FTCreateParams.createParams().on(IndexDataType.JSON).addPrefix(this.config.prefix), schemaFields());
  393 + if (!RESPONSE_OK.test(response)) {
  394 + String message = MessageFormat.format("Could not create index: {0}", response);
  395 + throw new RuntimeException(message);
  396 + }
  397 + }
  398 +
  399 + private Iterable<SchemaField> schemaFields() {
  400 + Map<String, Object> vectorAttrs = new HashMap<>();
  401 + vectorAttrs.put("DIM", this.embeddingModel.dimensions());
  402 + vectorAttrs.put("DISTANCE_METRIC", DEFAULT_DISTANCE_METRIC);
  403 + vectorAttrs.put("TYPE", VECTOR_TYPE_FLOAT32);
  404 + List<SchemaField> fields = new ArrayList<>();
  405 + fields.add(TextField.of(jsonPath(this.config.contentFieldName)).as(this.config.contentFieldName).weight(1.0));
  406 + fields.add(VectorField.builder()
  407 + .fieldName(jsonPath(this.config.embeddingFieldName))
  408 + .algorithm(vectorAlgorithm())
  409 + .attributes(vectorAttrs)
  410 + .as(this.config.embeddingFieldName)
  411 + .build());
  412 +
  413 + if (!CollectionUtils.isEmpty(this.config.metadataFields)) {
  414 + for (MetadataField field : this.config.metadataFields) {
  415 + fields.add(schemaField(field));
  416 + }
  417 + }
  418 + return fields;
  419 + }
  420 +
  421 + private SchemaField schemaField(MetadataField field) {
  422 + String fieldName = jsonPath(field.name);
  423 + switch (field.fieldType) {
  424 + case NUMERIC:
  425 + return NumericField.of(fieldName).as(field.name);
  426 + case TAG:
  427 + return TagField.of(fieldName).as(field.name);
  428 + case TEXT:
  429 + return TextField.of(fieldName).as(field.name);
  430 + default:
  431 + throw new IllegalArgumentException(
  432 + MessageFormat.format("Field {0} has unsupported type {1}", field.name, field.fieldType));
  433 + }
  434 + }
  435 +
  436 + private VectorAlgorithm vectorAlgorithm() {
  437 + if (config.vectorAlgorithm == Algorithm.HSNW) {
  438 + return VectorAlgorithm.HNSW;
  439 + }
  440 + return VectorAlgorithm.FLAT;
  441 + }
  442 +
  443 + private String jsonPath(String field) {
  444 + return JSON_PATH_PREFIX + field;
  445 + }
  446 +
  447 + private static float[] toFloatArray(List<Double> embeddingDouble) {
  448 + float[] embeddingFloat = new float[embeddingDouble.size()];
  449 + int i = 0;
  450 + for (Double d : embeddingDouble) {
  451 + embeddingFloat[i++] = d.floatValue();
  452 + }
  453 + return embeddingFloat;
  454 + }
  455 +
  456 +}
0 457 \ No newline at end of file
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/resources/webapp/test/Fel.pdf 0 → 100755
No preview for this file type
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.ai.chat;
  2 +
  3 +import com.azure.ai.openai.OpenAIClient;
  4 +import com.azure.ai.openai.OpenAIClientBuilder;
  5 +import com.azure.core.credential.AzureKeyCredential;
  6 +import com.azure.core.util.ClientOptions;
  7 +import org.junit.jupiter.api.Disabled;
  8 +import org.junit.jupiter.api.Test;
  9 +import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
  10 +import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
  11 +import org.springframework.ai.chat.messages.Message;
  12 +import org.springframework.ai.chat.messages.SystemMessage;
  13 +import org.springframework.ai.chat.messages.UserMessage;
  14 +import org.springframework.ai.chat.model.ChatResponse;
  15 +import org.springframework.ai.chat.prompt.Prompt;
  16 +import reactor.core.publisher.Flux;
  17 +
  18 +import java.util.ArrayList;
  19 +import java.util.List;
  20 +
  21 +import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME;
  22 +
  23 +/**
  24 + * {@link AzureOpenAiChatModel} 集成测试
  25 + *
  26 + * @author 芋道源码
  27 + */
  28 +public class AzureOpenAIChatModelTests {
  29 +
  30 + private final OpenAIClient openAiApi = (new OpenAIClientBuilder())
  31 + .endpoint("https://eastusprejade.openai.azure.com")
  32 + .credential(new AzureKeyCredential("xxx"))
  33 + .clientOptions((new ClientOptions()).setApplicationId("spring-ai"))
  34 + .buildClient();
  35 + private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi,
  36 + AzureOpenAiChatOptions.builder().withDeploymentName(DEFAULT_DEPLOYMENT_NAME).build());
  37 +
  38 + @Test
  39 + @Disabled
  40 + public void testCall() {
  41 + // 准备参数
  42 + List<Message> messages = new ArrayList<>();
  43 + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  44 + messages.add(new UserMessage("1 + 1 = ?"));
  45 +
  46 + // 调用
  47 + ChatResponse response = chatModel.call(new Prompt(messages));
  48 + // 打印结果
  49 + System.out.println(response);
  50 + System.out.println(response.getResult().getOutput());
  51 + }
  52 +
  53 + @Test
  54 + @Disabled
  55 + public void testStream() {
  56 + // 准备参数
  57 + List<Message> messages = new ArrayList<>();
  58 + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
  59 + messages.add(new UserMessage("1 + 1 = ?"));
  60 +
  61 + // 调用
  62 + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
  63 + // 打印结果
  64 + flux.doOnNext(response -> {
  65 +// System.out.println(response);
  66 + System.out.println(response.getResult().getOutput());
  67 + }).then().block();
  68 + }
  69 +
  70 +}
... ...
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java
1 1 package cn.iocoder.yudao.framework.ai.chat;
2 2  
3   -import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
4 3 import org.junit.jupiter.api.Disabled;
5 4 import org.junit.jupiter.api.Test;
6 5 import org.springframework.ai.chat.messages.Message;
... ... @@ -17,7 +16,7 @@ import java.util.ArrayList;
17 16 import java.util.List;
18 17  
19 18 /**
20   - * {@link XingHuoChatModel} 集成测试
  19 + * {@link OpenAiChatModel} 集成测试
21 20 *
22 21 * @author 芋道源码
23 22 */
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -147,14 +147,22 @@ spring:
147 147  
148 148 spring:
149 149 ai:
  150 + vectorstore: # 向量存储
  151 + redis:
  152 + index: default-index
  153 + prefix: "default:"
150 154 qianfan: # 文心一言
151 155 api-key: x0cuLZ7XsaTCU08vuJWO87Lg
152 156 secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK
153 157 zhipuai: # 智谱 AI
154 158 api-key: 32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs
155   - openai:
  159 + openai: # OpenAI 官方
156 160 api-key: sk-yzKea6d8e8212c3bdd99f9f44ced1cae37c097e5aa3BTS7z
157 161 base-url: https://api.gptsapi.net
  162 + azure: # OpenAI 微软
  163 + openai:
  164 + endpoint: https://eastusprejade.openai.azure.com
  165 + api-key: xxx
158 166 ollama:
159 167 base-url: http://127.0.0.1:11434
160 168 chat:
... ...